pax_global_header00006660000000000000000000000064147221533150014515gustar00rootroot0000000000000052 comment=d9cb9c9d80b6dd75d216e142a6576040798edfc2 golang-gopkg-eapache-go-resiliency.v1-1.7.0/000077500000000000000000000000001472215331500205565ustar00rootroot00000000000000golang-gopkg-eapache-go-resiliency.v1-1.7.0/.github/000077500000000000000000000000001472215331500221165ustar00rootroot00000000000000golang-gopkg-eapache-go-resiliency.v1-1.7.0/.github/workflows/000077500000000000000000000000001472215331500241535ustar00rootroot00000000000000golang-gopkg-eapache-go-resiliency.v1-1.7.0/.github/workflows/golang-ci.yml000066400000000000000000000007201472215331500265350ustar00rootroot00000000000000name: Golang CI on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: go-version: - '1.13' - '1.18' - '1.22' steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Build run: go build -v ./... - name: Test run: go test -race -v ./... golang-gopkg-eapache-go-resiliency.v1-1.7.0/.gitignore000066400000000000000000000004121472215331500225430ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof golang-gopkg-eapache-go-resiliency.v1-1.7.0/CHANGELOG.md000066400000000000000000000044621472215331500223750ustar00rootroot00000000000000# Changelog *Note: I will occasionally bump the minimum required Golang version without bumping the major version of this package, which violates the official Golang packaging convention around breaking changes. Typically the versions being dropped are multiple years old and long unsupported.* #### Version 1.7.0 (2024-07-19) - Adds `Retrier.WithSurfaceWorkErrors()` to ask the Retrier to always return the work function's error even if a context deadline is hit (thanks to Elizabeth Cox). #### Version 1.6.0 (2024-02-19) - Adds `Breaker.GetState()` to check the breaker state directly (e.g. for monitoring metrics). - Fix a race condition in the Batcher that could have lead to a panic if multiple batches ended up executing at once (thanks to Tiago Peczenyj for the discovery and very clear bug report). - Fix `Batcher.Shutdown()` to behave correctly when multiple batches end up executing at once. - A variety of small refactors, simplifications, and test suite improvements. #### Version 1.5.0 (2023-12-14) - Adds `Retrier.WithInfiniteRetry()` and `Retrier.RunFn()` to handle more complex cases (thanks to Maxime Beckman). #### Version 1.4.0 (2023-08-14) - Adds `Batcher.Shutdown()` to flush any pending work without waiting for the timer, e.g. on application shutdown (thanks to Ivan Stankov). - Fix possible memory leaks of Timer objects in Deadline, Retrier, and Semaphore (thanks to Dmytro Nozdrin). #### Version 1.3.0 (2022-06-27) - Increased minimum Golang version to 1.13. - Fix a goroutine leak in `Deadline.Run()` on `ErrTimeOut`. - Add a `go.mod` file to conform to more recent Golang version standards. - Use `errors.Is` when classifying errors for the `Retrier` (thanks to Taufik Rama). - Add implementation of `LimitedExponentialBackoff` for the `Retrier` (thanks to tukeJonny). #### Version 1.2.0 (2019-06-14) - Increased minimum Golang version to 1.7. - Add `RunCtx` method on `Retrier` to support running with a context. - Ensure the `Retrier`'s use of random numbers is concurrency-safe. - Bump CI to ensure we support newer Golang versions. #### Version 1.1.0 (2018-03-26) - Improve documentation and fix some typos. - Bump CI to ensure we support newer Golang versions. - Add `IsEmpty()` method on `Semaphore`. #### Version 1.0.0 (2015-02-13) Initial release. golang-gopkg-eapache-go-resiliency.v1-1.7.0/LICENSE000066400000000000000000000020651472215331500215660ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 Evan Huus Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-gopkg-eapache-go-resiliency.v1-1.7.0/README.md000066400000000000000000000022021472215331500220310ustar00rootroot00000000000000go-resiliency ============= [![Golang CI](https://github.com/eapache/go-resiliency/actions/workflows/golang-ci.yml/badge.svg)](https://github.com/eapache/go-resiliency/actions/workflows/golang-ci.yml) [![GoDoc](https://godoc.org/github.com/eapache/go-resiliency?status.svg)](https://godoc.org/github.com/eapache/go-resiliency) [![Code of Conduct](https://img.shields.io/badge/code%20of%20conduct-active-blue.svg)](https://eapache.github.io/conduct.html) Resiliency patterns for golang. Based in part on [Hystrix](https://github.com/Netflix/Hystrix), [Semian](https://github.com/Shopify/semian), and others. Currently implemented patterns include: - circuit-breaker (in the `breaker` directory) - semaphore (in the `semaphore` directory) - deadline/timeout (in the `deadline` directory) - batching (in the `batcher` directory) - retriable (in the `retrier` directory) *Note: I will occasionally bump the minimum required Golang version without bumping the major version of this package, which violates the official Golang packaging convention around breaking changes. Typically the versions being dropped are multiple years old and long unsupported.* golang-gopkg-eapache-go-resiliency.v1-1.7.0/batcher/000077500000000000000000000000001472215331500221665ustar00rootroot00000000000000golang-gopkg-eapache-go-resiliency.v1-1.7.0/batcher/README.md000066400000000000000000000020471472215331500234500ustar00rootroot00000000000000batcher ======= [![Golang CI](https://github.com/eapache/go-resiliency/actions/workflows/golang-ci.yml/badge.svg)](https://github.com/eapache/go-resiliency/actions/workflows/golang-ci.yml) [![GoDoc](https://godoc.org/github.com/eapache/go-resiliency/batcher?status.svg)](https://godoc.org/github.com/eapache/go-resiliency/batcher) [![Code of Conduct](https://img.shields.io/badge/code%20of%20conduct-active-blue.svg)](https://eapache.github.io/conduct.html) The batching resiliency pattern for golang. Creating a batcher takes two parameters: - the timeout to wait while collecting a batch - the function to run once a batch has been collected You can also optionally set a prefilter to fail queries before they enter the batch. ```go b := batcher.New(10*time.Millisecond, func(params []interface{}) error { // do something with the batch of parameters return nil }) b.Prefilter(func(param interface{}) error { // do some sort of sanity check on the parameter, and return an error if it fails return nil }) for i := 0; i < 10; i++ { go b.Run(i) } ``` golang-gopkg-eapache-go-resiliency.v1-1.7.0/batcher/batcher.go000066400000000000000000000066401472215331500241330ustar00rootroot00000000000000// Package batcher implements the batching resiliency pattern for Go. package batcher import ( "sync" "time" ) type work struct { param interface{} future chan error } // Batcher implements the batching resiliency pattern type Batcher struct { timeout time.Duration prefilter func(interface{}) error lock sync.Mutex submit chan *work doWork func([]interface{}) error batchCounter sync.WaitGroup flushTimer *time.Timer } // New constructs a new batcher that will batch all calls to Run that occur within // `timeout` time before calling doWork just once for the entire batch. The doWork // function must be safe to run concurrently with itself as this may occur, especially // when the doWork function is slow, or the timeout is small. func New(timeout time.Duration, doWork func([]interface{}) error) *Batcher { return &Batcher{ timeout: timeout, doWork: doWork, } } // Run runs the work function with the given parameter, possibly // including it in a batch with other calls to Run that occur within the // specified timeout. It is safe to call Run concurrently on the same batcher. func (b *Batcher) Run(param interface{}) error { if b.prefilter != nil { if err := b.prefilter(param); err != nil { return err } } if b.timeout == 0 { return b.doWork([]interface{}{param}) } w := &work{ param: param, future: make(chan error, 1), } b.submitWork(w) return <-w.future } // Prefilter specifies an optional function that can be used to run initial checks on parameters // passed to Run before being added to the batch. If the prefilter returns a non-nil error, // that error is returned immediately from Run and the batcher is not invoked. A prefilter // cannot safely be specified for a batcher if Run has already been invoked. The filter function // specified must be concurrency-safe. func (b *Batcher) Prefilter(filter func(interface{}) error) { b.prefilter = filter } func (b *Batcher) submitWork(w *work) { b.lock.Lock() defer b.lock.Unlock() // kick off a new batch if needed if b.submit == nil { b.batchCounter.Add(1) b.submit = make(chan *work, 4) go b.batch(b.submit) b.flushTimer = time.AfterFunc(b.timeout, b.flushCurrentBatch) } // then add this work to the current batch b.submit <- w } func (b *Batcher) batch(input <-chan *work) { defer b.batchCounter.Done() var params []interface{} var futures []chan error for work := range input { params = append(params, work.param) futures = append(futures, work.future) } ret := b.doWork(params) for _, future := range futures { future <- ret close(future) } } // Shutdown flushes and executes any pending batches. If wait is true, it also waits for the pending batches // to finish executing before it returns. This can be used to avoid waiting for the timeout to expire when // gracefully shutting down your application. Calling Run at any point after calling Shutdown will lead to // undefined behaviour. func (b *Batcher) Shutdown(wait bool) { b.flushCurrentBatch() if wait { b.batchCounter.Wait() } } func (b *Batcher) flushCurrentBatch() { b.lock.Lock() defer b.lock.Unlock() if b.submit == nil { return } // stop the timer to avoid spurious flushes and trigger immediate cleanup in case this flush was // triggered manually by a call to Shutdown (it has to happen inside the lock, so it can't be done // in the Shutdown method directly) b.flushTimer.Stop() close(b.submit) b.submit = nil } golang-gopkg-eapache-go-resiliency.v1-1.7.0/batcher/batcher_test.go000066400000000000000000000061041472215331500251650ustar00rootroot00000000000000package batcher import ( "errors" "sync" "sync/atomic" "testing" "time" ) var errSomeError = errors.New("errSomeError") func returnsError(params []interface{}) error { return errSomeError } func returnsSuccess(params []interface{}) error { return nil } func TestBatcherSuccess(t *testing.T) { b := New(10*time.Millisecond, returnsSuccess) wg := &sync.WaitGroup{} for i := 0; i < 10; i++ { wg.Add(1) go func() { if err := b.Run(nil); err != nil { t.Error(err) } wg.Done() }() } wg.Wait() b = New(0, returnsSuccess) for i := 0; i < 10; i++ { if err := b.Run(nil); err != nil { t.Error(err) } } } func TestShutdownSuccess(t *testing.T) { sleepDuration := 5 * time.Millisecond durationLimit := 2 * sleepDuration timeout := 2 * durationLimit total := 0 doSum := func(params []interface{}) error { for _, param := range params { intValue, ok := param.(int) if !ok { t.Error("expected type int") } total += intValue } return nil } b := New(timeout, doSum) go func() { time.Sleep(sleepDuration) b.Shutdown(true) }() wg := &sync.WaitGroup{} expectedTotal := 0 start := time.Now() for i := 0; i < 10; i++ { expectedTotal += i wg.Add(1) go func(i int) { if err := b.Run(i); err != nil { t.Error(err) } wg.Done() }(i) } wg.Wait() duration := time.Since(start) if duration >= durationLimit { t.Errorf("expected duration[%v] < durationLimit[%v]", duration, durationLimit) } if total != expectedTotal { t.Errorf("expected processed count[%v] < actual[%v]", expectedTotal, total) } } func TestShutdownEmpty(t *testing.T) { b := New(10*time.Millisecond, returnsSuccess) b.Shutdown(true) } func TestBatcherError(t *testing.T) { b := New(10*time.Millisecond, returnsError) wg := &sync.WaitGroup{} for i := 0; i < 10; i++ { wg.Add(1) go func() { if err := b.Run(nil); err != errSomeError { t.Error(err) } wg.Done() }() } wg.Wait() } func TestBatcherPrefilter(t *testing.T) { b := New(1*time.Millisecond, returnsSuccess) b.Prefilter(func(param interface{}) error { if param == nil { return errSomeError } return nil }) if err := b.Run(nil); err != errSomeError { t.Error(err) } if err := b.Run(1); err != nil { t.Error(err) } } func TestBatcherMultipleBatches(t *testing.T) { var iters uint32 b := New(10*time.Millisecond, func(params []interface{}) error { atomic.AddUint32(&iters, 1) return nil }) wg := &sync.WaitGroup{} for group := 0; group < 5; group++ { for i := 0; i < 10; i++ { wg.Add(1) go func() { if err := b.Run(nil); err != nil { t.Error(err) } wg.Done() }() } time.Sleep(15 * time.Millisecond) } wg.Wait() if iters != 5 { t.Error("Wrong number of iters:", iters) } } func ExampleBatcher() { b := New(10*time.Millisecond, func(params []interface{}) error { // do something with the batch of parameters return nil }) b.Prefilter(func(param interface{}) error { // do some sort of sanity check on the parameter, and return an error if it fails return nil }) for i := 0; i < 10; i++ { go b.Run(i) } } golang-gopkg-eapache-go-resiliency.v1-1.7.0/breaker/000077500000000000000000000000001472215331500221715ustar00rootroot00000000000000golang-gopkg-eapache-go-resiliency.v1-1.7.0/breaker/README.md000066400000000000000000000020651472215331500234530ustar00rootroot00000000000000circuit-breaker =============== [![Golang CI](https://github.com/eapache/go-resiliency/actions/workflows/golang-ci.yml/badge.svg)](https://github.com/eapache/go-resiliency/actions/workflows/golang-ci.yml) [![GoDoc](https://godoc.org/github.com/eapache/go-resiliency/breaker?status.svg)](https://godoc.org/github.com/eapache/go-resiliency/breaker) [![Code of Conduct](https://img.shields.io/badge/code%20of%20conduct-active-blue.svg)](https://eapache.github.io/conduct.html) The circuit-breaker resiliency pattern for golang. Creating a breaker takes three parameters: - error threshold (for opening the breaker) - success threshold (for closing the breaker) - timeout (how long to keep the breaker open) ```go b := breaker.New(3, 1, 5*time.Second) for { result := b.Run(func() error { // communicate with some external service and // return an error if the communication failed return nil }) switch result { case nil: // success! case breaker.ErrBreakerOpen: // our function wasn't run because the breaker was open default: // some other error } } ``` golang-gopkg-eapache-go-resiliency.v1-1.7.0/breaker/breaker.go000066400000000000000000000101061472215331500241310ustar00rootroot00000000000000// Package breaker implements the circuit-breaker resiliency pattern for Go. package breaker import ( "errors" "sync" "sync/atomic" "time" ) // ErrBreakerOpen is the error returned from Run() when the function is not executed // because the breaker is currently open. var ErrBreakerOpen = errors.New("circuit breaker is open") // State is a type representing the possible states of a circuit breaker. type State uint32 const ( Closed State = iota Open HalfOpen ) // Breaker implements the circuit-breaker resiliency pattern type Breaker struct { errorThreshold, successThreshold int timeout time.Duration lock sync.Mutex state State errors, successes int lastError time.Time } // New constructs a new circuit-breaker that starts closed. // From closed, the breaker opens if "errorThreshold" errors are seen // without an error-free period of at least "timeout". From open, the // breaker half-closes after "timeout". From half-open, the breaker closes // after "successThreshold" consecutive successes, or opens on a single error. func New(errorThreshold, successThreshold int, timeout time.Duration) *Breaker { return &Breaker{ errorThreshold: errorThreshold, successThreshold: successThreshold, timeout: timeout, } } // Run will either return ErrBreakerOpen immediately if the circuit-breaker is // already open, or it will run the given function and pass along its return // value. It is safe to call Run concurrently on the same Breaker. func (b *Breaker) Run(work func() error) error { state := b.GetState() if state == Open { return ErrBreakerOpen } return b.doWork(state, work) } // Go will either return ErrBreakerOpen immediately if the circuit-breaker is // already open, or it will run the given function in a separate goroutine. // If the function is run, Go will return nil immediately, and will *not* return // the return value of the function. It is safe to call Go concurrently on the // same Breaker. func (b *Breaker) Go(work func() error) error { state := b.GetState() if state == Open { return ErrBreakerOpen } // errcheck complains about ignoring the error return value, but // that's on purpose; if you want an error from a goroutine you have to // get it over a channel or something go b.doWork(state, work) return nil } // GetState returns the current State of the circuit-breaker at the moment // that it is called. func (b *Breaker) GetState() State { return (State)(atomic.LoadUint32((*uint32)(&b.state))) } func (b *Breaker) doWork(state State, work func() error) error { var panicValue interface{} result := func() error { defer func() { panicValue = recover() }() return work() }() if result == nil && panicValue == nil && state == Closed { // short-circuit the normal, success path without contending // on the lock return nil } // oh well, I guess we have to contend on the lock b.processResult(result, panicValue) if panicValue != nil { // as close as Go lets us come to a "rethrow" although unfortunately // we lose the original panicing location panic(panicValue) } return result } func (b *Breaker) processResult(result error, panicValue interface{}) { b.lock.Lock() defer b.lock.Unlock() if result == nil && panicValue == nil { if b.state == HalfOpen { b.successes++ if b.successes == b.successThreshold { b.closeBreaker() } } } else { if b.errors > 0 { expiry := b.lastError.Add(b.timeout) if time.Now().After(expiry) { b.errors = 0 } } switch b.state { case Closed: b.errors++ if b.errors == b.errorThreshold { b.openBreaker() } else { b.lastError = time.Now() } case HalfOpen: b.openBreaker() } } } func (b *Breaker) openBreaker() { b.changeState(Open) go b.timer() } func (b *Breaker) closeBreaker() { b.changeState(Closed) } func (b *Breaker) timer() { time.Sleep(b.timeout) b.lock.Lock() defer b.lock.Unlock() b.changeState(HalfOpen) } func (b *Breaker) changeState(newState State) { b.errors = 0 b.successes = 0 atomic.StoreUint32((*uint32)(&b.state), (uint32)(newState)) } golang-gopkg-eapache-go-resiliency.v1-1.7.0/breaker/breaker_test.go000066400000000000000000000120211472215331500251660ustar00rootroot00000000000000package breaker import ( "errors" "testing" "time" ) var errSomeError = errors.New("errSomeError") func alwaysPanics() error { panic("foo") } func returnsError() error { return errSomeError } func returnsSuccess() error { return nil } func TestBreakerErrorExpiry(t *testing.T) { breaker := New(2, 1, 10*time.Millisecond) if breaker.GetState() != Closed { t.Error("incorrect state") } for i := 0; i < 3; i++ { if err := breaker.Run(returnsError); err != errSomeError { t.Error(err) } time.Sleep(10 * time.Millisecond) } if breaker.GetState() != Closed { t.Error("incorrect state") } for i := 0; i < 3; i++ { if err := breaker.Go(returnsError); err != nil { t.Error(err) } time.Sleep(10 * time.Millisecond) } if breaker.GetState() != Closed { t.Error("incorrect state") } } func TestBreakerPanicsCountAsErrors(t *testing.T) { breaker := New(3, 2, 1*time.Second) if breaker.GetState() != Closed { t.Error("incorrect state") } // three errors opens the breaker for i := 0; i < 3; i++ { func() { defer func() { val := recover() if val.(string) != "foo" { t.Error("incorrect panic") } }() if err := breaker.Run(alwaysPanics); err != nil { t.Error(err) } t.Error("shouldn't get here") }() } // breaker is open if breaker.GetState() != Open { t.Error("incorrect state") } for i := 0; i < 5; i++ { if err := breaker.Run(returnsError); err != ErrBreakerOpen { t.Error(err) } } } func TestBreakerStateTransitions(t *testing.T) { breaker := New(3, 2, 10*time.Millisecond) if breaker.GetState() != Closed { t.Error("incorrect state") } // three errors opens the breaker for i := 0; i < 3; i++ { if err := breaker.Run(returnsError); err != errSomeError { t.Error(err) } } // breaker is open if breaker.GetState() != Open { t.Error("incorrect state") } for i := 0; i < 5; i++ { if err := breaker.Run(returnsError); err != ErrBreakerOpen { t.Error(err) } } // wait for it to half-close time.Sleep(20 * time.Millisecond) if breaker.GetState() != HalfOpen { t.Error("incorrect state") } // one success works, but is not enough to fully close if err := breaker.Run(returnsSuccess); err != nil { t.Error(err) } // error works, but re-opens immediately if err := breaker.Run(returnsError); err != errSomeError { t.Error(err) } // breaker is open if breaker.GetState() != Open { t.Error("incorrect state") } if err := breaker.Run(returnsError); err != ErrBreakerOpen { t.Error(err) } // wait for it to half-close time.Sleep(20 * time.Millisecond) if breaker.GetState() != HalfOpen { t.Error("incorrect state") } // two successes is enough to close it for good for i := 0; i < 2; i++ { if err := breaker.Run(returnsSuccess); err != nil { t.Error(err) } } if breaker.GetState() != Closed { t.Error("incorrect state") } // error works if err := breaker.Run(returnsError); err != errSomeError { t.Error(err) } // breaker is still closed if breaker.GetState() != Closed { t.Error("incorrect state") } if err := breaker.Run(returnsSuccess); err != nil { t.Error(err) } } func TestBreakerAsyncStateTransitions(t *testing.T) { breaker := New(3, 2, 10*time.Millisecond) // three errors opens the breaker for i := 0; i < 3; i++ { if err := breaker.Go(returnsError); err != nil { t.Error(err) } } // just enough to yield the scheduler and let the goroutines work off time.Sleep(1 * time.Millisecond) // breaker is open for i := 0; i < 5; i++ { if err := breaker.Go(returnsError); err != ErrBreakerOpen { t.Error(err) } } // wait for it to half-close time.Sleep(20 * time.Millisecond) // one success works, but is not enough to fully close if err := breaker.Go(returnsSuccess); err != nil { t.Error(err) } // error works, but re-opens immediately if err := breaker.Go(returnsError); err != nil { t.Error(err) } // just enough to yield the scheduler and let the goroutines work off time.Sleep(1 * time.Millisecond) // breaker is open if err := breaker.Go(returnsError); err != ErrBreakerOpen { t.Error(err) } // wait for it to half-close time.Sleep(20 * time.Millisecond) // two successes is enough to close it for good for i := 0; i < 2; i++ { if err := breaker.Go(returnsSuccess); err != nil { t.Error(err) } } // just enough to yield the scheduler and let the goroutines work off time.Sleep(1 * time.Millisecond) // error works if err := breaker.Go(returnsError); err != nil { t.Error(err) } // just enough to yield the scheduler and let the goroutines work off time.Sleep(1 * time.Millisecond) // breaker is still closed if err := breaker.Go(returnsSuccess); err != nil { t.Error(err) } } func ExampleBreaker() { breaker := New(3, 1, 5*time.Second) for { result := breaker.Run(func() error { // communicate with some external service and // return an error if the communication failed return nil }) switch result { case nil: // success! case ErrBreakerOpen: // our function wasn't run because the breaker was open default: // some other error } } } golang-gopkg-eapache-go-resiliency.v1-1.7.0/deadline/000077500000000000000000000000001472215331500223235ustar00rootroot00000000000000golang-gopkg-eapache-go-resiliency.v1-1.7.0/deadline/README.md000066400000000000000000000016111472215331500236010ustar00rootroot00000000000000deadline ======== [![Golang CI](https://github.com/eapache/go-resiliency/actions/workflows/golang-ci.yml/badge.svg)](https://github.com/eapache/go-resiliency/actions/workflows/golang-ci.yml) [![GoDoc](https://godoc.org/github.com/eapache/go-resiliency/deadline?status.svg)](https://godoc.org/github.com/eapache/go-resiliency/deadline) [![Code of Conduct](https://img.shields.io/badge/code%20of%20conduct-active-blue.svg)](https://eapache.github.io/conduct.html) The deadline/timeout resiliency pattern for golang. Creating a deadline takes one parameter: how long to wait. ```go dl := deadline.New(1 * time.Second) err := dl.Run(func(stopper <-chan struct{}) error { // do something potentially slow // give up when the `stopper` channel is closed (indicating a time-out) return nil }) switch err { case deadline.ErrTimedOut: // execution took too long, oops default: // some other error } ``` golang-gopkg-eapache-go-resiliency.v1-1.7.0/deadline/deadline.go000066400000000000000000000026211472215331500244200ustar00rootroot00000000000000// Package deadline implements the deadline (also known as "timeout") resiliency pattern for Go. package deadline import ( "errors" "time" ) // ErrTimedOut is the error returned from Run when the deadline expires. var ErrTimedOut = errors.New("timed out waiting for function to finish") // Deadline implements the deadline/timeout resiliency pattern. type Deadline struct { timeout time.Duration } // New constructs a new Deadline with the given timeout. func New(timeout time.Duration) *Deadline { return &Deadline{ timeout: timeout, } } // Run runs the given function, passing it a stopper channel. If the deadline passes before // the function finishes executing, Run returns ErrTimeOut to the caller and closes the stopper // channel so that the work function can attempt to exit gracefully. It does not (and cannot) // kill the running function's goroutine, so if the function doesn't respect the stopper channel, // then it may keep running after the deadline passes. If the function finishes before the // deadline, then the return value of the function is returned from Run. func (d *Deadline) Run(work func(<-chan struct{}) error) error { result := make(chan error, 1) stopper := make(chan struct{}) go func() { result <- work(stopper) }() timer := time.NewTimer(d.timeout) select { case ret := <-result: timer.Stop() return ret case <-timer.C: close(stopper) return ErrTimedOut } } golang-gopkg-eapache-go-resiliency.v1-1.7.0/deadline/deadline_test.go000066400000000000000000000021541472215331500254600ustar00rootroot00000000000000package deadline import ( "errors" "testing" "time" ) func takesFiveMillis(stopper <-chan struct{}) error { time.Sleep(5 * time.Millisecond) return nil } func takesTwentyMillis(stopper <-chan struct{}) error { time.Sleep(20 * time.Millisecond) return nil } func returnsError(stopper <-chan struct{}) error { return errors.New("foo") } func TestDeadline(t *testing.T) { dl := New(10 * time.Millisecond) if err := dl.Run(takesFiveMillis); err != nil { t.Error(err) } if err := dl.Run(takesTwentyMillis); err != ErrTimedOut { t.Error(err) } if err := dl.Run(returnsError); err.Error() != "foo" { t.Error(err) } done := make(chan struct{}) err := dl.Run(func(stopper <-chan struct{}) error { <-stopper close(done) return nil }) if err != ErrTimedOut { t.Error(err) } <-done } func ExampleDeadline() { dl := New(1 * time.Second) err := dl.Run(func(stopper <-chan struct{}) error { // do something possibly slow // check stopper function and give up if timed out return nil }) switch err { case ErrTimedOut: // execution took too long, oops default: // some other error } } golang-gopkg-eapache-go-resiliency.v1-1.7.0/go.mod000066400000000000000000000000611472215331500216610ustar00rootroot00000000000000module github.com/eapache/go-resiliency go 1.13 golang-gopkg-eapache-go-resiliency.v1-1.7.0/retrier/000077500000000000000000000000001472215331500222325ustar00rootroot00000000000000golang-gopkg-eapache-go-resiliency.v1-1.7.0/retrier/README.md000066400000000000000000000016131472215331500235120ustar00rootroot00000000000000retrier ======= [![Golang CI](https://github.com/eapache/go-resiliency/actions/workflows/golang-ci.yml/badge.svg)](https://github.com/eapache/go-resiliency/actions/workflows/golang-ci.yml) [![GoDoc](https://godoc.org/github.com/eapache/go-resiliency/retrier?status.svg)](https://godoc.org/github.com/eapache/go-resiliency/retrier) [![Code of Conduct](https://img.shields.io/badge/code%20of%20conduct-active-blue.svg)](https://eapache.github.io/conduct.html) The retriable resiliency pattern for golang. Creating a retrier takes two parameters: - the times to back-off between retries (and implicitly the number of times to retry) - the classifier that determines which errors to retry ```go r := retrier.New(retrier.ConstantBackoff(3, 100*time.Millisecond), nil) err := r.Run(func() error { // do some work return nil }) if err != nil { // handle the case where the work failed three times } ``` golang-gopkg-eapache-go-resiliency.v1-1.7.0/retrier/backoffs.go000066400000000000000000000022561472215331500243440ustar00rootroot00000000000000package retrier import "time" // ConstantBackoff generates a simple back-off strategy of retrying 'n' times, and waiting 'amount' time after each one. func ConstantBackoff(n int, amount time.Duration) []time.Duration { ret := make([]time.Duration, n) for i := range ret { ret[i] = amount } return ret } // ExponentialBackoff generates a simple back-off strategy of retrying 'n' times, and doubling the amount of // time waited after each one. func ExponentialBackoff(n int, initialAmount time.Duration) []time.Duration { ret := make([]time.Duration, n) next := initialAmount for i := range ret { ret[i] = next next *= 2 } return ret } // LimitedExponentialBackoff generates a simple back-off strategy of retrying 'n' times, and doubling the amount of // time waited after each one. // If back-off reaches `limitAmount` , thereafter back-off will be filled with `limitAmount` . func LimitedExponentialBackoff(n int, initialAmount time.Duration, limitAmount time.Duration) []time.Duration { ret := make([]time.Duration, n) next := initialAmount for i := range ret { if next < limitAmount { ret[i] = next next *= 2 } else { ret[i] = limitAmount } } return ret } golang-gopkg-eapache-go-resiliency.v1-1.7.0/retrier/backoffs_test.go000066400000000000000000000032221472215331500253750ustar00rootroot00000000000000package retrier import ( "testing" "time" ) func TestConstantBackoff(t *testing.T) { b := ConstantBackoff(1, 10*time.Millisecond) if len(b) != 1 { t.Error("incorrect length") } for i := range b { if b[i] != 10*time.Millisecond { t.Error("incorrect value at", i) } } b = ConstantBackoff(10, 250*time.Hour) if len(b) != 10 { t.Error("incorrect length") } for i := range b { if b[i] != 250*time.Hour { t.Error("incorrect value at", i) } } } func TestExponentialBackoff(t *testing.T) { b := ExponentialBackoff(1, 10*time.Millisecond) if len(b) != 1 { t.Error("incorrect length") } if b[0] != 10*time.Millisecond { t.Error("incorrect value") } b = ExponentialBackoff(4, 1*time.Minute) if len(b) != 4 { t.Error("incorrect length") } if b[0] != 1*time.Minute { t.Error("incorrect value") } if b[1] != 2*time.Minute { t.Error("incorrect value") } if b[2] != 4*time.Minute { t.Error("incorrect value") } if b[3] != 8*time.Minute { t.Error("incorrect value") } } func TestLimitedExponentialBackoff(t *testing.T) { b := LimitedExponentialBackoff(1, 10*time.Millisecond, 11*time.Millisecond) if len(b) != 1 { t.Error("incorrect length") } if b[0] != 10*time.Millisecond { t.Error("incorrect value") } b = LimitedExponentialBackoff(5, 1*time.Minute, 4*time.Minute) if len(b) != 5 { t.Error("incorrect length") } if b[0] != 1*time.Minute { t.Error("incorrect value") } if b[1] != 2*time.Minute { t.Error("incorrect value") } if b[2] != 4*time.Minute { t.Error("incorrect value") } if b[3] != 4*time.Minute { t.Error("incorrect value") } if b[4] != 4*time.Minute { t.Error("incorrect value") } } golang-gopkg-eapache-go-resiliency.v1-1.7.0/retrier/classifier.go000066400000000000000000000035271472215331500247140ustar00rootroot00000000000000package retrier import "errors" // Action is the type returned by a Classifier to indicate how the Retrier should proceed. type Action int const ( Succeed Action = iota // Succeed indicates the Retrier should treat this value as a success. Fail // Fail indicates the Retrier should treat this value as a hard failure and not retry. Retry // Retry indicates the Retrier should treat this value as a soft failure and retry. ) // Classifier is the interface implemented by anything that can classify Errors for a Retrier. type Classifier interface { Classify(error) Action } // DefaultClassifier classifies errors in the simplest way possible. If // the error is nil, it returns Succeed, otherwise it returns Retry. type DefaultClassifier struct{} // Classify implements the Classifier interface. func (c DefaultClassifier) Classify(err error) Action { if err == nil { return Succeed } return Retry } // WhitelistClassifier classifies errors based on a whitelist. If the error is nil, it // returns Succeed; if the error is in the whitelist, it returns Retry; otherwise, it returns Fail. type WhitelistClassifier []error // Classify implements the Classifier interface. func (list WhitelistClassifier) Classify(err error) Action { if err == nil { return Succeed } for _, pass := range list { if errors.Is(err, pass) { return Retry } } return Fail } // BlacklistClassifier classifies errors based on a blacklist. If the error is nil, it // returns Succeed; if the error is in the blacklist, it returns Fail; otherwise, it returns Retry. type BlacklistClassifier []error // Classify implements the Classifier interface. func (list BlacklistClassifier) Classify(err error) Action { if err == nil { return Succeed } for _, pass := range list { if errors.Is(err, pass) { return Fail } } return Retry } golang-gopkg-eapache-go-resiliency.v1-1.7.0/retrier/classifier_test.go000066400000000000000000000040501472215331500257430ustar00rootroot00000000000000package retrier import ( "errors" "testing" ) var ( errFoo = errors.New("FOO") errBar = errors.New("BAR") errBaz = errors.New("BAZ") ) func TestDefaultClassifier(t *testing.T) { c := DefaultClassifier{} if c.Classify(nil) != Succeed { t.Error("default misclassified nil") } if c.Classify(errFoo) != Retry { t.Error("default misclassified foo") } if c.Classify(errBar) != Retry { t.Error("default misclassified bar") } if c.Classify(errBaz) != Retry { t.Error("default misclassified baz") } } type wrappedErr struct { error } func (w wrappedErr) Error() string { return "there's an error happening during X: " + w.Error() } func (w wrappedErr) Unwrap() error { return w.error } func TestWhitelistClassifier(t *testing.T) { c := WhitelistClassifier{errFoo, errBar} if c.Classify(nil) != Succeed { t.Error("whitelist misclassified nil") } if c.Classify(errFoo) != Retry { t.Error("whitelist misclassified foo") } if c.Classify(errBar) != Retry { t.Error("whitelist misclassified bar") } if c.Classify(errBaz) != Fail { t.Error("whitelist misclassified baz") } if c.Classify(wrappedErr{error: errFoo}) != Retry { t.Error("whitelist misclassified foo") } if c.Classify(wrappedErr{error: errBar}) != Retry { t.Error("whitelist misclassified bar") } if c.Classify(wrappedErr{error: errBaz}) != Fail { t.Error("whitelist misclassified baz") } } func TestBlacklistClassifier(t *testing.T) { c := BlacklistClassifier{errBar} if c.Classify(nil) != Succeed { t.Error("blacklist misclassified nil") } if c.Classify(errFoo) != Retry { t.Error("blacklist misclassified foo") } if c.Classify(errBar) != Fail { t.Error("blacklist misclassified bar") } if c.Classify(errBaz) != Retry { t.Error("blacklist misclassified baz") } if c.Classify(wrappedErr{error: errFoo}) != Retry { t.Error("blacklist misclassified foo") } if c.Classify(wrappedErr{error: errBar}) != Fail { t.Error("blacklist misclassified bar") } if c.Classify(wrappedErr{error: errBaz}) != Retry { t.Error("blacklist misclassified baz") } } golang-gopkg-eapache-go-resiliency.v1-1.7.0/retrier/retrier.go000066400000000000000000000107421472215331500242410ustar00rootroot00000000000000// Package retrier implements the "retriable" resiliency pattern for Go. package retrier import ( "context" "math/rand" "sync" "time" ) // Retrier implements the "retriable" resiliency pattern, abstracting out the process of retrying a failed action // a certain number of times with an optional back-off between each retry. type Retrier struct { backoff []time.Duration infiniteRetry bool surfaceWorkErrors bool class Classifier jitter float64 rand *rand.Rand randMu sync.Mutex } // New constructs a Retrier with the given backoff pattern and classifier. The length of the backoff pattern // indicates how many times an action will be retried, and the value at each index indicates the amount of time // waited before each subsequent retry. The classifier is used to determine which errors should be retried and // which should cause the retrier to fail fast. The DefaultClassifier is used if nil is passed. func New(backoff []time.Duration, class Classifier) *Retrier { if class == nil { class = DefaultClassifier{} } return &Retrier{ backoff: backoff, class: class, rand: rand.New(rand.NewSource(time.Now().UnixNano())), } } // WithInfiniteRetry set the retrier to loop infinitely on the last backoff duration. Using this option, // the program will not exit until the retried function has been executed successfully. // WARNING : This may run indefinitely. func (r *Retrier) WithInfiniteRetry() *Retrier { r.infiniteRetry = true return r } // WithSurfaceWorkErrors configures the retrier to always return the last error received from work function // even if a context timeout/deadline is hit. func (r *Retrier) WithSurfaceWorkErrors() *Retrier { r.surfaceWorkErrors = true return r } // Run executes the given work function by executing RunCtx without context.Context. func (r *Retrier) Run(work func() error) error { return r.RunFn(context.Background(), func(c context.Context, r int) error { // never use ctx return work() }) } // RunCtx executes the given work function, then classifies its return value based on the classifier used // to construct the Retrier. If the result is Succeed or Fail, the return value of the work function is // returned to the caller. If the result is Retry, then Run sleeps according to the its backoff policy // before retrying. If the total number of retries is exceeded then the return value of the work function // is returned to the caller regardless. func (r *Retrier) RunCtx(ctx context.Context, work func(ctx context.Context) error) error { return r.RunFn(ctx, func(c context.Context, r int) error { return work(c) }) } // RunFn executes the given work function, then classifies its return value based on the classifier used // to construct the Retrier. If the result is Succeed or Fail, the return value of the work function is // returned to the caller. If the result is Retry, then Run sleeps according to the backoff policy // before retrying. If the total number of retries is exceeded then the return value of the work function // is returned to the caller regardless. The work function takes 2 args, the context and // the number of attempted retries. func (r *Retrier) RunFn(ctx context.Context, work func(ctx context.Context, retries int) error) error { retries := 0 for { ret := work(ctx, retries) switch r.class.Classify(ret) { case Succeed, Fail: return ret case Retry: if !r.infiniteRetry && retries >= len(r.backoff) { return ret } timer := time.NewTimer(r.calcSleep(retries)) if err := r.sleep(ctx, timer); err != nil { if r.surfaceWorkErrors { return ret } return err } retries++ } } } func (r *Retrier) sleep(ctx context.Context, timer *time.Timer) error { select { case <-timer.C: return nil case <-ctx.Done(): timer.Stop() return ctx.Err() } } func (r *Retrier) calcSleep(i int) time.Duration { if i >= len(r.backoff) { i = len(r.backoff) - 1 } // lock unsafe rand prng r.randMu.Lock() defer r.randMu.Unlock() // take a random float in the range (-r.jitter, +r.jitter) and multiply it by the base amount return r.backoff[i] + time.Duration(((r.rand.Float64()*2)-1)*r.jitter*float64(r.backoff[i])) } // SetJitter sets the amount of jitter on each back-off to a factor between 0.0 and 1.0 (values outside this range // are silently ignored). When a retry occurs, the back-off is adjusted by a random amount up to this value. func (r *Retrier) SetJitter(jit float64) { if jit < 0 || jit > 1 { return } r.jitter = jit } golang-gopkg-eapache-go-resiliency.v1-1.7.0/retrier/retrier_test.go000066400000000000000000000134261472215331500253020ustar00rootroot00000000000000package retrier import ( "context" "errors" "testing" "time" ) var i int func genWork(returns []error) func() error { i = 0 return func() error { i++ if i > len(returns) { return nil } return returns[i-1] } } func genWorkWithCtx() func(ctx context.Context) error { i = 0 return func(ctx context.Context) error { select { case <-ctx.Done(): return errFoo default: i++ } return nil } } func TestRetrier(t *testing.T) { r := New([]time.Duration{0, 10 * time.Millisecond}, WhitelistClassifier{errFoo}) err := r.Run(genWork([]error{errFoo, errFoo})) if err != nil { t.Error(err) } if i != 3 { t.Error("run wrong number of times") } err = r.Run(genWork([]error{errFoo, errBar})) if err != errBar { t.Error(err) } if i != 2 { t.Error("run wrong number of times") } err = r.Run(genWork([]error{errBar, errBaz})) if err != errBar { t.Error(err) } if i != 1 { t.Error("run wrong number of times") } } func TestRetrierCtx(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) r := New([]time.Duration{0, 10 * time.Millisecond}, WhitelistClassifier{}) err := r.RunCtx(ctx, genWorkWithCtx()) if err != nil { t.Error(err) } if i != 1 { t.Error("run wrong number of times") } cancel() err = r.RunCtx(ctx, genWorkWithCtx()) if err != errFoo { t.Error("context must be cancelled") } if i != 0 { t.Error("run wrong number of times") } } func TestRetrierCtxError(t *testing.T) { ctx := context.Background() r := New([]time.Duration{0, 10 * time.Millisecond}, nil) errExpected := []error{errFoo, errFoo, errBar, errBaz} retries := 0 err := r.RunCtx(ctx, func(ctx context.Context) error { if retries >= len(errExpected) { return nil } err := errExpected[retries] retries++ return err }) if err != errBar { t.Error(err) } } func TestRetrierRunFnError(t *testing.T) { ctx := context.Background() r := New([]time.Duration{0, 10 * time.Millisecond}, nil) errExpected := []error{errFoo, errFoo, errBar, errBaz} err := r.RunFn(ctx, func(ctx context.Context, retries int) error { if retries >= len(errExpected) { return nil } return errExpected[retries] }) if err != errBar { t.Error(err) } } func TestRetrierCtxWithInfinite(t *testing.T) { ctx := context.Background() r := New([]time.Duration{0, 10 * time.Millisecond}, nil).WithInfiniteRetry() errExpected := []error{errFoo, errFoo, errFoo, errBar, errBaz} retries := 0 err := r.RunCtx(ctx, func(ctx context.Context) error { if retries >= len(errExpected) { return nil } err := errExpected[retries] retries++ return err }) if err != nil { t.Error(err) } } func TestRetrierRunFnWithInfinite(t *testing.T) { ctx := context.Background() r := New([]time.Duration{0, 10 * time.Millisecond}, nil).WithInfiniteRetry() errExpected := []error{errFoo, errFoo, errFoo, errBar, errBaz} err := r.RunFn(ctx, func(ctx context.Context, retries int) error { if retries >= len(errExpected) { return nil } return errExpected[retries] }) if err != nil { t.Error(err) } } func TestRetrierRunFnWithSurfaceWorkErrors(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() r := New([]time.Duration{0, 10 * time.Millisecond}, nil).WithSurfaceWorkErrors() errExpected := []error{errFoo, errBar, errBaz} err := r.RunFn(ctx, func(ctx context.Context, retries int) error { if retries >= len(errExpected) { return nil } if retries == 1 { // Context canceled inside second call to work function. cancel() } err := errExpected[retries] retries++ return err }) if err != errBar { t.Error(err) } } func TestRetrierRunFnWithoutSurfaceWorkErrors(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() r := New([]time.Duration{0, 10 * time.Millisecond}, nil) errExpected := []error{errFoo, errBar, errBaz} err := r.RunFn(ctx, func(ctx context.Context, retries int) error { if retries >= len(errExpected) { return nil } if retries == 1 { // Context canceled inside second call to work function. cancel() } err := errExpected[retries] retries++ return err }) if err != context.Canceled { t.Error(err) } } func TestRetrierNone(t *testing.T) { r := New(nil, nil) i = 0 err := r.Run(func() error { i++ return errFoo }) if err != errFoo { t.Error(err) } if i != 1 { t.Error("run wrong number of times") } i = 0 err = r.Run(func() error { i++ return nil }) if err != nil { t.Error(err) } if i != 1 { t.Error("run wrong number of times") } } func TestRetrierJitter(t *testing.T) { r := New([]time.Duration{0, 10 * time.Millisecond, 4 * time.Hour}, nil) if r.calcSleep(0) != 0 { t.Error("Incorrect sleep calculated") } if r.calcSleep(1) != 10*time.Millisecond { t.Error("Incorrect sleep calculated") } if r.calcSleep(2) != 4*time.Hour { t.Error("Incorrect sleep calculated") } r.SetJitter(0.25) for i := 0; i < 20; i++ { if r.calcSleep(0) != 0 { t.Error("Incorrect sleep calculated") } slp := r.calcSleep(1) if slp < 7500*time.Microsecond || slp > 12500*time.Microsecond { t.Error("Incorrect sleep calculated") } slp = r.calcSleep(2) if slp < 3*time.Hour || slp > 5*time.Hour { t.Error("Incorrect sleep calculated") } } r.SetJitter(-1) if r.jitter != 0.25 { t.Error("Invalid jitter value accepted") } r.SetJitter(2) if r.jitter != 0.25 { t.Error("Invalid jitter value accepted") } } func TestRetrierThreadSafety(t *testing.T) { r := New([]time.Duration{0}, nil) for i := 0; i < 2; i++ { go func() { r.Run(func() error { return errors.New("error") }) }() } } func ExampleRetrier() { r := New(ConstantBackoff(3, 100*time.Millisecond), nil) err := r.Run(func() error { // do some work return nil }) if err != nil { // handle the case where the work failed three times } } golang-gopkg-eapache-go-resiliency.v1-1.7.0/semaphore/000077500000000000000000000000001472215331500225415ustar00rootroot00000000000000golang-gopkg-eapache-go-resiliency.v1-1.7.0/semaphore/README.md000066400000000000000000000015101472215331500240150ustar00rootroot00000000000000semaphore ========= [![Golang CI](https://github.com/eapache/go-resiliency/actions/workflows/golang-ci.yml/badge.svg)](https://github.com/eapache/go-resiliency/actions/workflows/golang-ci.yml) [![GoDoc](https://godoc.org/github.com/eapache/go-resiliency/semaphore?status.svg)](https://godoc.org/github.com/eapache/go-resiliency/semaphore) [![Code of Conduct](https://img.shields.io/badge/code%20of%20conduct-active-blue.svg)](https://eapache.github.io/conduct.html) The semaphore resiliency pattern for golang. Creating a semaphore takes two parameters: - ticket count (how many tickets to give out at once) - timeout (how long to wait for a ticket if none are currently available) ```go sem := semaphore.New(3, 1*time.Second) if err := sem.Acquire(); err != nil { // could not acquire semaphore return err } defer sem.Release() ``` golang-gopkg-eapache-go-resiliency.v1-1.7.0/semaphore/semaphore.go000066400000000000000000000031201472215331500250470ustar00rootroot00000000000000// Package semaphore implements the semaphore resiliency pattern for Go. package semaphore import ( "errors" "time" ) // ErrNoTickets is the error returned by Acquire when it could not acquire // a ticket from the semaphore within the configured timeout. var ErrNoTickets = errors.New("could not acquire semaphore ticket") // Semaphore implements the semaphore resiliency pattern type Semaphore struct { sem chan struct{} timeout time.Duration } // New constructs a new Semaphore with the given ticket-count // and timeout. func New(tickets int, timeout time.Duration) *Semaphore { return &Semaphore{ sem: make(chan struct{}, tickets), timeout: timeout, } } // Acquire tries to acquire a ticket from the semaphore. If it can, it returns nil. // If it cannot after "timeout" amount of time, it returns ErrNoTickets. It is // safe to call Acquire concurrently on a single Semaphore. func (s *Semaphore) Acquire() error { timer := time.NewTimer(s.timeout) select { case s.sem <- struct{}{}: timer.Stop() return nil case <-timer.C: return ErrNoTickets } } // Release releases an acquired ticket back to the semaphore. It is safe to call // Release concurrently on a single Semaphore. It is an error to call Release on // a Semaphore from which you have not first acquired a ticket. func (s *Semaphore) Release() { <-s.sem } // IsEmpty will return true if no tickets are being held at that instant. // It is safe to call concurrently with Acquire and Release, though do note // that the result may then be unpredictable. func (s *Semaphore) IsEmpty() bool { return len(s.sem) == 0 } golang-gopkg-eapache-go-resiliency.v1-1.7.0/semaphore/semaphore_test.go000066400000000000000000000025321472215331500261140ustar00rootroot00000000000000package semaphore import ( "testing" "time" ) func TestSemaphoreAcquireRelease(t *testing.T) { sem := New(3, 1*time.Second) for i := 0; i < 10; i++ { if err := sem.Acquire(); err != nil { t.Error(err) } if err := sem.Acquire(); err != nil { t.Error(err) } if err := sem.Acquire(); err != nil { t.Error(err) } sem.Release() sem.Release() sem.Release() } } func TestSemaphoreBlockTimeout(t *testing.T) { sem := New(1, 200*time.Millisecond) if err := sem.Acquire(); err != nil { t.Error(err) } start := time.Now() if err := sem.Acquire(); err != ErrNoTickets { t.Error(err) } if start.Add(200 * time.Millisecond).After(time.Now()) { t.Error("semaphore did not wait long enough") } sem.Release() if err := sem.Acquire(); err != nil { t.Error(err) } } func TestSemaphoreEmpty(t *testing.T) { sem := New(2, 200*time.Millisecond) if !sem.IsEmpty() { t.Error("semaphore should be empty") } sem.Acquire() if sem.IsEmpty() { t.Error("semaphore should not be empty") } sem.Release() if !sem.IsEmpty() { t.Error("semaphore should be empty") } } func ExampleSemaphore() { sem := New(3, 1*time.Second) for i := 0; i < 10; i++ { go func() { if err := sem.Acquire(); err != nil { return //could not acquire semaphore } defer sem.Release() // do something semaphore-guarded }() } }