go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/common/pubsub/pump_test.go (about)

     1  // Copyright 2021 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package pubsub
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"testing"
    21  	"time"
    22  
    23  	"cloud.google.com/go/pubsub"
    24  
    25  	"go.chromium.org/luci/common/data/stringset"
    26  	"go.chromium.org/luci/common/retry/transient"
    27  	"go.chromium.org/luci/cv/internal/cvtesting"
    28  
    29  	. "github.com/smartystreets/goconvey/convey"
    30  )
    31  
    32  func TestPullingBatchProcessor(t *testing.T) {
    33  	t.Parallel()
    34  	Convey("PBP works ", t, func() {
    35  		ct := cvtesting.Test{MaxDuration: time.Minute}
    36  		ctx, cancel := ct.SetUp(t)
    37  		defer cancel()
    38  
    39  		psSrv, err := NewTestPSServer(ctx)
    40  		defer func() { _ = psSrv.Close() }()
    41  		So(err, ShouldBeNil)
    42  
    43  		processed := make(chan string, 100)
    44  		defer close(processed)
    45  
    46  		// Use this to inject errors.
    47  		batchErrChan := make(chan error, 1)
    48  		defer close(batchErrChan)
    49  
    50  		pump := &PullingBatchProcessor{
    51  			ProcessBatch: makeTestProcessBatch(processed, batchErrChan),
    52  			ProjectID:    psSrv.ProjID,
    53  			SubID:        psSrv.SubID,
    54  		}
    55  
    56  		Convey("non-erroring payload", func() {
    57  			Convey("with concurrent batches", func() {
    58  				pump.Options.ConcurrentBatches = 5
    59  			})
    60  			Convey("without concurrent batches", func() {
    61  				pump.Options.ConcurrentBatches = 1
    62  			})
    63  			So(pump.Validate(), ShouldBeNil)
    64  			nMessages := pump.Options.MaxBatchSize + 1
    65  
    66  			sent := psSrv.PublishTestMessages(nMessages)
    67  			So(sent, ShouldHaveLength, nMessages)
    68  			expected := len(sent)
    69  			received := make(stringset.Set, len(sent))
    70  
    71  			processCtx, cancelProcess := context.WithCancel(ctx)
    72  			done := runProcessAsync(processCtx, psSrv.Client, pump.process)
    73  			for len(received) < expected {
    74  				select {
    75  				case <-ctx.Done():
    76  					panic(fmt.Errorf("expected %d messages, only received %d after too long", expected, len(received)))
    77  				case msg := <-processed:
    78  					received.Add(msg)
    79  				}
    80  			}
    81  
    82  			// There should be no more messages in processed.
    83  			select {
    84  			case msg := <-processed:
    85  				panic(fmt.Sprintf("This test fails because there should be no more messages in this channel, but there was %q", msg))
    86  			default:
    87  			}
    88  			cancelProcess()
    89  			So(<-done, ShouldBeNil)
    90  			So(received, ShouldResemble, sent)
    91  		})
    92  
    93  		Convey("With error", func() {
    94  			// Configure the pump to handle one message at a time s.t. a single
    95  			// batch results in the injected error.
    96  			pump.Options.MaxBatchSize = 1
    97  			pump.Options.ConcurrentBatches = 1
    98  			So(pump.Validate(), ShouldBeNil)
    99  
   100  			var processingError error
   101  			var deliveriesExpected, processedMessagesExpected int
   102  			var processErrorAssertion, sentVSReceivedAssertion func(actual any, expected ...any) string
   103  			Convey("permanent", func() {
   104  				processingError = fmt.Errorf("non-transient error")
   105  				// Each of the messages should be delivered at least once.
   106  				deliveriesExpected = 2
   107  				// The error should be surfaced by .Process()
   108  				processErrorAssertion = ShouldBeError
   109  
   110  				processedMessagesExpected = 1
   111  
   112  				sentVSReceivedAssertion = ShouldNotResemble
   113  			})
   114  			Convey("transient", func() {
   115  				processingError = transient.Tag.Apply(fmt.Errorf("transient error"))
   116  				// The message that failed transiently should be re-delivered at least once.
   117  				deliveriesExpected = 3
   118  				// The error should not be surfaced by .Process()
   119  				processErrorAssertion = ShouldBeNil
   120  
   121  				processedMessagesExpected = 2
   122  
   123  				sentVSReceivedAssertion = ShouldResemble
   124  			})
   125  
   126  			// Publish two messages.
   127  			sent := psSrv.PublishTestMessages(2)
   128  			// Inject error to be returned by the processing of the first
   129  			// batch-of-one-message.
   130  			batchErrChan <- processingError
   131  
   132  			// Run the pump.
   133  			processCtx, cancelProcess := context.WithCancel(ctx)
   134  			done := runProcessAsync(processCtx, psSrv.Client, pump.process)
   135  
   136  			// Receive exactly as many distinct messages as expected.
   137  			received := make(stringset.Set, processedMessagesExpected)
   138  			for len(received) < processedMessagesExpected {
   139  				select {
   140  				case <-ctx.Done():
   141  					panic(fmt.Errorf("expected %d messages, only received %d after too long", processedMessagesExpected, len(received)))
   142  				case msg := <-processed:
   143  					received.Add(msg)
   144  				}
   145  			}
   146  			cancelProcess()
   147  
   148  			// Ensure there were at least as many message deliveries as expected.
   149  			actualDeliveries := 0
   150  			for _, m := range psSrv.Messages() {
   151  				actualDeliveries += m.Deliveries
   152  			}
   153  			So(actualDeliveries, ShouldBeGreaterThanOrEqualTo, deliveriesExpected)
   154  
   155  			// Compare sent and received messages.
   156  			So(sent, sentVSReceivedAssertion, received)
   157  
   158  			// Check that the pump surfaced (or not) the error, as appropriate.
   159  			So(<-done, processErrorAssertion)
   160  		})
   161  
   162  		Convey("Can enforce runTime", func() {
   163  			// Publish a lot of messages
   164  			sent := psSrv.PublishTestMessages(100)
   165  			received := make(stringset.Set, len(sent))
   166  
   167  			pump.Options.MaxBatchSize = 1
   168  			pump.Options.ConcurrentBatches = 1
   169  			pump.ProcessBatch = func(ctx context.Context, msgs []*pubsub.Message) error {
   170  				// As soon as the first message is received, move the clock
   171  				// past the runTime deadline.
   172  				ct.Clock.Add(pump.Options.ReceiveDuration)
   173  				for _, msg := range msgs {
   174  					received.Add(string(msg.Data))
   175  				}
   176  				return nil
   177  			}
   178  			So(pump.Validate(), ShouldBeNil)
   179  
   180  			done := runProcessAsync(ctx, psSrv.Client, pump.process)
   181  
   182  			select {
   183  			case <-ctx.Done():
   184  				panic("This test failed because .Process() did not respect .runTime")
   185  			case err := <-done:
   186  				So(err, ShouldBeNil)
   187  			}
   188  			So(len(received), ShouldBeLessThan, len(sent))
   189  		})
   190  	})
   191  }
   192  
   193  func runProcessAsync(ctx context.Context, client *pubsub.Client, payload func(context.Context, *pubsub.Client) error) <-chan error {
   194  	done := make(chan error, 1)
   195  	go func() {
   196  		defer close(done)
   197  		done <- payload(ctx, client)
   198  	}()
   199  	return done
   200  }
   201  
   202  // makeTestProcessBatch returns a trivial batch processing function (a closure).
   203  //
   204  // It puts every message in the given output channel, and if an error is
   205  // available at batchErrChan, returns that instead of nil.
   206  func makeTestProcessBatch(processed chan<- string, batchErrChan <-chan error) func(ctx context.Context, msgs []*pubsub.Message) error {
   207  	return func(ctx context.Context, msgs []*pubsub.Message) error {
   208  
   209  		select {
   210  		case err := <-batchErrChan:
   211  			return err
   212  		default:
   213  
   214  		}
   215  		for _, msg := range msgs {
   216  			processed <- string(msg.Data)
   217  		}
   218  		return nil
   219  	}
   220  }