From ed3cbab3848e9c6011edb1002c2cb622446e5980 Mon Sep 17 00:00:00 2001 From: Edward Date: Sun, 3 May 2020 10:53:43 +0800 Subject: [PATCH] add a semaphore pattern --- gomore/08_semaphore/semaphore/semaphore.go | 52 ++++++++++++ .../08_semaphore/semaphore/semaphore_test.go | 81 +++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 gomore/08_semaphore/semaphore/semaphore.go create mode 100644 gomore/08_semaphore/semaphore/semaphore_test.go diff --git a/gomore/08_semaphore/semaphore/semaphore.go b/gomore/08_semaphore/semaphore/semaphore.go new file mode 100644 index 0000000..d4aaa04 --- /dev/null +++ b/gomore/08_semaphore/semaphore/semaphore.go @@ -0,0 +1,52 @@ +// 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 { + select { + case s.sem <- struct{}{}: + return nil + case <-time.After(s.timeout): + 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 +} diff --git a/gomore/08_semaphore/semaphore/semaphore_test.go b/gomore/08_semaphore/semaphore/semaphore_test.go new file mode 100644 index 0000000..08da791 --- /dev/null +++ b/gomore/08_semaphore/semaphore/semaphore_test.go @@ -0,0 +1,81 @@ +package 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 + }() + } +}