Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 2 additions & 46 deletions gen/template_webhook_event.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -93,29 +93,7 @@ func (g *EventHandler) handleBeforeAny(ctx context.Context, deliveryID string, e
if event == nil {
return fmt.Errorf("event was empty or nil")
}
if _, ok := g.onBeforeAny[EventAnyAction]; !ok {
return nil
}
eg := new(errgroup.Group)
for _, h := range g.onBeforeAny[EventAnyAction] {
handle := h
eg.Go(func() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
err = handle(ctx, deliveryID, eventName, event)
if err != nil {
return err
}
return nil
})
}
if err := eg.Wait(); err != nil {
return err
}
return nil
return dispatch[any](ctx, deliveryID, eventName, event, g.onBeforeAny[EventAnyAction])
}

// OnAfterAny registers callbacks which are triggered after any event.
Expand Down Expand Up @@ -157,29 +135,7 @@ func (g *EventHandler) handleAfterAny(ctx context.Context, deliveryID string, ev
if event == nil {
return fmt.Errorf("event was empty or nil")
}
if _, ok := g.onAfterAny[EventAnyAction]; !ok {
return nil
}
eg := new(errgroup.Group)
for _, h := range g.onAfterAny[EventAnyAction] {
handle := h
eg.Go(func() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
err = handle(ctx, deliveryID, eventName, event)
if err != nil {
return err
}
return nil
})
}
if err := eg.Wait(); err != nil {
return err
}
return nil
return dispatch[any](ctx, deliveryID, eventName, event, g.onAfterAny[EventAnyAction])
}

// ErrorEventHandleFunc represents a generic callback function which receives any event and an error thrown by
Expand Down
56 changes: 5 additions & 51 deletions gen/template_webhook_event_types.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"context"
"fmt"
"{{ .GoGithubImport }}"
"golang.org/x/sync/errgroup"
)

{{ range $_, $webhook := .Webhooks }}
Expand Down Expand Up @@ -96,33 +95,10 @@ func (g *EventHandler) handle{{ $action.Handler }}(ctx context.Context, delivery
*event.Action,
)
}
eg := new(errgroup.Group)
for _, action := range []string{
{{ $action.Handler }}Action,
{{ $webhook.Event }}AnyAction,
} {
if _, ok := g.on{{ $webhook.Event }}[action]; ok {
for _, h := range g.on{{ $webhook.Event }}[action] {
handle := h
eg.Go(func() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
err = handle(ctx, deliveryID, eventName, event)
if err != nil {
return err
}
return nil
})
}
}
}
if err := eg.Wait(); err != nil {
return err
}
return nil
return dispatch[*github.{{ $webhook.Event }}](ctx, deliveryID, eventName, event,
g.on{{ $webhook.Event }}[{{ $action.Handler }}Action],
g.on{{ $webhook.Event }}[{{ $webhook.Event }}AnyAction],
)
}

{{ end }}
Expand Down Expand Up @@ -177,29 +153,7 @@ func (g *EventHandler) handle{{ $webhook.Event }}Any(ctx context.Context, delive
if event == nil {
return fmt.Errorf("event was empty or nil")
}
if _, ok := g.on{{ $webhook.Event }}[{{ $webhook.Event }}AnyAction]; !ok {
return nil
}
eg := new(errgroup.Group)
for _, h := range g.on{{ $webhook.Event }}[{{ $webhook.Event }}AnyAction] {
handle := h
eg.Go(func() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
err = handle(ctx, deliveryID, eventName, event)
if err != nil {
return err
}
return nil
})
}
if err := eg.Wait(); err != nil {
return err
}
return nil
return dispatch[*github.{{ $webhook.Event }}](ctx, deliveryID, eventName, event, g.on{{ $webhook.Event }}[{{ $webhook.Event }}AnyAction])
}

// {{ $webhook.Event }} handles github.{{ $webhook.Event }}.
Expand Down
38 changes: 38 additions & 0 deletions githubevents/dispatch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2022 The GithubEvents Authors. All rights reserved.
// Use of this source code is governed by the MIT License
// that can be found in the LICENSE file.

package githubevents

import (
"context"
"fmt"

"golang.org/x/sync/errgroup"
)

// dispatch runs every callback across the given groups in parallel, recovers
// from panics as errors, and returns the first non-nil error. It is the single
// implementation of the concurrency semantics shared by all generated handlers.
func dispatch[T any, F ~func(ctx context.Context, deliveryID, eventName string, event T) error](
ctx context.Context,
deliveryID string,
eventName string,
event T,
groups ...[]F,
) error {
eg := new(errgroup.Group)
for _, group := range groups {
for _, h := range group {
eg.Go(func() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
return h(ctx, deliveryID, eventName, event)
})
}
}
return eg.Wait()
}
81 changes: 81 additions & 0 deletions githubevents/dispatch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package githubevents

import (
"context"
"errors"
"strings"
"sync/atomic"
"testing"

"github.com/google/go-github/v89/github"
)

func labelEvent(action string) *github.LabelEvent {
return &github.LabelEvent{Action: github.Ptr(action)}
}

// panic in one callback surfaces as a recovered-from-panic error.
func TestDispatchRecoversPanic(t *testing.T) {
g := New("")
g.OnLabelEventCreated(func(ctx context.Context, id, name string, e *github.LabelEvent) error {
panic("boom")
})
err := g.LabelEvent(context.Background(), "id", "label", labelEvent("created"))
if err == nil || !strings.Contains(err.Error(), "recovered from panic:") {
t.Fatalf("want recovered-from-panic error, got %v", err)
}
}

// every registered callback runs (parallel fan-out).
func TestDispatchRunsAll(t *testing.T) {
g := New("")
var n atomic.Int32
inc := func(ctx context.Context, id, name string, e *github.LabelEvent) error {
n.Add(1)
return nil
}
g.OnLabelEventCreated(inc, inc, inc)
if err := g.LabelEvent(context.Background(), "id", "label", labelEvent("created")); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n.Load() != 3 {
t.Fatalf("want 3 callbacks run, got %d", n.Load())
}
}

// a returned error propagates.
func TestDispatchPropagatesError(t *testing.T) {
g := New("")
want := errors.New("nope")
g.OnLabelEventCreated(func(ctx context.Context, id, name string, e *github.LabelEvent) error {
return want
})
if err := g.LabelEvent(context.Background(), "id", "label", labelEvent("created")); !errors.Is(err, want) {
t.Fatalf("want %v to propagate, got %v", want, err)
}
}

// wrong / empty action is rejected by the action handler contract.
func TestDispatchRejectsEmptyAction(t *testing.T) {
g := New("")
err := g.LabelEvent(context.Background(), "id", "label", &github.LabelEvent{})
if err == nil || !strings.Contains(err.Error(), "empty or nil") {
t.Fatalf("want empty-action error, got %v", err)
}
}

// the Any path (event without a matching action handler) still fans out.
func TestDispatchAnyPath(t *testing.T) {
g := New("")
var n atomic.Int32
g.OnLabelEventAny(func(ctx context.Context, id, name string, e *github.LabelEvent) error {
n.Add(1)
return nil
})
if err := g.LabelEvent(context.Background(), "id", "label", labelEvent("created")); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n.Load() != 1 {
t.Fatalf("want Any callback to run once, got %d", n.Load())
}
}
48 changes: 2 additions & 46 deletions githubevents/events.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading