Explorar el Código

Implement semaphore pattern

Evan Huus hace 11 años
padre
commit
008c74ab45
Se han modificado 3 ficheros con 124 adiciones y 0 borrados
  1. 18 0
      semaphore/README.md
  2. 45 0
      semaphore/semaphore.go
  3. 61 0
      semaphore/semaphore_test.go

+ 18 - 0
semaphore/README.md

@@ -0,0 +1,18 @@
+semaphore
+=========
+
+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()
+```

+ 45 - 0
semaphore/semaphore.go

@@ -0,0 +1,45 @@
+// 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 aquire 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
+}

+ 61 - 0
semaphore/semaphore_test.go

@@ -0,0 +1,61 @@
+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 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
+		}()
+	}
+}