github.com/asynkron/protoactor-go@v0.0.0-20240308120642-ef91a6abee75/cluster/pubsub_producer_test.go (about)

     1  package cluster
     2  
     3  import (
     4  	"context"
     5  	"log/slog"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/stretchr/testify/suite"
    10  	"google.golang.org/protobuf/proto"
    11  )
    12  
    13  type PubSubBatchingProducerTestSuite struct {
    14  	suite.Suite
    15  	batchesSent []*PubSubBatch
    16  }
    17  
    18  func (suite *PubSubBatchingProducerTestSuite) SetupTest() {
    19  	suite.batchesSent = make([]*PubSubBatch, 0)
    20  }
    21  
    22  func (suite *PubSubBatchingProducerTestSuite) allSentNumbersShouldEqual(batchesSent []*PubSubBatch, nums ...int) {
    23  	allNumbers := make([]int, 0, len(nums))
    24  	for _, batch := range batchesSent {
    25  		for _, envelope := range batch.Envelopes {
    26  			allNumbers = append(allNumbers, int(envelope.(*TestMessage).Number))
    27  		}
    28  	}
    29  	suite.Assert().ElementsMatch(nums, allNumbers)
    30  }
    31  
    32  func (suite *PubSubBatchingProducerTestSuite) iter(from, to int) []int {
    33  	nums := make([]int, 0, to-from)
    34  	for i := from; i < to; i++ {
    35  		nums = append(nums, i)
    36  	}
    37  	return nums
    38  }
    39  
    40  func (suite *PubSubBatchingProducerTestSuite) record(batch *PubSubBatch) (*PublishResponse, error) {
    41  	b := &PubSubBatch{Envelopes: make([]proto.Message, 0, len(batch.Envelopes))}
    42  	b.Envelopes = append(b.Envelopes, batch.Envelopes...)
    43  
    44  	suite.batchesSent = append(suite.batchesSent, b)
    45  	return &PublishResponse{Status: PublishStatus_Ok}, nil
    46  }
    47  
    48  func (suite *PubSubBatchingProducerTestSuite) wait(_ *PubSubBatch) (*PublishResponse, error) {
    49  	time.Sleep(time.Second * 1)
    50  	return &PublishResponse{Status: PublishStatus_Ok}, nil
    51  }
    52  
    53  func (suite *PubSubBatchingProducerTestSuite) waitThenFail(_ *PubSubBatch) (*PublishResponse, error) {
    54  	time.Sleep(time.Millisecond * 500)
    55  	return &PublishResponse{Status: PublishStatus_Failed}, &testException{}
    56  }
    57  
    58  func (suite *PubSubBatchingProducerTestSuite) fail(_ *PubSubBatch) (*PublishResponse, error) {
    59  	return &PublishResponse{Status: PublishStatus_Failed}, &testException{}
    60  }
    61  
    62  func (suite *PubSubBatchingProducerTestSuite) failTimesThenSucceed(times int) func(*PubSubBatch) (*PublishResponse, error) {
    63  	count := 0
    64  	return func(batch *PubSubBatch) (*PublishResponse, error) {
    65  		count++
    66  		if count <= times {
    67  			return &PublishResponse{Status: PublishStatus_Failed}, &testException{}
    68  		}
    69  		return suite.record(batch)
    70  	}
    71  }
    72  
    73  func (suite *PubSubBatchingProducerTestSuite) timeout() (*PublishResponse, error) {
    74  	return nil, nil
    75  }
    76  
    77  func (suite *PubSubBatchingProducerTestSuite) TestProducerSendsMessagesInBatches() {
    78  	producer := NewBatchingProducer(newMockPublisher(suite.record), "topic", WithBatchingProducerBatchSize(10))
    79  	defer producer.Dispose()
    80  
    81  	infos := make([]*ProduceProcessInfo, 0, 10000)
    82  	for i := 0; i < 10000; i++ {
    83  		info, err := producer.Produce(context.Background(), &TestMessage{Number: int32(i)})
    84  		suite.Assert().NoError(err)
    85  		infos = append(infos, info)
    86  	}
    87  	for _, info := range infos {
    88  		<-info.Finished
    89  		suite.Assert().Nil(info.Err)
    90  	}
    91  
    92  	anyBatchesEnvelopesCountIsGreaterThanOne := false
    93  	for _, batch := range suite.batchesSent {
    94  		if len(batch.Envelopes) > 1 {
    95  			anyBatchesEnvelopesCountIsGreaterThanOne = true
    96  			break
    97  		}
    98  	}
    99  	suite.Assert().True(anyBatchesEnvelopesCountIsGreaterThanOne, "messages should be batched")
   100  
   101  	allBatchesEnvelopeCountAreLessThanBatchSize := true
   102  	for _, batch := range suite.batchesSent {
   103  		if len(batch.Envelopes) > 10 {
   104  			allBatchesEnvelopeCountAreLessThanBatchSize = false
   105  			break
   106  		}
   107  	}
   108  	suite.Assert().True(allBatchesEnvelopeCountAreLessThanBatchSize, "batches should not exceed configured size")
   109  
   110  	suite.allSentNumbersShouldEqual(suite.batchesSent, suite.iter(0, 10000)...)
   111  }
   112  
   113  func (suite *PubSubBatchingProducerTestSuite) TestPublishingThroughStoppedProducerThrows() {
   114  	producer := NewBatchingProducer(newMockPublisher(suite.record), "topic", WithBatchingProducerBatchSize(10))
   115  	producer.Dispose()
   116  
   117  	_, err := producer.Produce(context.Background(), &TestMessage{Number: 1})
   118  	suite.Assert().ErrorIs(err, &InvalidOperationException{Topic: "topic"})
   119  }
   120  
   121  func (suite *PubSubBatchingProducerTestSuite) TestAllPendingTasksCompleteWhenProducerIsStopped() {
   122  	provider := NewBatchingProducer(newMockPublisher(suite.wait), "topic", WithBatchingProducerBatchSize(5))
   123  
   124  	infoList := make([]*ProduceProcessInfo, 0, 100)
   125  	for i := 0; i < 100; i++ {
   126  		info, err := provider.Produce(context.Background(), &TestMessage{Number: int32(i)})
   127  		suite.Assert().NoError(err)
   128  		infoList = append(infoList, info)
   129  	}
   130  
   131  	provider.Dispose()
   132  
   133  	for _, info := range infoList {
   134  		<-info.Finished
   135  		suite.Assert().Nil(info.Err)
   136  	}
   137  }
   138  
   139  func (suite *PubSubBatchingProducerTestSuite) TestAllPendingTasksCompleteWhenProducerFails() {
   140  	producer := NewBatchingProducer(newMockPublisher(suite.waitThenFail), "topic", WithBatchingProducerBatchSize(5))
   141  	defer producer.Dispose()
   142  
   143  	infoList := make([]*ProduceProcessInfo, 0, 100)
   144  	for i := 0; i < 100; i++ {
   145  		info, err := producer.Produce(context.Background(), &TestMessage{Number: int32(i)})
   146  		suite.Assert().NoError(err)
   147  		infoList = append(infoList, info)
   148  	}
   149  
   150  	for _, info := range infoList {
   151  		<-info.Finished
   152  		suite.Assert().Error(info.Err)
   153  	}
   154  }
   155  
   156  func (suite *PubSubBatchingProducerTestSuite) TestPublishingThroughFailedProducerThrows() {
   157  	producer := NewBatchingProducer(newMockPublisher(suite.fail), "topic", WithBatchingProducerBatchSize(10))
   158  	defer producer.Dispose()
   159  
   160  	info, err := producer.Produce(context.Background(), &TestMessage{Number: 1})
   161  	suite.Assert().NoError(err)
   162  	<-info.Finished
   163  	suite.Assert().ErrorIs(info.Err, &testException{})
   164  
   165  	_, err = producer.Produce(context.Background(), &TestMessage{Number: 1})
   166  	suite.Assert().ErrorIs(err, &InvalidOperationException{Topic: "topic"})
   167  }
   168  
   169  func (suite *PubSubBatchingProducerTestSuite) TestThrowsWhenQueueFull() {
   170  	producer := NewBatchingProducer(newMockPublisher(suite.record), "topic", WithBatchingProducerBatchSize(1), WithBatchingProducerMaxQueueSize(10))
   171  	defer producer.Dispose()
   172  
   173  	hasError := false
   174  	for i := 0; i < 20; i++ {
   175  		_, err := producer.Produce(context.Background(), &TestMessage{Number: int32(i)})
   176  		if err != nil {
   177  			hasError = true
   178  			suite.Assert().ErrorIs(err, &ProducerQueueFullException{})
   179  		}
   180  	}
   181  	suite.Assert().True(hasError)
   182  }
   183  
   184  func (suite *PubSubBatchingProducerTestSuite) TestCanCancelPublishingAMessage() {
   185  	producer := NewBatchingProducer(newMockPublisher(suite.record), "topic", WithBatchingProducerBatchSize(1), WithBatchingProducerMaxQueueSize(10))
   186  	defer producer.Dispose()
   187  
   188  	messageWithoutCancellation := &TestMessage{Number: 1}
   189  	t1, err := producer.Produce(context.Background(), messageWithoutCancellation)
   190  	suite.Assert().NoError(err)
   191  
   192  	ctx, cancel := context.WithCancel(context.Background())
   193  	t2, err := producer.Produce(ctx, &TestMessage{Number: 2})
   194  	cancel()
   195  	suite.Assert().NoError(err)
   196  
   197  	<-t1.Finished
   198  	suite.Assert().NoError(t1.Err)
   199  	<-t2.Finished
   200  	suite.Assert().True(t2.IsCancelled())
   201  
   202  	suite.allSentNumbersShouldEqual(suite.batchesSent, 1)
   203  }
   204  
   205  func (suite *PubSubBatchingProducerTestSuite) TestCanRetryOnPublishingError() {
   206  	retries := make([]int, 0, 10)
   207  	producer := NewBatchingProducer(newMockPublisher(suite.failTimesThenSucceed(3)), "topic",
   208  		WithBatchingProducerBatchSize(1),
   209  		WithBatchingProducerOnPublishingError(func(retry int, e error, batch *PubSubBatch) *PublishingErrorDecision {
   210  			retries = append(retries, retry)
   211  			return RetryBatchImmediately
   212  		}))
   213  	defer producer.Dispose()
   214  
   215  	info, err := producer.Produce(context.Background(), &TestMessage{Number: 1})
   216  	suite.Assert().NoError(err)
   217  
   218  	<-info.Finished
   219  	suite.Assert().Equal([]int{1, 2, 3}, retries)
   220  }
   221  
   222  func (suite *PubSubBatchingProducerTestSuite) TestCanSkipBatchOnPublishingError() {
   223  	producer := NewBatchingProducer(newMockPublisher(suite.failTimesThenSucceed(1)), "topic",
   224  		WithBatchingProducerBatchSize(1),
   225  		WithBatchingProducerOnPublishingError(func(retry int, e error, batch *PubSubBatch) *PublishingErrorDecision {
   226  			return FailBatchAndContinue
   227  		}))
   228  	defer producer.Dispose()
   229  
   230  	t1, err := producer.Produce(context.Background(), &TestMessage{Number: 1})
   231  	suite.Assert().NoError(err)
   232  
   233  	t2, err := producer.Produce(context.Background(), &TestMessage{Number: 2})
   234  	suite.Assert().NoError(err)
   235  
   236  	<-t1.Finished
   237  	suite.Assert().ErrorIs(t1.Err, &testException{})
   238  	<-t2.Finished
   239  	suite.Assert().NoError(t2.Err)
   240  }
   241  
   242  func (suite *PubSubBatchingProducerTestSuite) TestCanStopProducerWhenRetryingInfinitely() {
   243  	producer := NewBatchingProducer(newMockPublisher(suite.fail), "topic",
   244  		WithBatchingProducerBatchSize(1),
   245  		WithBatchingProducerOnPublishingError(func(retry int, e error, batch *PubSubBatch) *PublishingErrorDecision {
   246  			return RetryBatchImmediately
   247  		}))
   248  
   249  	t1, err := producer.Produce(context.Background(), &TestMessage{Number: 1})
   250  	suite.Assert().NoError(err)
   251  
   252  	time.Sleep(50 * time.Millisecond)
   253  	producer.Dispose()
   254  	suite.Assert().True(t1.IsCancelled())
   255  }
   256  
   257  func (suite *PubSubBatchingProducerTestSuite) TestIfMessageIsCancelledMeanwhileRetryingItIsNotPublished() {
   258  	publisher := newOptionalFailureMockPublisher(true)
   259  	producer := NewBatchingProducer(publisher, "topic",
   260  		WithBatchingProducerBatchSize(1),
   261  		WithBatchingProducerOnPublishingError(func(retry int, e error, batch *PubSubBatch) *PublishingErrorDecision {
   262  			return RetryBatchImmediately
   263  		}))
   264  	defer producer.Dispose()
   265  	ctx, cancel := context.WithCancel(context.Background())
   266  	t1, err := producer.Produce(ctx, &TestMessage{Number: 1})
   267  	suite.Assert().NoError(err)
   268  
   269  	// give it a moment to spin
   270  	time.Sleep(50 * time.Millisecond)
   271  
   272  	// cancel the message publish
   273  	cancel()
   274  	<-t1.Finished
   275  	suite.Assert().True(t1.IsCancelled())
   276  
   277  	suite.Assert().Len(publisher.sentBatches, 0)
   278  	publisher.shouldFail = false
   279  	t2, err := producer.Produce(context.Background(), &TestMessage{Number: 2})
   280  	suite.Assert().NoError(err)
   281  	<-t2.Finished
   282  	suite.Assert().NoError(t2.Err)
   283  
   284  	suite.allSentNumbersShouldEqual(publisher.sentBatches, 2)
   285  }
   286  
   287  func (suite *PubSubBatchingProducerTestSuite) TestCanHandlePublishTimeouts() {
   288  }
   289  
   290  // In order for 'go test' to run this suite, we need to create
   291  // a normal test function and pass our suite to suite.Run
   292  func TestPubSubBatchingTestSuite(t *testing.T) {
   293  	suite.Run(t, new(PubSubBatchingProducerTestSuite))
   294  }
   295  
   296  type mockPublisher struct {
   297  	publish func(*PubSubBatch) (*PublishResponse, error)
   298  }
   299  
   300  func (m *mockPublisher) Logger() *slog.Logger {
   301  	return slog.Default()
   302  }
   303  
   304  func newMockPublisher(publish func(*PubSubBatch) (*PublishResponse, error)) *mockPublisher {
   305  	return &mockPublisher{publish: publish}
   306  }
   307  
   308  func (m *mockPublisher) Initialize(_ context.Context, topic string, config PublisherConfig) (*Acknowledge, error) {
   309  	return &Acknowledge{}, nil
   310  }
   311  
   312  func (m *mockPublisher) PublishBatch(_ context.Context, topic string, batch *PubSubBatch, opts ...GrainCallOption) (*PublishResponse, error) {
   313  	return m.publish(batch)
   314  }
   315  
   316  func (m *mockPublisher) Publish(_ context.Context, topic string, message proto.Message, opts ...GrainCallOption) (*PublishResponse, error) {
   317  	return m.publish(&PubSubBatch{Envelopes: []proto.Message{message}})
   318  }
   319  
   320  type optionalFailureMockPublisher struct {
   321  	sentBatches []*PubSubBatch
   322  	shouldFail  bool
   323  }
   324  
   325  func (o *optionalFailureMockPublisher) Logger() *slog.Logger {
   326  	return slog.Default()
   327  }
   328  
   329  // newOptionalFailureMockPublisher creates a mock publisher that can be configured to fail or not
   330  func newOptionalFailureMockPublisher(shouldFail bool) *optionalFailureMockPublisher {
   331  	return &optionalFailureMockPublisher{shouldFail: shouldFail}
   332  }
   333  
   334  func (o *optionalFailureMockPublisher) Initialize(ctx context.Context, topic string, config PublisherConfig) (*Acknowledge, error) {
   335  	return &Acknowledge{}, nil
   336  }
   337  
   338  func (o *optionalFailureMockPublisher) PublishBatch(ctx context.Context, topic string, batch *PubSubBatch, opts ...GrainCallOption) (*PublishResponse, error) {
   339  	if o.shouldFail {
   340  		return nil, &testException{}
   341  	}
   342  	copiedBatch := &PubSubBatch{Envelopes: make([]proto.Message, len(batch.Envelopes))}
   343  	copy(copiedBatch.Envelopes, batch.Envelopes)
   344  
   345  	o.sentBatches = append(o.sentBatches, copiedBatch)
   346  	return &PublishResponse{}, nil
   347  }
   348  
   349  func (o *optionalFailureMockPublisher) Publish(ctx context.Context, topic string, message proto.Message, opts ...GrainCallOption) (*PublishResponse, error) {
   350  	return o.PublishBatch(ctx, topic, &PubSubBatch{Envelopes: []proto.Message{message}}, opts...)
   351  }
   352  
   353  type testException struct{}
   354  
   355  func (t *testException) Error() string {
   356  	return "test exception"
   357  }
   358  
   359  func (t *testException) Is(err error) bool {
   360  	_, ok := err.(*testException)
   361  	return ok
   362  }