Forráskód Böngészése

Implement breaker.Go()

Properly supports launching breaker-protected goroutines.
Evan Huus 11 éve
szülő
commit
73049397fc
2 módosított fájl, 109 hozzáadás és 1 törlés
  1. 42 0
      breaker/breaker.go
  2. 67 1
      breaker/breaker_test.go

+ 42 - 0
breaker/breaker.go

@@ -81,6 +81,48 @@ func (b *Breaker) Run(x func() error) error {
 	return result
 }
 
+// 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(x func() error) error {
+	b.lock.RLock()
+	state := b.state
+	b.lock.RUnlock()
+
+	if state == open {
+		return ErrBreakerOpen
+	}
+
+	go func() {
+		var panicValue interface{}
+
+		result := func() error {
+			defer func() {
+				panicValue = recover()
+			}()
+			return x()
+		}()
+
+		if result == nil && panicValue == nil && state == closed {
+			// short-circuit the normal, success path without
+			// contending on the lock
+			return
+		}
+
+		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 nil
+}
+
 func (b *Breaker) processResult(result error, panicValue interface{}) {
 	b.lock.Lock()
 	defer b.lock.Unlock()

+ 67 - 1
breaker/breaker_test.go

@@ -19,12 +19,19 @@ func returnsSuccess() error {
 func TestBreakerErrorExpiry(t *testing.T) {
 	breaker := New(2, 1, 1*time.Second)
 
-	for i := 0; i < 5; i++ {
+	for i := 0; i < 3; i++ {
 		if err := breaker.Run(returnsError); err != errSomeError {
 			t.Error(err)
 		}
 		time.Sleep(1 * time.Second)
 	}
+
+	for i := 0; i < 3; i++ {
+		if err := breaker.Go(returnsError); err != nil {
+			t.Error(err)
+		}
+		time.Sleep(1 * time.Second)
+	}
 }
 
 func TestBreakerStateTransitions(t *testing.T) {
@@ -77,6 +84,65 @@ func TestBreakerStateTransitions(t *testing.T) {
 	}
 }
 
+func TestBreakerAsyncStateTransitions(t *testing.T) {
+	breaker := New(3, 2, 1*time.Second)
+
+	// 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(2 * time.Second)
+	// 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(2 * time.Second)
+	// 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)