├── vendor.mod ├── errors.go ├── .gitignore ├── filter_test.go ├── event.go ├── queue_test.go ├── MAINTAINERS ├── filter.go ├── channel.go ├── vendor.sum ├── SECURITY.md ├── channel_test.go ├── .github └── workflows │ ├── codeql.yml │ └── ci.yml ├── common_test.go ├── retry_test.go ├── queue.go ├── CONTRIBUTING.md ├── broadcast_test.go ├── README.md ├── broadcast.go ├── retry.go └── LICENSE /vendor.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker/go-events 2 | 3 | go 1.13 4 | 5 | require github.com/sirupsen/logrus v1.9.3 6 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import "fmt" 4 | 5 | var ( 6 | // ErrSinkClosed is returned if a write is issued to a sink that has been 7 | // closed. If encountered, the error should be considered terminal and 8 | // retries will not be successful. 9 | ErrSinkClosed = fmt.Errorf("events: sink closed") 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /filter_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import "testing" 4 | 5 | func TestFilter(t *testing.T) { 6 | const nevents = 100 7 | ts := newTestSink(t, nevents/2) 8 | filter := NewFilter(ts, MatcherFunc(func(event Event) bool { 9 | i, ok := event.(int) 10 | return ok && i%2 == 0 11 | })) 12 | 13 | for i := 0; i < nevents; i++ { 14 | if err := filter.Write(i); err != nil { 15 | t.Fatalf("unexpected error writing event: %v", err) 16 | } 17 | } 18 | 19 | checkClose(t, filter) 20 | 21 | } 22 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | // Event marks items that can be sent as events. 4 | type Event interface{} 5 | 6 | // Sink accepts and sends events. 7 | type Sink interface { 8 | // Write an event to the Sink. If no error is returned, the caller will 9 | // assume that all events have been committed to the sink. If an error is 10 | // received, the caller may retry sending the event. 11 | Write(event Event) error 12 | 13 | // Close the sink, possibly waiting for pending events to flush. 14 | Close() error 15 | } 16 | -------------------------------------------------------------------------------- /queue_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestQueue(t *testing.T) { 11 | const nevents = 1000 12 | 13 | ts := newTestSink(t, nevents) 14 | eq := NewQueue( 15 | // delayed sync simulates destination slower than channel comms 16 | &delayedSink{ 17 | Sink: ts, 18 | delay: time.Millisecond * 1, 19 | }) 20 | time.Sleep(10 * time.Millisecond) // let's queue settle to wait conidition. 21 | 22 | var ( 23 | wg sync.WaitGroup 24 | asyncErr error 25 | once sync.Once 26 | ) 27 | for i := 1; i <= nevents; i++ { 28 | wg.Add(1) 29 | go func(event Event) { 30 | defer wg.Done() 31 | 32 | if err := eq.Write(event); err != nil { 33 | once.Do(func() { 34 | asyncErr = fmt.Errorf("error writing event(%v): %v", event, err) 35 | }) 36 | } 37 | }(fmt.Sprintf("event-%d", i)) 38 | } 39 | 40 | wg.Wait() 41 | 42 | if asyncErr != nil { 43 | t.Fatalf("expected nil error, got %v", asyncErr) 44 | } 45 | 46 | checkClose(t, eq) 47 | 48 | ts.mu.Lock() 49 | defer ts.mu.Unlock() 50 | 51 | if len(ts.events) != nevents { 52 | t.Fatalf("events did not make it to the sink: %d != %d", len(ts.events), 1000) 53 | } 54 | 55 | if !ts.closed { 56 | t.Fatalf("sink should have been closed") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | # go-events maintainers file 2 | # 3 | # This file describes who runs the docker/go-events project and how. 4 | # This is a living document - if you see something out of date or missing, speak up! 5 | # 6 | # It is structured to be consumable by both humans and programs. 7 | # To extract its contents programmatically, use any TOML-compliant parser. 8 | # 9 | # This file is compiled into the MAINTAINERS file in docker/opensource. 10 | # 11 | [Org] 12 | [Org."Core maintainers"] 13 | people = [ 14 | "aaronlehmann", 15 | "aluzzardi", 16 | "lk4d4", 17 | "stevvooe", 18 | ] 19 | 20 | [people] 21 | 22 | # A reference list of all people associated with the project. 23 | # All other sections should refer to people by their canonical key 24 | # in the people section. 25 | 26 | # ADD YOURSELF HERE IN ALPHABETICAL ORDER 27 | 28 | [people.aaronlehmann] 29 | Name = "Aaron Lehmann" 30 | Email = "aaron.lehmann@docker.com" 31 | GitHub = "aaronlehmann" 32 | 33 | [people.aluzzardi] 34 | Name = "Andrea Luzzardi" 35 | Email = "al@docker.com" 36 | GitHub = "aluzzardi" 37 | 38 | [people.lk4d4] 39 | Name = "Alexander Morozov" 40 | Email = "lk4d4@docker.com" 41 | GitHub = "lk4d4" 42 | 43 | [people.stevvooe] 44 | Name = "Stephen Day" 45 | Email = "stephen.day@docker.com" 46 | GitHub = "stevvooe" 47 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | // Matcher matches events. 4 | type Matcher interface { 5 | Match(event Event) bool 6 | } 7 | 8 | // MatcherFunc implements matcher with just a function. 9 | type MatcherFunc func(event Event) bool 10 | 11 | // Match calls the wrapped function. 12 | func (fn MatcherFunc) Match(event Event) bool { 13 | return fn(event) 14 | } 15 | 16 | // Filter provides an event sink that sends only events that are accepted by a 17 | // Matcher. No methods on filter are goroutine safe. 18 | type Filter struct { 19 | dst Sink 20 | matcher Matcher 21 | closed bool 22 | } 23 | 24 | // NewFilter returns a new filter that will send to events to dst that return 25 | // true for Matcher. 26 | func NewFilter(dst Sink, matcher Matcher) Sink { 27 | return &Filter{dst: dst, matcher: matcher} 28 | } 29 | 30 | // Write an event to the filter. 31 | func (f *Filter) Write(event Event) error { 32 | if f.closed { 33 | return ErrSinkClosed 34 | } 35 | 36 | if f.matcher.Match(event) { 37 | return f.dst.Write(event) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | // Close the filter and allow no more events to pass through. 44 | func (f *Filter) Close() error { 45 | // TODO(stevvooe): Not all sinks should have Close. 46 | if f.closed { 47 | return nil 48 | } 49 | 50 | f.closed = true 51 | return f.dst.Close() 52 | } 53 | -------------------------------------------------------------------------------- /channel.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // Channel provides a sink that can be listened on. The writer and channel 9 | // listener must operate in separate goroutines. 10 | // 11 | // Consumers should listen on Channel.C until Closed is closed. 12 | type Channel struct { 13 | C chan Event 14 | 15 | closed chan struct{} 16 | once sync.Once 17 | } 18 | 19 | // NewChannel returns a channel. If buffer is zero, the channel is 20 | // unbuffered. 21 | func NewChannel(buffer int) *Channel { 22 | return &Channel{ 23 | C: make(chan Event, buffer), 24 | closed: make(chan struct{}), 25 | } 26 | } 27 | 28 | // Done returns a channel that will always proceed once the sink is closed. 29 | func (ch *Channel) Done() chan struct{} { 30 | return ch.closed 31 | } 32 | 33 | // Write the event to the channel. Must be called in a separate goroutine from 34 | // the listener. 35 | func (ch *Channel) Write(event Event) error { 36 | select { 37 | case ch.C <- event: 38 | return nil 39 | case <-ch.closed: 40 | return ErrSinkClosed 41 | } 42 | } 43 | 44 | // Close the channel sink. 45 | func (ch *Channel) Close() error { 46 | ch.once.Do(func() { 47 | close(ch.closed) 48 | }) 49 | 50 | return nil 51 | } 52 | 53 | func (ch *Channel) String() string { 54 | // Serialize a copy of the Channel that doesn't contain the sync.Once, 55 | // to avoid a data race. 56 | ch2 := map[string]interface{}{ 57 | "C": ch.C, 58 | "closed": ch.closed, 59 | } 60 | return fmt.Sprint(ch2) 61 | } 62 | -------------------------------------------------------------------------------- /vendor.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 7 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 10 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 12 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 16 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 17 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | The maintainers of the Docker Events package take security seriously. If you discover 4 | a security issue, please bring it to their attention right away! 5 | 6 | ## Reporting a Vulnerability 7 | 8 | Please **DO NOT** file a public issue, instead send your report privately 9 | to [security@docker.com](mailto:security@docker.com). 10 | 11 | Reporter(s) can expect a response within 72 hours, acknowledging the issue was 12 | received. 13 | 14 | ## Review Process 15 | 16 | After receiving the report, an initial triage and technical analysis is 17 | performed to confirm the report and determine its scope. We may request 18 | additional information in this stage of the process. 19 | 20 | Once a reviewer has confirmed the relevance of the report, a draft security 21 | advisory will be created on GitHub. The draft advisory will be used to discuss 22 | the issue with maintainers, the reporter(s), and where applicable, other 23 | affected parties under embargo. 24 | 25 | If the vulnerability is accepted, a timeline for developing a patch, public 26 | disclosure, and patch release will be determined. If there is an embargo period 27 | on public disclosure before the patch release, the reporter(s) are expected to 28 | participate in the discussion of the timeline and abide by agreed upon dates 29 | for public disclosure. 30 | 31 | ## Accreditation 32 | 33 | Security reports are greatly appreciated and we will publicly thank you, 34 | although we will keep your name confidential if you request it. We also like to 35 | send gifts - if you're into swag, make sure to let us know. We do not currently 36 | offer a paid security bounty program at this time. 37 | -------------------------------------------------------------------------------- /channel_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "testing" 7 | ) 8 | 9 | func TestChannel(t *testing.T) { 10 | const nevents = 100 11 | 12 | errCh := make(chan error) 13 | sink := NewChannel(0) 14 | 15 | go func() { 16 | var ( 17 | wg sync.WaitGroup 18 | asyncErr error 19 | once sync.Once 20 | ) 21 | for i := 1; i <= nevents; i++ { 22 | wg.Add(1) 23 | go func(event Event) { 24 | defer wg.Done() 25 | 26 | if err := sink.Write(event); err != nil { 27 | once.Do(func() { 28 | asyncErr = fmt.Errorf("error writing event(%v): %v", event, err) 29 | }) 30 | } 31 | }(fmt.Sprintf("event-%d", i)) 32 | } 33 | 34 | wg.Wait() 35 | 36 | if asyncErr != nil { 37 | errCh <- asyncErr 38 | return 39 | } 40 | 41 | sink.Close() 42 | 43 | // now send another bunch of events and ensure we stay closed 44 | for i := 1; i <= nevents; i++ { 45 | wg.Add(1) 46 | go func(event Event) { 47 | defer wg.Done() 48 | 49 | if err := sink.Write(event); err != ErrSinkClosed { 50 | once.Do(func() { 51 | asyncErr = fmt.Errorf("expected %v, got %v", ErrSinkClosed, err) 52 | }) 53 | } 54 | }(fmt.Sprintf("event-%d", i)) 55 | } 56 | 57 | wg.Wait() 58 | 59 | if asyncErr != nil { 60 | errCh <- asyncErr 61 | } 62 | }() 63 | 64 | var received int 65 | loop: 66 | for { 67 | select { 68 | case <-sink.C: 69 | received++ 70 | case err := <-errCh: 71 | t.Fatal(err) 72 | case <-sink.Done(): 73 | break loop 74 | } 75 | } 76 | 77 | close(errCh) 78 | 79 | sink.Close() 80 | _, ok := <-sink.Done() // test will timeout if this hangs 81 | if ok { 82 | t.Fatalf("done should be a closed channel") 83 | } 84 | 85 | if received != nevents { 86 | t.Fatalf("events did not make it through sink: %v != %v", received, nevents) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: codeql 2 | 3 | # Default to 'contents: read', which grants actions to read commits. 4 | # 5 | # If any permission is set, any permission not included in the list is 6 | # implicitly set to "none". 7 | # 8 | # see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions 9 | permissions: 10 | contents: read 11 | 12 | on: 13 | push: 14 | branches: 15 | - 'master' 16 | tags: 17 | - 'v*' 18 | pull_request: 19 | # The branches below must be a subset of the branches above 20 | branches: ["master"] 21 | schedule: 22 | # ┌───────────── minute (0 - 59) 23 | # │ ┌───────────── hour (0 - 23) 24 | # │ │ ┌───────────── day of the month (1 - 31) 25 | # │ │ │ ┌───────────── month (1 - 12) 26 | # │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday) 27 | # │ │ │ │ │ 28 | # │ │ │ │ │ 29 | # │ │ │ │ │ 30 | # * * * * * 31 | - cron: '0 9 * * 4' 32 | 33 | jobs: 34 | codeql: 35 | runs-on: ubuntu-24.04 36 | timeout-minutes: 10 37 | permissions: 38 | actions: read 39 | contents: read 40 | security-events: write 41 | 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: actions/setup-go@v5 45 | with: 46 | go-version: "stable" 47 | - name: Create go.mod 48 | run: | 49 | ln -s vendor.mod go.mod 50 | ln -s vendor.sum go.sum 51 | - name: Initialize CodeQL 52 | uses: github/codeql-action/init@v3 53 | with: 54 | languages: go 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v3 57 | - name: Perform CodeQL Analysis 58 | uses: github/codeql-action/analyze@v3 59 | with: 60 | category: "/language:go" 61 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | # Default to 'contents: read', which grants actions to read commits. 4 | # 5 | # If any permission is set, any permission not included in the list is 6 | # implicitly set to "none". 7 | # 8 | # see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions 9 | permissions: 10 | contents: read 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | on: 17 | workflow_dispatch: 18 | push: 19 | branches: 20 | - 'main' 21 | pull_request: 22 | 23 | env: 24 | DESTDIR: ./build 25 | 26 | jobs: 27 | lint: 28 | runs-on: ubuntu-24.04 29 | timeout-minutes: 5 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: actions/setup-go@v5 34 | with: 35 | go-version: "oldstable" 36 | cache: false 37 | - name: Create go.mod 38 | run: | 39 | ln -s vendor.mod go.mod 40 | ln -s vendor.sum go.sum 41 | - uses: golangci/golangci-lint-action@v6 42 | with: 43 | version: v1.60.1 44 | skip-cache: true 45 | 46 | test: 47 | runs-on: ubuntu-24.04 48 | timeout-minutes: 5 49 | 50 | strategy: 51 | matrix: 52 | go: ["1.13", "oldstable", "stable"] 53 | 54 | steps: 55 | - uses: actions/checkout@v4 56 | - uses: actions/setup-go@v5 57 | with: 58 | go-version: ${{ matrix.go }} 59 | cache: false 60 | - name: Create go.mod 61 | run: | 62 | ln -s vendor.mod go.mod 63 | ln -s vendor.sum go.sum 64 | - name: Run tests 65 | run: go test -race 66 | 67 | govulncheck: 68 | runs-on: ubuntu-24.04 69 | timeout-minutes: 5 70 | 71 | permissions: 72 | # required to write sarif report 73 | security-events: write 74 | 75 | steps: 76 | - uses: actions/checkout@v4 77 | - name: Create go.mod 78 | run: | 79 | ln -s vendor.mod go.mod 80 | ln -s vendor.sum go.sum 81 | - name: Create artifact directory 82 | run: mkdir -p ${{ env.DESTDIR }} 83 | - name: Run govulncheck 84 | uses: golang/govulncheck-action@v1 85 | with: 86 | go-package: ./... 87 | check-latest: true 88 | repo-checkout: false 89 | output-format: 'sarif' 90 | output-file: ${{ env.DESTDIR }}/govulncheck.out 91 | - name: Upload SARIF report 92 | if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/go-events' }} 93 | uses: github/codeql-action/upload-sarif@v3 94 | with: 95 | sarif_file: ${{ env.DESTDIR }}/govulncheck.out 96 | -------------------------------------------------------------------------------- /common_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "sync" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | type tOrB interface { 12 | Fatalf(format string, args ...interface{}) 13 | Logf(format string, args ...interface{}) 14 | } 15 | 16 | type testSink struct { 17 | t tOrB 18 | 19 | events []Event 20 | expected int 21 | mu sync.Mutex 22 | closed bool 23 | } 24 | 25 | func newTestSink(t tOrB, expected int) *testSink { 26 | return &testSink{ 27 | t: t, 28 | events: make([]Event, 0, expected), // pre-allocate so we aren't benching alloc 29 | expected: expected, 30 | } 31 | } 32 | 33 | func (ts *testSink) Write(event Event) error { 34 | ts.mu.Lock() 35 | defer ts.mu.Unlock() 36 | 37 | if ts.closed { 38 | return ErrSinkClosed 39 | } 40 | 41 | ts.events = append(ts.events, event) 42 | 43 | if len(ts.events) > ts.expected { 44 | ts.t.Fatalf("len(ts.events) == %v, expected %v", len(ts.events), ts.expected) 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func (ts *testSink) Close() error { 51 | ts.mu.Lock() 52 | defer ts.mu.Unlock() 53 | if ts.closed { 54 | return ErrSinkClosed 55 | } 56 | 57 | ts.closed = true 58 | 59 | if len(ts.events) != ts.expected { 60 | ts.t.Fatalf("len(ts.events) == %v, expected %v", len(ts.events), ts.expected) 61 | } 62 | 63 | return nil 64 | } 65 | 66 | type delayedSink struct { 67 | Sink 68 | delay time.Duration 69 | } 70 | 71 | func (ds *delayedSink) Write(event Event) error { 72 | time.Sleep(ds.delay) 73 | return ds.Sink.Write(event) 74 | } 75 | 76 | type flakySink struct { 77 | Sink 78 | rate float64 79 | mu sync.Mutex 80 | } 81 | 82 | func (fs *flakySink) Write(event Event) error { 83 | fs.mu.Lock() 84 | defer fs.mu.Unlock() 85 | 86 | if rand.Float64() < fs.rate { 87 | return fmt.Errorf("error writing event: %v", event) 88 | } 89 | 90 | return fs.Sink.Write(event) 91 | } 92 | 93 | func checkClose(t *testing.T, sink Sink) { 94 | if err := sink.Close(); err != nil { 95 | t.Fatalf("unexpected error closing: %v", err) 96 | } 97 | 98 | // second close should not crash but should return an error. 99 | if err := sink.Close(); err != nil { 100 | t.Fatalf("unexpected error on double close: %v", err) 101 | } 102 | 103 | // Write after closed should be an error 104 | if err := sink.Write("fail"); err == nil { 105 | t.Fatalf("write after closed did not have an error") 106 | } else if err != ErrSinkClosed { 107 | t.Fatalf("error should be ErrSinkClosed") 108 | } 109 | } 110 | 111 | func benchmarkSink(b *testing.B, sink Sink) { 112 | defer sink.Close() 113 | var event = "myevent" 114 | for i := 0; i < b.N; i++ { 115 | _ = sink.Write(event) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /retry_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestRetryingSinkBreaker(t *testing.T) { 11 | testRetryingSink(t, NewBreaker(3, 10*time.Millisecond)) 12 | } 13 | 14 | func TestRetryingSinkExponentialBackoff(t *testing.T) { 15 | testRetryingSink(t, NewExponentialBackoff(ExponentialBackoffConfig{ 16 | Base: time.Millisecond, 17 | Factor: time.Millisecond, 18 | Max: time.Millisecond * 5, 19 | })) 20 | } 21 | 22 | func testRetryingSink(t *testing.T, strategy RetryStrategy) { 23 | const nevents = 100 24 | ts := newTestSink(t, nevents) 25 | 26 | // Make a sync that fails most of the time, ensuring that all the events 27 | // make it through. 28 | flaky := &flakySink{ 29 | rate: 1.0, // start out always failing. 30 | Sink: ts, 31 | } 32 | 33 | s := NewRetryingSink(flaky, strategy) 34 | 35 | var ( 36 | wg sync.WaitGroup 37 | asyncErr error 38 | once sync.Once 39 | ) 40 | for i := 1; i <= nevents; i++ { 41 | wg.Add(1) 42 | 43 | // Above 50, set the failure rate lower 44 | if i > 50 { 45 | flaky.mu.Lock() 46 | flaky.rate = 0.9 47 | flaky.mu.Unlock() 48 | } 49 | 50 | go func(event Event) { 51 | defer wg.Done() 52 | 53 | if err := s.Write(event); err != nil { 54 | once.Do(func() { 55 | asyncErr = fmt.Errorf("error writing event(%v): %v", event, err) 56 | }) 57 | } 58 | }(fmt.Sprintf("event-%d", i)) 59 | } 60 | 61 | wg.Wait() 62 | 63 | if asyncErr != nil { 64 | t.Fatalf("expected nil error, got %v", asyncErr) 65 | } 66 | 67 | checkClose(t, s) 68 | 69 | ts.mu.Lock() 70 | defer ts.mu.Unlock() 71 | } 72 | 73 | func TestExponentialBackoff(t *testing.T) { 74 | strategy := NewExponentialBackoff(DefaultExponentialBackoffConfig) 75 | backoff := strategy.Proceed(nil) 76 | 77 | if backoff != 0 { 78 | t.Errorf("untouched backoff should be zero-wait: %v != 0", backoff) 79 | } 80 | 81 | expected := strategy.config.Base + strategy.config.Factor 82 | for i := 1; i <= 10; i++ { 83 | if strategy.Failure(nil, nil) { 84 | t.Errorf("no facilities for dropping events in ExponentialBackoff") 85 | } 86 | 87 | for j := 0; j < 1000; j++ { 88 | // sample this several thousand times. 89 | backoff := strategy.Proceed(nil) 90 | if backoff > expected { 91 | t.Fatalf("expected must be bounded by %v after %v failures: %v", expected, i, backoff) 92 | } 93 | } 94 | 95 | expected = strategy.config.Base + strategy.config.Factor*time.Duration(1< strategy.config.Max { 97 | expected = strategy.config.Max 98 | } 99 | } 100 | 101 | strategy.Success(nil) // recovery! 102 | 103 | backoff = strategy.Proceed(nil) 104 | if backoff != 0 { 105 | t.Errorf("should have recovered: %v != 0", backoff) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /queue.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "container/list" 5 | "sync" 6 | 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // Queue accepts all messages into a queue for asynchronous consumption 11 | // by a sink. It is unbounded and thread safe but the sink must be reliable or 12 | // events will be dropped. 13 | type Queue struct { 14 | dst Sink 15 | events *list.List 16 | cond *sync.Cond 17 | mu sync.Mutex 18 | closed bool 19 | } 20 | 21 | // NewQueue returns a queue to the provided Sink dst. 22 | func NewQueue(dst Sink) *Queue { 23 | eq := Queue{ 24 | dst: dst, 25 | events: list.New(), 26 | } 27 | 28 | eq.cond = sync.NewCond(&eq.mu) 29 | go eq.run() 30 | return &eq 31 | } 32 | 33 | // Write accepts the events into the queue, only failing if the queue has 34 | // been closed. 35 | func (eq *Queue) Write(event Event) error { 36 | eq.mu.Lock() 37 | defer eq.mu.Unlock() 38 | 39 | if eq.closed { 40 | return ErrSinkClosed 41 | } 42 | 43 | eq.events.PushBack(event) 44 | eq.cond.Signal() // signal waiters 45 | 46 | return nil 47 | } 48 | 49 | // Close shutsdown the event queue, flushing 50 | func (eq *Queue) Close() error { 51 | eq.mu.Lock() 52 | defer eq.mu.Unlock() 53 | 54 | if eq.closed { 55 | return nil 56 | } 57 | 58 | // set closed flag 59 | eq.closed = true 60 | eq.cond.Signal() // signal flushes queue 61 | eq.cond.Wait() // wait for signal from last flush 62 | return eq.dst.Close() 63 | } 64 | 65 | // run is the main goroutine to flush events to the target sink. 66 | func (eq *Queue) run() { 67 | for { 68 | event := eq.next() 69 | 70 | if event == nil { 71 | return // nil block means event queue is closed. 72 | } 73 | 74 | if err := eq.dst.Write(event); err != nil { 75 | // TODO(aaronl): Dropping events could be bad depending 76 | // on the application. We should have a way of 77 | // communicating this condition. However, logging 78 | // at a log level above debug may not be appropriate. 79 | // Eventually, go-events should not use logrus at all, 80 | // and should bubble up conditions like this through 81 | // error values. 82 | logrus.WithFields(logrus.Fields{ 83 | "event": event, 84 | "sink": eq.dst, 85 | }).WithError(err).Debug("eventqueue: dropped event") 86 | } 87 | } 88 | } 89 | 90 | // next encompasses the critical section of the run loop. When the queue is 91 | // empty, it will block on the condition. If new data arrives, it will wake 92 | // and return a block. When closed, a nil slice will be returned. 93 | func (eq *Queue) next() Event { 94 | eq.mu.Lock() 95 | defer eq.mu.Unlock() 96 | 97 | for eq.events.Len() < 1 { 98 | if eq.closed { 99 | eq.cond.Broadcast() 100 | return nil 101 | } 102 | 103 | eq.cond.Wait() 104 | } 105 | 106 | front := eq.events.Front() 107 | block := front.Value.(Event) 108 | eq.events.Remove(front) 109 | 110 | return block 111 | } 112 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Docker open source projects 2 | 3 | Want to hack on go-events? Awesome! Here are instructions to get you started. 4 | 5 | go-events is part of the [Docker](https://www.docker.com) project, and 6 | follows the same rules and principles. If you're already familiar with the way 7 | Docker does things, you'll feel right at home. 8 | 9 | Otherwise, go read Docker's 10 | [contributions guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md), 11 | [issue triaging](https://github.com/docker/docker/blob/master/project/ISSUE-TRIAGE.md), 12 | [review process](https://github.com/docker/docker/blob/master/project/REVIEWING.md) and 13 | [branches and tags](https://github.com/docker/docker/blob/master/project/BRANCHES-AND-TAGS.md). 14 | 15 | For an in-depth description of our contribution process, visit the 16 | contributors guide: [Understand how to contribute](https://docs.docker.com/opensource/workflow/make-a-contribution/) 17 | 18 | ### Sign your work 19 | 20 | The sign-off is a simple line at the end of the explanation for the patch. Your 21 | signature certifies that you wrote the patch or otherwise have the right to pass 22 | it on as an open-source patch. The rules are pretty simple: if you can certify 23 | the below (from [developercertificate.org](http://developercertificate.org/)): 24 | 25 | ``` 26 | Developer Certificate of Origin 27 | Version 1.1 28 | 29 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 30 | 660 York Street, Suite 102, 31 | San Francisco, CA 94110 USA 32 | 33 | Everyone is permitted to copy and distribute verbatim copies of this 34 | license document, but changing it is not allowed. 35 | 36 | Developer's Certificate of Origin 1.1 37 | 38 | By making a contribution to this project, I certify that: 39 | 40 | (a) The contribution was created in whole or in part by me and I 41 | have the right to submit it under the open source license 42 | indicated in the file; or 43 | 44 | (b) The contribution is based upon previous work that, to the best 45 | of my knowledge, is covered under an appropriate open source 46 | license and I have the right under that license to submit that 47 | work with modifications, whether created in whole or in part 48 | by me, under the same open source license (unless I am 49 | permitted to submit under a different license), as indicated 50 | in the file; or 51 | 52 | (c) The contribution was provided directly to me by some other 53 | person who certified (a), (b) or (c) and I have not modified 54 | it. 55 | 56 | (d) I understand and agree that this project and the contribution 57 | are public and that a record of the contribution (including all 58 | personal information I submit with it, including my sign-off) is 59 | maintained indefinitely and may be redistributed consistent with 60 | this project or the open source license(s) involved. 61 | ``` 62 | 63 | Then you just add a line to every git commit message: 64 | 65 | Signed-off-by: Joe Smith 66 | 67 | Use your real name (sorry, no pseudonyms or anonymous contributions.) 68 | 69 | If you set your `user.name` and `user.email` git configs, you can sign your 70 | commit automatically with `git commit -s`. 71 | -------------------------------------------------------------------------------- /broadcast_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "testing" 7 | ) 8 | 9 | func TestBroadcaster(t *testing.T) { 10 | const nEvents = 1000 11 | var sinks []Sink 12 | b := NewBroadcaster() 13 | for i := 0; i < 10; i++ { 14 | sinks = append(sinks, newTestSink(t, nEvents)) 15 | err := b.Add(sinks[i]) 16 | if err != nil { 17 | t.Errorf("expected nil error, got %v", err) 18 | } 19 | 20 | err = b.Add(sinks[i]) // noop 21 | if err != nil { 22 | t.Errorf("expected nil error, got %v", err) 23 | } 24 | } 25 | 26 | var ( 27 | wg sync.WaitGroup 28 | asyncErr error 29 | once sync.Once 30 | ) 31 | for i := 1; i <= nEvents; i++ { 32 | wg.Add(1) 33 | go func(event Event) { 34 | defer wg.Done() 35 | 36 | if err := b.Write(event); err != nil { 37 | once.Do(func() { 38 | asyncErr = fmt.Errorf("error writing event(%v): %v", event, err) 39 | }) 40 | } 41 | }(fmt.Sprintf("event-%d", i)) 42 | } 43 | 44 | wg.Wait() // Wait until writes complete 45 | 46 | if asyncErr != nil { 47 | t.Fatalf("expected nil error, got %v", asyncErr) 48 | } 49 | 50 | for i := range sinks { 51 | err := b.Remove(sinks[i]) 52 | if err != nil { 53 | t.Errorf("expected nil error, got %v", err) 54 | } 55 | } 56 | 57 | // sending one more should trigger test failure if they weren't removed. 58 | if err := b.Write("onemore"); err != nil { 59 | t.Fatalf("unexpected error sending one more: %v", err) 60 | } 61 | 62 | // add them back to test closing. 63 | for i := range sinks { 64 | err := b.Add(sinks[i]) 65 | if err != nil { 66 | t.Errorf("expected nil error, got %v", err) 67 | } 68 | } 69 | 70 | checkClose(t, b) 71 | 72 | // Iterate through the sinks and check that they all have the expected length. 73 | for _, sink := range sinks { 74 | ts := sink.(*testSink) 75 | ts.mu.Lock() 76 | defer ts.mu.Unlock() 77 | 78 | if len(ts.events) != nEvents { 79 | t.Fatalf("not all events ended up in testsink: len(testSink) == %d, not %d", len(ts.events), nEvents) 80 | } 81 | 82 | if !ts.closed { 83 | t.Fatalf("sink should have been closed") 84 | } 85 | } 86 | } 87 | 88 | func BenchmarkBroadcast10(b *testing.B) { 89 | benchmarkBroadcast(b, 10) 90 | } 91 | 92 | func BenchmarkBroadcast100(b *testing.B) { 93 | benchmarkBroadcast(b, 100) 94 | } 95 | 96 | func BenchmarkBroadcast1000(b *testing.B) { 97 | benchmarkBroadcast(b, 1000) 98 | } 99 | 100 | func BenchmarkBroadcast10000(b *testing.B) { 101 | benchmarkBroadcast(b, 10000) 102 | } 103 | 104 | func benchmarkBroadcast(b *testing.B, nsinks int) { 105 | // counter := metrics.NewCounter() 106 | // metrics.DefaultRegistry.Register(fmt.Sprintf("nsinks: %v", nsinks), counter) 107 | // go metrics.Log(metrics.DefaultRegistry, 500*time.Millisecond, log.New(os.Stderr, "metrics: ", log.LstdFlags)) 108 | 109 | b.StopTimer() 110 | var sinks []Sink 111 | for i := 0; i < nsinks; i++ { 112 | // counter.Inc(1) 113 | sinks = append(sinks, newTestSink(b, b.N)) 114 | // sinks = append(sinks, NewQueue(&testSink{t: b, expected: b.N})) 115 | } 116 | b.StartTimer() 117 | 118 | // meter := metered{} 119 | // NewQueue(meter.Egress(dst)) 120 | 121 | benchmarkSink(b, NewBroadcaster(sinks...)) 122 | } 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Events Package 2 | 3 | [![GoDoc](https://godoc.org/github.com/docker/go-events?status.svg)](https://godoc.org/github.com/docker/go-events) 4 | [![ci](https://github.com/docker/go-events/actions/workflows/ci.yml/badge.svg)](https://github.com/docker/go-events/actions/workflows/ci.yml) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/docker/go-events)](https://goreportcard.com/report/github.com/docker/go-events) 6 | 7 | The Docker `events` package implements a composable event distribution package 8 | for Go. 9 | 10 | Originally created to implement the [notifications in Docker Registry 11 | 2](https://github.com/docker/distribution/blob/master/docs/notifications.md), 12 | we've found the pattern to be useful in other applications. This package is 13 | most of the same code with slightly updated interfaces. Much of the internals 14 | have been made available. 15 | 16 | ## Usage 17 | 18 | The `events` package centers around a `Sink` type. Events are written with 19 | calls to `Sink.Write(event Event)`. Sinks can be wired up in various 20 | configurations to achieve interesting behavior. 21 | 22 | The canonical example is that employed by the 23 | [docker/distribution/notifications](https://godoc.org/github.com/docker/distribution/notifications) 24 | package. Let's say we have a type `httpSink` where we'd like to queue 25 | notifications. As a rule, it should send a single http request and return an 26 | error if it fails: 27 | 28 | ```go 29 | func (h *httpSink) Write(event Event) error { 30 | p, err := json.Marshal(event) 31 | if err != nil { 32 | return err 33 | } 34 | body := bytes.NewReader(p) 35 | resp, err := h.client.Post(h.url, "application/json", body) 36 | if err != nil { 37 | return err 38 | } 39 | defer resp.Body.Close() 40 | 41 | if resp.Status != 200 { 42 | return errors.New("unexpected status") 43 | } 44 | 45 | return nil 46 | } 47 | 48 | // implement (*httpSink).Close() 49 | ``` 50 | 51 | With just that, we can start using components from this package. One can call 52 | `(*httpSink).Write` to send events as the body of a post request to a 53 | configured URL. 54 | 55 | ### Retries 56 | 57 | HTTP can be unreliable. The first feature we'd like is to have some retry: 58 | 59 | ```go 60 | hs := newHTTPSink(/*...*/) 61 | retry := NewRetryingSink(hs, NewBreaker(5, time.Second)) 62 | ``` 63 | 64 | We now have a sink that will retry events against the `httpSink` until they 65 | succeed. The retry will backoff for one second after 5 consecutive failures 66 | using the breaker strategy. 67 | 68 | ### Queues 69 | 70 | This isn't quite enough. We we want a sink that doesn't block while we are 71 | waiting for events to be sent. Let's add a `Queue`: 72 | 73 | ```go 74 | queue := NewQueue(retry) 75 | ``` 76 | 77 | Now, we have an unbounded queue that will work through all events sent with 78 | `(*Queue).Write`. Events can be added asynchronously to the queue without 79 | blocking the current execution path. This is ideal for use in an http request. 80 | 81 | ### Broadcast 82 | 83 | It usually turns out that you want to send to more than one listener. We can 84 | use `Broadcaster` to support this: 85 | 86 | ```go 87 | var broadcast = NewBroadcaster() // make it available somewhere in your application. 88 | broadcast.Add(queue) // add your queue! 89 | broadcast.Add(queue2) // and another! 90 | ``` 91 | 92 | With the above, we can now call `broadcast.Write` in our http handlers and have 93 | all the events distributed to each queue. Because the events are queued, not 94 | listener blocks another. 95 | 96 | ### Extending 97 | 98 | For the most part, the above is sufficient for a lot of applications. However, 99 | extending the above functionality can be done implementing your own `Sink`. The 100 | behavior and semantics of the sink can be completely dependent on the 101 | application requirements. The interface is provided below for reference: 102 | 103 | ```go 104 | type Sink { 105 | Write(Event) error 106 | Close() error 107 | } 108 | ``` 109 | 110 | Application behavior can be controlled by how `Write` behaves. The examples 111 | above are designed to queue the message and return as quickly as possible. 112 | Other implementations may block until the event is committed to durable 113 | storage. 114 | 115 | ## Copyright and license 116 | 117 | Copyright © 2016 Docker, Inc. go-events is licensed under the Apache License, 118 | Version 2.0. See [LICENSE](LICENSE) for the full license text. 119 | -------------------------------------------------------------------------------- /broadcast.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // Broadcaster sends events to multiple, reliable Sinks. The goal of this 11 | // component is to dispatch events to configured endpoints. Reliability can be 12 | // provided by wrapping incoming sinks. 13 | type Broadcaster struct { 14 | sinks []Sink 15 | events chan Event 16 | adds chan configureRequest 17 | removes chan configureRequest 18 | 19 | shutdown chan struct{} 20 | closed chan struct{} 21 | once sync.Once 22 | } 23 | 24 | // NewBroadcaster appends one or more sinks to the list of sinks. The 25 | // broadcaster behavior will be affected by the properties of the sink. 26 | // Generally, the sink should accept all messages and deal with reliability on 27 | // its own. Use of EventQueue and RetryingSink should be used here. 28 | func NewBroadcaster(sinks ...Sink) *Broadcaster { 29 | b := Broadcaster{ 30 | sinks: sinks, 31 | events: make(chan Event), 32 | adds: make(chan configureRequest), 33 | removes: make(chan configureRequest), 34 | shutdown: make(chan struct{}), 35 | closed: make(chan struct{}), 36 | } 37 | 38 | // Start the broadcaster 39 | go b.run() 40 | 41 | return &b 42 | } 43 | 44 | // Write accepts an event to be dispatched to all sinks. This method will never 45 | // fail and should never block (hopefully!). The caller cedes the memory to the 46 | // broadcaster and should not modify it after calling write. 47 | func (b *Broadcaster) Write(event Event) error { 48 | select { 49 | case b.events <- event: 50 | case <-b.closed: 51 | return ErrSinkClosed 52 | } 53 | return nil 54 | } 55 | 56 | // Add the sink to the broadcaster. 57 | // 58 | // The provided sink must be comparable with equality. Typically, this just 59 | // works with a regular pointer type. 60 | func (b *Broadcaster) Add(sink Sink) error { 61 | return b.configure(b.adds, sink) 62 | } 63 | 64 | // Remove the provided sink. 65 | func (b *Broadcaster) Remove(sink Sink) error { 66 | return b.configure(b.removes, sink) 67 | } 68 | 69 | type configureRequest struct { 70 | sink Sink 71 | response chan error 72 | } 73 | 74 | func (b *Broadcaster) configure(ch chan configureRequest, sink Sink) error { 75 | response := make(chan error, 1) 76 | 77 | for { 78 | select { 79 | case ch <- configureRequest{ 80 | sink: sink, 81 | response: response}: 82 | ch = nil 83 | case err := <-response: 84 | return err 85 | case <-b.closed: 86 | return ErrSinkClosed 87 | } 88 | } 89 | } 90 | 91 | // Close the broadcaster, ensuring that all messages are flushed to the 92 | // underlying sink before returning. 93 | func (b *Broadcaster) Close() error { 94 | b.once.Do(func() { 95 | close(b.shutdown) 96 | }) 97 | 98 | <-b.closed 99 | return nil 100 | } 101 | 102 | // run is the main broadcast loop, started when the broadcaster is created. 103 | // Under normal conditions, it waits for events on the event channel. After 104 | // Close is called, this goroutine will exit. 105 | func (b *Broadcaster) run() { 106 | defer close(b.closed) 107 | remove := func(target Sink) { 108 | for i, sink := range b.sinks { 109 | if sink == target { 110 | b.sinks = append(b.sinks[:i], b.sinks[i+1:]...) 111 | break 112 | } 113 | } 114 | } 115 | 116 | for { 117 | select { 118 | case event := <-b.events: 119 | for _, sink := range b.sinks { 120 | if err := sink.Write(event); err != nil { 121 | if err == ErrSinkClosed { 122 | // remove closed sinks 123 | remove(sink) 124 | continue 125 | } 126 | logrus.WithField("event", event).WithField("events.sink", sink).WithError(err). 127 | Errorf("broadcaster: dropping event") 128 | } 129 | } 130 | case request := <-b.adds: 131 | // while we have to iterate for add/remove, common iteration for 132 | // send is faster against slice. 133 | 134 | var found bool 135 | for _, sink := range b.sinks { 136 | if request.sink == sink { 137 | found = true 138 | break 139 | } 140 | } 141 | 142 | if !found { 143 | b.sinks = append(b.sinks, request.sink) 144 | } 145 | // b.sinks[request.sink] = struct{}{} 146 | request.response <- nil 147 | case request := <-b.removes: 148 | remove(request.sink) 149 | request.response <- nil 150 | case <-b.shutdown: 151 | // close all the underlying sinks 152 | for _, sink := range b.sinks { 153 | if err := sink.Close(); err != nil && err != ErrSinkClosed { 154 | logrus.WithField("events.sink", sink).WithError(err). 155 | Errorf("broadcaster: closing sink failed") 156 | } 157 | } 158 | return 159 | } 160 | } 161 | } 162 | 163 | func (b *Broadcaster) String() string { 164 | // Serialize copy of this broadcaster without the sync.Once, to avoid 165 | // a data race. 166 | 167 | b2 := map[string]interface{}{ 168 | "sinks": b.sinks, 169 | "events": b.events, 170 | "adds": b.adds, 171 | "removes": b.removes, 172 | 173 | "shutdown": b.shutdown, 174 | "closed": b.closed, 175 | } 176 | 177 | return fmt.Sprint(b2) 178 | } 179 | -------------------------------------------------------------------------------- /retry.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // RetryingSink retries the write until success or an ErrSinkClosed is 14 | // returned. Underlying sink must have p > 0 of succeeding or the sink will 15 | // block. Retry is configured with a RetryStrategy. Concurrent calls to a 16 | // retrying sink are serialized through the sink, meaning that if one is 17 | // in-flight, another will not proceed. 18 | type RetryingSink struct { 19 | sink Sink 20 | strategy RetryStrategy 21 | closed chan struct{} 22 | once sync.Once 23 | } 24 | 25 | // NewRetryingSink returns a sink that will retry writes to a sink, backing 26 | // off on failure. Parameters threshold and backoff adjust the behavior of the 27 | // circuit breaker. 28 | func NewRetryingSink(sink Sink, strategy RetryStrategy) *RetryingSink { 29 | rs := &RetryingSink{ 30 | sink: sink, 31 | strategy: strategy, 32 | closed: make(chan struct{}), 33 | } 34 | 35 | return rs 36 | } 37 | 38 | // Write attempts to flush the events to the downstream sink until it succeeds 39 | // or the sink is closed. 40 | func (rs *RetryingSink) Write(event Event) error { 41 | logger := logrus.WithField("event", event) 42 | 43 | retry: 44 | select { 45 | case <-rs.closed: 46 | return ErrSinkClosed 47 | default: 48 | } 49 | 50 | if backoff := rs.strategy.Proceed(event); backoff > 0 { 51 | select { 52 | case <-time.After(backoff): 53 | // TODO(stevvooe): This branch holds up the next try. Before, we 54 | // would simply break to the "retry" label and then possibly wait 55 | // again. However, this requires all retry strategies to have a 56 | // large probability of probing the sync for success, rather than 57 | // just backing off and sending the request. 58 | case <-rs.closed: 59 | return ErrSinkClosed 60 | } 61 | } 62 | 63 | if err := rs.sink.Write(event); err != nil { 64 | if err == ErrSinkClosed { 65 | // terminal! 66 | return err 67 | } 68 | 69 | logger := logger.WithError(err) // shadow!! 70 | 71 | if rs.strategy.Failure(event, err) { 72 | logger.Errorf("retryingsink: dropped event") 73 | return nil 74 | } 75 | 76 | logger.Errorf("retryingsink: error writing event, retrying") 77 | goto retry 78 | } 79 | 80 | rs.strategy.Success(event) 81 | return nil 82 | } 83 | 84 | // Close closes the sink and the underlying sink. 85 | func (rs *RetryingSink) Close() error { 86 | rs.once.Do(func() { 87 | close(rs.closed) 88 | }) 89 | 90 | return nil 91 | } 92 | 93 | func (rs *RetryingSink) String() string { 94 | // Serialize a copy of the RetryingSink without the sync.Once, to avoid 95 | // a data race. 96 | rs2 := map[string]interface{}{ 97 | "sink": rs.sink, 98 | "strategy": rs.strategy, 99 | "closed": rs.closed, 100 | } 101 | return fmt.Sprint(rs2) 102 | } 103 | 104 | // RetryStrategy defines a strategy for retrying event sink writes. 105 | // 106 | // All methods should be goroutine safe. 107 | type RetryStrategy interface { 108 | // Proceed is called before every event send. If proceed returns a 109 | // positive, non-zero integer, the retryer will back off by the provided 110 | // duration. 111 | // 112 | // An event is provided, by may be ignored. 113 | Proceed(event Event) time.Duration 114 | 115 | // Failure reports a failure to the strategy. If this method returns true, 116 | // the event should be dropped. 117 | Failure(event Event, err error) bool 118 | 119 | // Success should be called when an event is sent successfully. 120 | Success(event Event) 121 | } 122 | 123 | // Breaker implements a circuit breaker retry strategy. 124 | // 125 | // The current implementation never drops events. 126 | type Breaker struct { 127 | threshold int 128 | recent int 129 | last time.Time 130 | backoff time.Duration // time after which we retry after failure. 131 | mu sync.Mutex 132 | } 133 | 134 | var _ RetryStrategy = &Breaker{} 135 | 136 | // NewBreaker returns a breaker that will backoff after the threshold has been 137 | // tripped. A Breaker is thread safe and may be shared by many goroutines. 138 | func NewBreaker(threshold int, backoff time.Duration) *Breaker { 139 | return &Breaker{ 140 | threshold: threshold, 141 | backoff: backoff, 142 | } 143 | } 144 | 145 | // Proceed checks the failures against the threshold. 146 | func (b *Breaker) Proceed(event Event) time.Duration { 147 | b.mu.Lock() 148 | defer b.mu.Unlock() 149 | 150 | if b.recent < b.threshold { 151 | return 0 152 | } 153 | 154 | return time.Until(b.last.Add(b.backoff)) 155 | } 156 | 157 | // Success resets the breaker. 158 | func (b *Breaker) Success(event Event) { 159 | b.mu.Lock() 160 | defer b.mu.Unlock() 161 | 162 | b.recent = 0 163 | b.last = time.Time{} 164 | } 165 | 166 | // Failure records the failure and latest failure time. 167 | func (b *Breaker) Failure(event Event, err error) bool { 168 | b.mu.Lock() 169 | defer b.mu.Unlock() 170 | 171 | b.recent++ 172 | b.last = time.Now().UTC() 173 | return false // never drop events. 174 | } 175 | 176 | var ( 177 | // DefaultExponentialBackoffConfig provides a default configuration for 178 | // exponential backoff. 179 | DefaultExponentialBackoffConfig = ExponentialBackoffConfig{ 180 | Base: time.Second, 181 | Factor: time.Second, 182 | Max: 20 * time.Second, 183 | } 184 | ) 185 | 186 | // ExponentialBackoffConfig configures backoff parameters. 187 | // 188 | // Note that these parameters operate on the upper bound for choosing a random 189 | // value. For example, at Base=1s, a random value in [0,1s) will be chosen for 190 | // the backoff value. 191 | type ExponentialBackoffConfig struct { 192 | // Base is the minimum bound for backing off after failure. 193 | Base time.Duration 194 | 195 | // Factor sets the amount of time by which the backoff grows with each 196 | // failure. 197 | Factor time.Duration 198 | 199 | // Max is the absolute maxiumum bound for a single backoff. 200 | Max time.Duration 201 | } 202 | 203 | // ExponentialBackoff implements random backoff with exponentially increasing 204 | // bounds as the number consecutive failures increase. 205 | type ExponentialBackoff struct { 206 | failures uint64 // consecutive failure counter (needs to be 64-bit aligned) 207 | config ExponentialBackoffConfig 208 | } 209 | 210 | // NewExponentialBackoff returns an exponential backoff strategy with the 211 | // desired config. If config is nil, the default is returned. 212 | func NewExponentialBackoff(config ExponentialBackoffConfig) *ExponentialBackoff { 213 | return &ExponentialBackoff{ 214 | config: config, 215 | } 216 | } 217 | 218 | // Proceed returns the next randomly bound exponential backoff time. 219 | func (b *ExponentialBackoff) Proceed(event Event) time.Duration { 220 | return b.backoff(atomic.LoadUint64(&b.failures)) 221 | } 222 | 223 | // Success resets the failures counter. 224 | func (b *ExponentialBackoff) Success(event Event) { 225 | atomic.StoreUint64(&b.failures, 0) 226 | } 227 | 228 | // Failure increments the failure counter. 229 | func (b *ExponentialBackoff) Failure(event Event, err error) bool { 230 | atomic.AddUint64(&b.failures, 1) 231 | return false 232 | } 233 | 234 | // backoff calculates the amount of time to wait based on the number of 235 | // consecutive failures. 236 | func (b *ExponentialBackoff) backoff(failures uint64) time.Duration { 237 | if failures <= 0 { 238 | // proceed normally when there are no failures. 239 | return 0 240 | } 241 | 242 | factor := b.config.Factor 243 | if factor <= 0 { 244 | factor = DefaultExponentialBackoffConfig.Factor 245 | } 246 | 247 | backoff := b.config.Base + factor*time.Duration(1<<(failures-1)) 248 | 249 | max := b.config.Max 250 | if max <= 0 { 251 | max = DefaultExponentialBackoffConfig.Max 252 | } 253 | 254 | if backoff > max || backoff < 0 { 255 | backoff = max 256 | } 257 | 258 | // Choose a uniformly distributed value from [0, backoff). 259 | return time.Duration(rand.Int63n(int64(backoff))) 260 | } 261 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Docker, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------