mirror of
https://github.com/hanzo-fi/go-libs.git
synced 2026-10-30 20:53:44 +00:00
955 lines
25 KiB
Go
955 lines
25 KiB
Go
package circuit
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ThreeDotsLabs/watermill/message"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.opentelemetry.io/otel"
|
|
"go.opentelemetry.io/otel/propagation"
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
"github.com/hanzo-fi/go-libs/v5/pkg/messaging/publish/circuit/storage"
|
|
logging "github.com/hanzo-fi/go-libs/v5/pkg/observe/log"
|
|
)
|
|
|
|
func TestCircuitBreaker(t *testing.T) {
|
|
t.Run("nominal", func(t *testing.T) {
|
|
messages := make(chan *testMessages, 10)
|
|
publisher := newMockPublisher(messages)
|
|
store := newMockStore()
|
|
|
|
cb := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
publisher,
|
|
store,
|
|
5*time.Second,
|
|
)
|
|
go cb.Loop(logging.TestingContext())
|
|
|
|
payload, err := json.Marshal("test")
|
|
require.NoError(t, err)
|
|
msg := message.NewMessage(uuid.New().String(), payload)
|
|
require.NoError(t, cb.Publish("test", msg))
|
|
|
|
select {
|
|
case m := <-messages:
|
|
require.Equal(t, "test", m.topic)
|
|
var payload string
|
|
require.NoError(t, json.Unmarshal(m.msg.Payload, &payload))
|
|
require.Equal(t, "test", payload)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Expected message but didn't receive one")
|
|
}
|
|
|
|
require.NoError(t, cb.Close())
|
|
})
|
|
|
|
t.Run("error publisher", func(t *testing.T) {
|
|
messages := make(chan *testMessages, 100)
|
|
defer close(messages)
|
|
|
|
errTest := errors.New("test")
|
|
underlyingPublisher := newMockPublisher(messages)
|
|
store := newMockStore()
|
|
publisher := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
underlyingPublisher,
|
|
store,
|
|
5*time.Second,
|
|
)
|
|
defer publisher.Close()
|
|
|
|
go publisher.Loop(logging.TestingContext())
|
|
|
|
expectedP1, _ := json.Marshal(&payload{Result: 1})
|
|
m1 := message.NewMessage("1", expectedP1)
|
|
m1.Metadata.Set("foo", "bar")
|
|
err := publisher.Publish("test", m1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, StateClose, publisher.GetState())
|
|
|
|
underlyingPublisher.WithPublishError(errTest)
|
|
|
|
expectedP2, _ := json.Marshal(&payload{Result: 2})
|
|
m2 := message.NewMessage("2", expectedP2)
|
|
m2.Metadata.Set("foo", "bar")
|
|
err = publisher.Publish("test", m2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, StateOpen, publisher.GetState())
|
|
|
|
expectedP3, _ := json.Marshal(&payload{Result: 3})
|
|
m3 := message.NewMessage("3", expectedP3)
|
|
m3.Metadata.Set("foo2", "bar2")
|
|
err = publisher.Publish("test", m3)
|
|
require.NoError(t, err)
|
|
require.Equal(t, StateOpen, publisher.GetState())
|
|
|
|
require.Len(t, messages, 1)
|
|
msg1 := <-messages
|
|
require.Equal(t, "test", msg1.topic)
|
|
require.Equal(t, "1", msg1.msg.UUID)
|
|
require.Equal(t, message.Metadata(map[string]string{"foo": "bar"}), msg1.msg.Metadata)
|
|
p1 := &payload{}
|
|
_ = json.Unmarshal(msg1.msg.Payload, p1)
|
|
require.Equal(t, 1, p1.Result)
|
|
|
|
storedMessages, err := store.List(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, storedMessages, 2)
|
|
|
|
require.Equal(t, "test", storedMessages[0].Topic)
|
|
require.Equal(t, map[string]string{"foo": "bar"}, storedMessages[0].Metadata)
|
|
p2 := &payload{}
|
|
_ = json.Unmarshal(storedMessages[0].Data, p2)
|
|
require.Equal(t, 2, p2.Result)
|
|
|
|
require.Equal(t, "test", storedMessages[1].Topic)
|
|
require.Equal(t, map[string]string{"foo2": "bar2"}, storedMessages[1].Metadata)
|
|
p3 := &payload{}
|
|
_ = json.Unmarshal(storedMessages[1].Data, p3)
|
|
require.Equal(t, 3, p3.Result)
|
|
})
|
|
|
|
t.Run("error publisher and store", func(t *testing.T) {
|
|
messages := make(chan *testMessages, 100)
|
|
defer close(messages)
|
|
|
|
errTest := errors.New("test")
|
|
underlyingPublisher := newMockPublisher(messages)
|
|
store := newMockStore().WithInsertError(errTest)
|
|
publisher := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
underlyingPublisher,
|
|
store,
|
|
5*time.Second,
|
|
)
|
|
defer publisher.Close()
|
|
|
|
go publisher.Loop(logging.TestingContext())
|
|
|
|
expectedP1, _ := json.Marshal(&payload{Result: 1})
|
|
m1 := message.NewMessage("1", expectedP1)
|
|
m1.Metadata.Set("foo", "bar")
|
|
err := publisher.Publish("test", m1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, StateClose, publisher.GetState())
|
|
|
|
underlyingPublisher.WithPublishError(errTest)
|
|
|
|
expectedP2, _ := json.Marshal(&payload{Result: 2})
|
|
m2 := message.NewMessage("2", expectedP2)
|
|
m2.Metadata.Set("foo", "bar")
|
|
err = publisher.Publish("test", m2)
|
|
require.ErrorIs(t, err, errTest)
|
|
require.Equal(t, StateOpen, publisher.GetState())
|
|
|
|
expectedP3, _ := json.Marshal(&payload{Result: 3})
|
|
m3 := message.NewMessage("3", expectedP3)
|
|
m3.Metadata.Set("foo2", "bar2")
|
|
err = publisher.Publish("test", m3)
|
|
require.ErrorIs(t, err, errTest)
|
|
require.Equal(t, StateOpen, publisher.GetState())
|
|
|
|
require.Len(t, messages, 1)
|
|
msg1 := <-messages
|
|
require.Equal(t, "test", msg1.topic)
|
|
require.Equal(t, "1", msg1.msg.UUID)
|
|
require.Equal(t, message.Metadata(map[string]string{"foo": "bar"}), msg1.msg.Metadata)
|
|
p1 := &payload{}
|
|
_ = json.Unmarshal(msg1.msg.Payload, p1)
|
|
require.Equal(t, 1, p1.Result)
|
|
|
|
storedMessages, err := store.List(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, storedMessages, 0)
|
|
})
|
|
|
|
t.Run("error publisher but recover after x seconds", func(t *testing.T) {
|
|
messages := make(chan *testMessages, 100)
|
|
defer close(messages)
|
|
|
|
errTest := errors.New("test")
|
|
underlyingPublisher := newMockPublisher(messages)
|
|
store := newMockStore()
|
|
publisher := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
underlyingPublisher,
|
|
store,
|
|
5*time.Second,
|
|
)
|
|
defer publisher.Close()
|
|
|
|
go publisher.Loop(logging.TestingContext())
|
|
|
|
expectedP1, _ := json.Marshal(&payload{Result: 1})
|
|
m1 := message.NewMessage("1", expectedP1)
|
|
m1.Metadata.Set("foo", "bar")
|
|
err := publisher.Publish("test", m1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, StateClose, publisher.GetState())
|
|
|
|
underlyingPublisher.WithPublishError(errTest)
|
|
|
|
expectedP2, _ := json.Marshal(&payload{Result: 2})
|
|
m2 := message.NewMessage("2", expectedP2)
|
|
m2.Metadata.Set("foo", "bar")
|
|
err = publisher.Publish("test", m2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, StateOpen, publisher.GetState())
|
|
|
|
expectedP3, _ := json.Marshal(&payload{Result: 3})
|
|
m3 := message.NewMessage("3", expectedP3)
|
|
m3.Metadata.Set("foo2", "bar2")
|
|
err = publisher.Publish("test", m3)
|
|
require.NoError(t, err)
|
|
require.Equal(t, StateOpen, publisher.GetState())
|
|
|
|
require.Len(t, messages, 1)
|
|
msg1 := <-messages
|
|
require.Equal(t, "test", msg1.topic)
|
|
require.Equal(t, "1", msg1.msg.UUID)
|
|
require.Equal(t, message.Metadata(map[string]string{"foo": "bar"}), msg1.msg.Metadata)
|
|
p1 := &payload{}
|
|
_ = json.Unmarshal(msg1.msg.Payload, p1)
|
|
require.Equal(t, 1, p1.Result)
|
|
|
|
storedMessages, err := store.List(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, storedMessages, 2)
|
|
|
|
require.Equal(t, "test", storedMessages[0].Topic)
|
|
require.Equal(t, map[string]string{"foo": "bar"}, storedMessages[0].Metadata)
|
|
p2 := &payload{}
|
|
_ = json.Unmarshal(storedMessages[0].Data, p2)
|
|
require.Equal(t, 2, p2.Result)
|
|
|
|
require.Equal(t, "test", storedMessages[1].Topic)
|
|
require.Equal(t, map[string]string{"foo2": "bar2"}, storedMessages[1].Metadata)
|
|
p3 := &payload{}
|
|
_ = json.Unmarshal(storedMessages[1].Data, p3)
|
|
require.Equal(t, 3, p3.Result)
|
|
|
|
underlyingPublisher.WithPublishError(nil)
|
|
|
|
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
if !assert.Equal(c, StateClose, publisher.GetState()) {
|
|
return
|
|
}
|
|
|
|
// Now we must fail if state is closed and there is nothing or wrong
|
|
// data in the messages channel
|
|
|
|
require.Len(t, messages, 2)
|
|
|
|
msg2 := <-messages
|
|
require.Equal(t, "test", msg2.topic)
|
|
require.Equal(t, message.Metadata(map[string]string{"foo": "bar"}), msg2.msg.Metadata)
|
|
p2 := &payload{}
|
|
_ = json.Unmarshal(msg2.msg.Payload, p2)
|
|
require.Equal(t, 2, p2.Result)
|
|
|
|
msg3 := <-messages
|
|
require.Equal(t, "test", msg3.topic)
|
|
require.Equal(t, message.Metadata(map[string]string{"foo2": "bar2"}), msg3.msg.Metadata)
|
|
p3 := &payload{}
|
|
_ = json.Unmarshal(msg3.msg.Payload, p3)
|
|
require.Equal(t, 3, p3.Result)
|
|
|
|
storedMessages, err := store.List(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, storedMessages, 0)
|
|
}, 10*time.Second, 1*time.Second)
|
|
})
|
|
|
|
t.Run("otel context propagation", func(t *testing.T) {
|
|
messages := make(chan *testMessages, 100)
|
|
defer close(messages)
|
|
|
|
publisher := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
newMockPublisher(messages),
|
|
newMockStore(),
|
|
5*time.Second,
|
|
)
|
|
defer publisher.Close()
|
|
|
|
go publisher.Loop(logging.TestingContext())
|
|
|
|
// Test with valid otel context
|
|
metadata := map[string]string{
|
|
otelContextKey: `{"traceparent":"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}`,
|
|
}
|
|
msg := message.NewMessage("1", []byte("test"))
|
|
for k, v := range metadata {
|
|
msg.Metadata.Set(k, v)
|
|
}
|
|
|
|
err := publisher.Publish("test", msg)
|
|
require.NoError(t, err)
|
|
|
|
// Test with invalid otel context
|
|
metadata = map[string]string{
|
|
otelContextKey: `invalid json`,
|
|
}
|
|
msg = message.NewMessage("2", []byte("test"))
|
|
for k, v := range metadata {
|
|
msg.Metadata.Set(k, v)
|
|
}
|
|
|
|
store := newMockStore()
|
|
publisher = NewCircuitBreaker(
|
|
logging.Testing(),
|
|
newMockPublisher(messages).WithPublishError(errors.New("publish error")),
|
|
store,
|
|
5*time.Second,
|
|
)
|
|
defer publisher.Close()
|
|
|
|
go publisher.Loop(logging.TestingContext())
|
|
|
|
err = publisher.Publish("test", msg)
|
|
require.NoError(t, err)
|
|
|
|
// Verify that the message was stored with the invalid context
|
|
storedMessages, err := store.List(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, storedMessages, 1)
|
|
require.Equal(t, metadata, storedMessages[0].Metadata)
|
|
})
|
|
|
|
t.Run("multiple messages in single publish", func(t *testing.T) {
|
|
messages := make(chan *testMessages, 100)
|
|
defer close(messages)
|
|
|
|
publisher := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
newMockPublisher(messages),
|
|
newMockStore(),
|
|
5*time.Second,
|
|
)
|
|
defer publisher.Close()
|
|
|
|
go publisher.Loop(logging.TestingContext())
|
|
|
|
// Create multiple messages
|
|
msg1 := message.NewMessage("1", []byte("test1"))
|
|
msg2 := message.NewMessage("2", []byte("test2"))
|
|
msg3 := message.NewMessage("3", []byte("test3"))
|
|
|
|
// Publish multiple messages at once
|
|
err := publisher.Publish("test", msg1, msg2, msg3)
|
|
require.NoError(t, err)
|
|
|
|
// Verify all messages were published
|
|
require.Len(t, messages, 3)
|
|
receivedMsg1 := <-messages
|
|
require.Equal(t, "1", receivedMsg1.msg.UUID)
|
|
receivedMsg2 := <-messages
|
|
require.Equal(t, "2", receivedMsg2.msg.UUID)
|
|
receivedMsg3 := <-messages
|
|
require.Equal(t, "3", receivedMsg3.msg.UUID)
|
|
})
|
|
|
|
t.Run("catchup database with invalid message", func(t *testing.T) {
|
|
messages := make(chan *testMessages, 100)
|
|
defer close(messages)
|
|
|
|
store := newMockStore()
|
|
publisher := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
newMockPublisher(messages).WithPublishError(errors.New("publish error")),
|
|
store,
|
|
1*time.Millisecond, // Use a very short interval to trigger catchup quickly
|
|
)
|
|
|
|
// Insert an invalid message directly into the store
|
|
err := store.Insert(context.Background(), "test", []byte("invalid json"), map[string]string{
|
|
otelContextKey: "invalid json",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
go publisher.Loop(logging.TestingContext())
|
|
defer publisher.Close()
|
|
|
|
// Wait for the catchup to happen and verify the state changes
|
|
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
assert.Equal(c, StateOpen, publisher.GetState())
|
|
}, 2*time.Second, 100*time.Millisecond)
|
|
})
|
|
|
|
t.Run("store error during catchup", func(t *testing.T) {
|
|
messages := make(chan *testMessages, 100)
|
|
defer close(messages)
|
|
|
|
errTest := errors.New("store error")
|
|
store := newMockStore().WithListError(errTest)
|
|
|
|
publisher := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
newMockPublisher(messages),
|
|
store,
|
|
1*time.Millisecond,
|
|
)
|
|
|
|
go publisher.Loop(logging.TestingContext())
|
|
defer publisher.Close()
|
|
|
|
// Wait for the catchup to happen and verify the state changes
|
|
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
assert.Equal(c, StateOpen, publisher.GetState())
|
|
}, 2*time.Second, 100*time.Millisecond)
|
|
})
|
|
|
|
t.Run("store delete error during catchup", func(t *testing.T) {
|
|
messages := make(chan *testMessages, 100)
|
|
defer close(messages)
|
|
|
|
errTest := errors.New("delete error")
|
|
store := newMockStore().WithDeleteError(errTest)
|
|
|
|
// Insert a valid message
|
|
err := store.Insert(context.Background(), "test", []byte(`{"test":"data"}`), nil)
|
|
require.NoError(t, err)
|
|
|
|
circuitBreaker := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
newMockPublisher(messages),
|
|
store,
|
|
1*time.Millisecond,
|
|
)
|
|
|
|
go circuitBreaker.Loop(logging.TestingContext())
|
|
defer circuitBreaker.Close()
|
|
|
|
// Wait for the catchup to happen and verify the state changes
|
|
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
assert.Equal(c, StateOpen, circuitBreaker.GetState())
|
|
}, 2*time.Second, 100*time.Millisecond)
|
|
})
|
|
}
|
|
|
|
func TestCircuitBreakerCloseWithoutLoopIsIdempotent(t *testing.T) {
|
|
messages := make(chan *testMessages, 10)
|
|
publisher := newMockPublisher(messages)
|
|
circuitBreaker := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
publisher,
|
|
newMockStore(),
|
|
5*time.Second,
|
|
)
|
|
|
|
requireCloseWithin(t, circuitBreaker)
|
|
requireCloseWithin(t, circuitBreaker)
|
|
require.Equal(t, 1, publisher.CloseCount())
|
|
}
|
|
|
|
func TestCircuitBreakerCloseCancelsCatchUp(t *testing.T) {
|
|
store := &blockingListStore{
|
|
listStarted: make(chan struct{}, 1),
|
|
}
|
|
circuitBreaker := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
newMockPublisher(make(chan *testMessages, 10)),
|
|
store,
|
|
5*time.Second,
|
|
)
|
|
|
|
go circuitBreaker.Loop(context.Background())
|
|
|
|
select {
|
|
case <-store.listStarted:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("expected catchup to start")
|
|
}
|
|
|
|
requireCloseWithin(t, circuitBreaker)
|
|
}
|
|
|
|
func TestCircuitBreakerCloseDrainsInFlightInsert(t *testing.T) {
|
|
store := &blockingInsertStore{
|
|
insertStarted: make(chan struct{}, 1),
|
|
releaseInsert: make(chan struct{}),
|
|
insertCtxCanceled: make(chan struct{}, 1),
|
|
}
|
|
circuitBreaker := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
newMockPublisher(make(chan *testMessages, 10)).WithPublishError(errors.New("publish failed")),
|
|
store,
|
|
5*time.Second,
|
|
)
|
|
go circuitBreaker.Loop(context.Background())
|
|
|
|
publishDone := make(chan error, 1)
|
|
go func() {
|
|
publishDone <- circuitBreaker.Publish("test", message.NewMessage("1", []byte("payload")))
|
|
}()
|
|
|
|
select {
|
|
case <-store.insertStarted:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("expected insert to start")
|
|
}
|
|
|
|
closeDone := make(chan error, 1)
|
|
go func() {
|
|
closeDone <- circuitBreaker.Close()
|
|
}()
|
|
|
|
select {
|
|
case err := <-closeDone:
|
|
t.Fatalf("Close returned before insert completed: %v", err)
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
select {
|
|
case <-store.insertCtxCanceled:
|
|
t.Fatal("insert context was canceled before insert completed")
|
|
default:
|
|
}
|
|
|
|
close(store.releaseInsert)
|
|
|
|
select {
|
|
case err := <-closeDone:
|
|
require.NoError(t, err)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("expected Close to return after insert completed")
|
|
}
|
|
select {
|
|
case <-publishDone:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("expected publish to return after close")
|
|
}
|
|
require.True(t, store.inserted)
|
|
}
|
|
|
|
func TestCircuitBreakerPublishWaitsForAcceptedMessageDuringClose(t *testing.T) {
|
|
publisher := &blockingPublishPublisher{
|
|
publishStarted: make(chan struct{}, 1),
|
|
releasePublish: make(chan struct{}),
|
|
}
|
|
circuitBreaker := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
publisher,
|
|
newMockStore(),
|
|
5*time.Second,
|
|
)
|
|
go circuitBreaker.Loop(context.Background())
|
|
|
|
publishDone := make(chan error, 1)
|
|
go func() {
|
|
publishDone <- circuitBreaker.Publish("test", message.NewMessage("1", []byte("payload")))
|
|
}()
|
|
|
|
select {
|
|
case <-publisher.publishStarted:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("expected publish to start")
|
|
}
|
|
|
|
closeDone := make(chan error, 1)
|
|
go func() {
|
|
closeDone <- circuitBreaker.Close()
|
|
}()
|
|
|
|
select {
|
|
case err := <-publishDone:
|
|
t.Fatalf("Publish returned before accepted message completed: %v", err)
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
close(publisher.releasePublish)
|
|
|
|
select {
|
|
case err := <-publishDone:
|
|
require.NoError(t, err)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("expected Publish to return after accepted message completed")
|
|
}
|
|
select {
|
|
case err := <-closeDone:
|
|
require.NoError(t, err)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("expected Close to return after accepted message completed")
|
|
}
|
|
}
|
|
|
|
func TestWaitAcceptedMessageResultPrefersQueuedResultAfterStop(t *testing.T) {
|
|
errChan := make(chan error, 1)
|
|
errChan <- nil
|
|
stopped := make(chan struct{})
|
|
close(stopped)
|
|
|
|
require.NoError(t, waitAcceptedMessageResult(errChan, stopped))
|
|
}
|
|
|
|
func TestCircuitBreakerCloseDrainsInFlightReplayDelete(t *testing.T) {
|
|
messages := make(chan *testMessages, 10)
|
|
store := &blockingDeleteStore{
|
|
deleteStarted: make(chan struct{}, 1),
|
|
releaseDelete: make(chan struct{}),
|
|
deleteCtxCanceled: make(chan struct{}, 1),
|
|
messages: []*storage.CircuitBreakerModel{{
|
|
ID: 42,
|
|
Topic: "test",
|
|
Data: []byte("payload"),
|
|
}},
|
|
}
|
|
circuitBreaker := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
newMockPublisher(messages),
|
|
store,
|
|
5*time.Second,
|
|
)
|
|
go circuitBreaker.Loop(context.Background())
|
|
|
|
select {
|
|
case <-messages:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("expected replayed message")
|
|
}
|
|
select {
|
|
case <-store.deleteStarted:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("expected delete to start")
|
|
}
|
|
|
|
closeDone := make(chan error, 1)
|
|
go func() {
|
|
closeDone <- circuitBreaker.Close()
|
|
}()
|
|
|
|
select {
|
|
case err := <-closeDone:
|
|
t.Fatalf("Close returned before delete completed: %v", err)
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
select {
|
|
case <-store.deleteCtxCanceled:
|
|
t.Fatal("delete context was canceled before delete completed")
|
|
default:
|
|
}
|
|
|
|
close(store.releaseDelete)
|
|
|
|
select {
|
|
case err := <-closeDone:
|
|
require.NoError(t, err)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("expected Close to return after delete completed")
|
|
}
|
|
require.Empty(t, store.messages)
|
|
}
|
|
|
|
func TestCircuitBreakerCloseStopsReplayAfterDeletingPublishedMessages(t *testing.T) {
|
|
publisher := &blockingReplayPublisher{
|
|
firstStarted: make(chan struct{}, 1),
|
|
releaseFirst: make(chan struct{}),
|
|
secondStarted: make(chan struct{}, 1),
|
|
}
|
|
store := &trackingReplayStore{
|
|
messages: []*storage.CircuitBreakerModel{
|
|
{ID: 1, Topic: "test", Data: []byte("first")},
|
|
{ID: 2, Topic: "test", Data: []byte("second")},
|
|
},
|
|
}
|
|
circuitBreaker := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
publisher,
|
|
store,
|
|
5*time.Second,
|
|
)
|
|
go circuitBreaker.Loop(context.Background())
|
|
|
|
select {
|
|
case <-publisher.firstStarted:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("expected first replay publish to start")
|
|
}
|
|
|
|
closeDone := make(chan error, 1)
|
|
go func() {
|
|
closeDone <- circuitBreaker.Close()
|
|
}()
|
|
|
|
select {
|
|
case err := <-closeDone:
|
|
t.Fatalf("Close returned before first replay publish completed: %v", err)
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
close(publisher.releaseFirst)
|
|
|
|
select {
|
|
case err := <-closeDone:
|
|
require.NoError(t, err)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("expected Close to return after deleting published replay")
|
|
}
|
|
select {
|
|
case <-publisher.secondStarted:
|
|
t.Fatal("second replay message should not be published after close")
|
|
default:
|
|
}
|
|
require.Equal(t, []uint64{1}, store.deletedIDs)
|
|
require.Len(t, store.messages, 1)
|
|
require.Equal(t, uint64(2), store.messages[0].ID)
|
|
}
|
|
|
|
func TestCircuitBreakerLoopStopsWhenContextIsCanceled(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
circuitBreaker := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
newMockPublisher(make(chan *testMessages, 10)),
|
|
newMockStore(),
|
|
5*time.Second,
|
|
)
|
|
|
|
go circuitBreaker.Loop(ctx)
|
|
require.Eventually(t, circuitBreaker.hasLoopStarted, time.Second, 10*time.Millisecond)
|
|
|
|
cancel()
|
|
|
|
select {
|
|
case <-circuitBreaker.stopped:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("expected loop to stop after context cancellation")
|
|
}
|
|
|
|
require.NoError(t, circuitBreaker.Close())
|
|
}
|
|
|
|
func TestCircuitBreakerCatchUpExtractsStoredTraceContext(t *testing.T) {
|
|
originalPropagator := otel.GetTextMapPropagator()
|
|
otel.SetTextMapPropagator(propagation.TraceContext{})
|
|
t.Cleanup(func() {
|
|
otel.SetTextMapPropagator(originalPropagator)
|
|
})
|
|
|
|
messages := make(chan *testMessages, 10)
|
|
store := newMockStore()
|
|
require.NoError(t, store.Insert(context.Background(), "test", []byte("test"), map[string]string{
|
|
otelContextKey: `{"traceparent":"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}`,
|
|
}))
|
|
|
|
circuitBreaker := NewCircuitBreaker(
|
|
logging.Testing(),
|
|
newMockPublisher(messages),
|
|
store,
|
|
5*time.Second,
|
|
)
|
|
go circuitBreaker.Loop(context.Background())
|
|
defer func() {
|
|
require.NoError(t, circuitBreaker.Close())
|
|
}()
|
|
|
|
var received *testMessages
|
|
select {
|
|
case received = <-messages:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("expected replayed message")
|
|
}
|
|
|
|
spanContext := trace.SpanContextFromContext(received.msg.Context())
|
|
require.True(t, spanContext.IsValid())
|
|
require.True(t, spanContext.IsRemote())
|
|
require.Equal(t, "4bf92f3577b34da6a3ce929d0e0e4736", spanContext.TraceID().String())
|
|
require.Equal(t, "00f067aa0ba902b7", spanContext.SpanID().String())
|
|
}
|
|
|
|
func requireCloseWithin(t *testing.T, circuitBreaker *CircuitBreaker) {
|
|
t.Helper()
|
|
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- circuitBreaker.Close()
|
|
}()
|
|
|
|
select {
|
|
case err := <-done:
|
|
require.NoError(t, err)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("expected circuit breaker close to return")
|
|
}
|
|
}
|
|
|
|
type blockingListStore struct {
|
|
listStarted chan struct{}
|
|
}
|
|
|
|
func (s *blockingListStore) Insert(ctx context.Context, topic string, data []byte, metadata map[string]string) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *blockingListStore) List(ctx context.Context) ([]*storage.CircuitBreakerModel, error) {
|
|
select {
|
|
case s.listStarted <- struct{}{}:
|
|
default:
|
|
}
|
|
|
|
<-ctx.Done()
|
|
return nil, ctx.Err()
|
|
}
|
|
|
|
func (s *blockingListStore) Delete(ctx context.Context, ids []uint64) error {
|
|
return nil
|
|
}
|
|
|
|
type blockingInsertStore struct {
|
|
insertStarted chan struct{}
|
|
releaseInsert chan struct{}
|
|
insertCtxCanceled chan struct{}
|
|
inserted bool
|
|
}
|
|
|
|
func (s *blockingInsertStore) Insert(ctx context.Context, topic string, data []byte, metadata map[string]string) error {
|
|
select {
|
|
case s.insertStarted <- struct{}{}:
|
|
default:
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
select {
|
|
case s.insertCtxCanceled <- struct{}{}:
|
|
default:
|
|
}
|
|
return ctx.Err()
|
|
case <-s.releaseInsert:
|
|
s.inserted = true
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (s *blockingInsertStore) List(ctx context.Context) ([]*storage.CircuitBreakerModel, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *blockingInsertStore) Delete(ctx context.Context, ids []uint64) error {
|
|
return nil
|
|
}
|
|
|
|
type blockingDeleteStore struct {
|
|
deleteStarted chan struct{}
|
|
releaseDelete chan struct{}
|
|
deleteCtxCanceled chan struct{}
|
|
messages []*storage.CircuitBreakerModel
|
|
}
|
|
|
|
func (s *blockingDeleteStore) Insert(ctx context.Context, topic string, data []byte, metadata map[string]string) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *blockingDeleteStore) List(ctx context.Context) ([]*storage.CircuitBreakerModel, error) {
|
|
result := make([]*storage.CircuitBreakerModel, len(s.messages))
|
|
copy(result, s.messages)
|
|
return result, nil
|
|
}
|
|
|
|
func (s *blockingDeleteStore) Delete(ctx context.Context, ids []uint64) error {
|
|
select {
|
|
case s.deleteStarted <- struct{}{}:
|
|
default:
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
select {
|
|
case s.deleteCtxCanceled <- struct{}{}:
|
|
default:
|
|
}
|
|
return ctx.Err()
|
|
case <-s.releaseDelete:
|
|
s.messages = nil
|
|
return nil
|
|
}
|
|
}
|
|
|
|
type blockingPublishPublisher struct {
|
|
publishStarted chan struct{}
|
|
releasePublish chan struct{}
|
|
}
|
|
|
|
func (p *blockingPublishPublisher) Publish(topic string, messages ...*message.Message) error {
|
|
select {
|
|
case p.publishStarted <- struct{}{}:
|
|
default:
|
|
}
|
|
<-p.releasePublish
|
|
return nil
|
|
}
|
|
|
|
func (p *blockingPublishPublisher) Close() error {
|
|
return nil
|
|
}
|
|
|
|
type blockingReplayPublisher struct {
|
|
calls atomic.Int32
|
|
firstStarted chan struct{}
|
|
releaseFirst chan struct{}
|
|
secondStarted chan struct{}
|
|
}
|
|
|
|
func (p *blockingReplayPublisher) Publish(topic string, messages ...*message.Message) error {
|
|
if p.calls.Add(1) == 1 {
|
|
select {
|
|
case p.firstStarted <- struct{}{}:
|
|
default:
|
|
}
|
|
<-p.releaseFirst
|
|
return nil
|
|
}
|
|
|
|
select {
|
|
case p.secondStarted <- struct{}{}:
|
|
default:
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *blockingReplayPublisher) Close() error {
|
|
return nil
|
|
}
|
|
|
|
type trackingReplayStore struct {
|
|
messages []*storage.CircuitBreakerModel
|
|
deletedIDs []uint64
|
|
}
|
|
|
|
func (s *trackingReplayStore) Insert(ctx context.Context, topic string, data []byte, metadata map[string]string) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *trackingReplayStore) List(ctx context.Context) ([]*storage.CircuitBreakerModel, error) {
|
|
result := make([]*storage.CircuitBreakerModel, len(s.messages))
|
|
copy(result, s.messages)
|
|
return result, nil
|
|
}
|
|
|
|
func (s *trackingReplayStore) Delete(ctx context.Context, ids []uint64) error {
|
|
s.deletedIDs = append(s.deletedIDs, ids...)
|
|
|
|
remaining := s.messages[:0]
|
|
for _, msg := range s.messages {
|
|
deleted := false
|
|
for _, id := range ids {
|
|
if msg.ID == id {
|
|
deleted = true
|
|
break
|
|
}
|
|
}
|
|
if !deleted {
|
|
remaining = append(remaining, msg)
|
|
}
|
|
}
|
|
s.messages = remaining
|
|
return nil
|
|
}
|