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 }