github.com/Jeffail/benthos/v3@v3.65.0/internal/impl/generic/buffer_system_window_test.go (about)

     1  package generic
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strconv"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/Jeffail/benthos/v3/public/bloblang"
    13  	"github.com/Jeffail/benthos/v3/public/service"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  func TestSystemWindowBufferConfigs(t *testing.T) {
    19  	tests := []struct {
    20  		config           string
    21  		lintErrContains  string
    22  		buildErrContains string
    23  	}{
    24  		{
    25  			config: `
    26  system_window:
    27    size: 60m
    28  `,
    29  		},
    30  		{
    31  			config: `
    32  system_window: {}
    33  `,
    34  			lintErrContains: "field size is required",
    35  		},
    36  		{
    37  			config: `
    38  system_window:
    39    timestamp_mapping: 'root ='
    40    size: 60m
    41  `,
    42  			lintErrContains: "expected whitespace",
    43  		},
    44  		{
    45  			config: `
    46  system_window:
    47    size: 60m
    48    slide: 5m
    49    offset: 1m
    50    allowed_lateness: 2m
    51  `,
    52  		},
    53  		{
    54  			config: `
    55  system_window:
    56    size: 60m
    57    slide: 120m
    58    offset: 1m
    59    allowed_lateness: 2m
    60  `,
    61  			buildErrContains: "invalid window slide",
    62  		},
    63  		{
    64  			config: `
    65  system_window:
    66    size: 60m
    67    offset: 60m
    68    allowed_lateness: 2m
    69  `,
    70  			buildErrContains: "invalid offset",
    71  		},
    72  		{
    73  			config: `
    74  system_window:
    75    size: 60m
    76    slide: 10m
    77    offset: 10m
    78    allowed_lateness: 2m
    79  `,
    80  			buildErrContains: "invalid offset",
    81  		},
    82  		{
    83  			config: `
    84  system_window:
    85    size: 60m
    86    slide: 10m
    87    allowed_lateness: 200m
    88  `,
    89  			buildErrContains: "invalid allowed_lateness",
    90  		},
    91  	}
    92  
    93  	for i, test := range tests {
    94  		t.Run(strconv.Itoa(i), func(t *testing.T) {
    95  			env := service.NewStreamBuilder()
    96  			require.NoError(t, env.SetLoggerYAML(`level: OFF`))
    97  			err := env.AddConsumerFunc(func(context.Context, *service.Message) error {
    98  				return nil
    99  			})
   100  			require.NoError(t, err)
   101  			_, err = env.AddProducerFunc()
   102  			require.NoError(t, err)
   103  
   104  			err = env.SetBufferYAML(test.config)
   105  			if test.lintErrContains != "" {
   106  				require.Error(t, err)
   107  				assert.Contains(t, err.Error(), test.lintErrContains)
   108  				return
   109  			}
   110  			require.NoError(t, err)
   111  
   112  			strm, err := env.Build()
   113  			require.NoError(t, err)
   114  
   115  			cancelledCtx, done := context.WithCancel(context.Background())
   116  			done()
   117  			err = strm.Run(cancelledCtx)
   118  			if test.buildErrContains != "" {
   119  				require.Error(t, err)
   120  				assert.Contains(t, err.Error(), test.buildErrContains)
   121  				return
   122  			}
   123  			require.EqualError(t, err, "context canceled")
   124  			require.NoError(t, strm.StopWithin(time.Second))
   125  		})
   126  	}
   127  }
   128  
   129  func TestSystemCurrentWindowCalc(t *testing.T) {
   130  	tests := []struct {
   131  		now                            string
   132  		size, slide, offset            time.Duration
   133  		prevStart, prevEnd, start, end string
   134  	}{
   135  		{
   136  			now:       `2006-01-02T15:00:00Z`,
   137  			size:      time.Hour,
   138  			start:     `2006-01-02T14:00:00.000000001Z`,
   139  			end:       `2006-01-02T15:00:00Z`,
   140  			prevStart: `2006-01-02T13:00:00.000000001Z`,
   141  			prevEnd:   `2006-01-02T14:00:00Z`,
   142  		},
   143  		{
   144  			now:       `2006-01-02T15:00:00.000000001Z`,
   145  			size:      time.Hour,
   146  			start:     `2006-01-02T15:00:00.000000001Z`,
   147  			end:       `2006-01-02T16:00:00Z`,
   148  			prevStart: `2006-01-02T14:00:00.000000001Z`,
   149  			prevEnd:   `2006-01-02T15:00:00Z`,
   150  		},
   151  		{
   152  			now:       `2006-01-02T15:04:05.123456789Z`,
   153  			size:      time.Hour,
   154  			start:     `2006-01-02T15:00:00.000000001Z`,
   155  			end:       `2006-01-02T16:00:00Z`,
   156  			prevStart: `2006-01-02T14:00:00.000000001Z`,
   157  			prevEnd:   `2006-01-02T15:00:00Z`,
   158  		},
   159  		{
   160  			now:       `2006-01-02T15:34:05.123456789Z`,
   161  			size:      time.Hour,
   162  			start:     `2006-01-02T15:00:00.000000001Z`,
   163  			end:       `2006-01-02T16:00:00Z`,
   164  			prevStart: `2006-01-02T14:00:00.000000001Z`,
   165  			prevEnd:   `2006-01-02T15:00:00Z`,
   166  		},
   167  		{
   168  			now:       `2006-01-02T00:04:05.123456789Z`,
   169  			size:      time.Hour,
   170  			start:     `2006-01-02T00:00:00.000000001Z`,
   171  			end:       `2006-01-02T01:00:00Z`,
   172  			prevStart: `2006-01-01T23:00:00.000000001Z`,
   173  			prevEnd:   `2006-01-02T00:00:00Z`,
   174  		},
   175  		{
   176  			now:       `2006-01-02T15:04:05.123456789Z`,
   177  			size:      time.Hour,
   178  			slide:     time.Minute * 10,
   179  			start:     `2006-01-02T14:10:00.000000001Z`,
   180  			end:       `2006-01-02T15:10:00Z`,
   181  			prevStart: `2006-01-02T14:00:00.000000001Z`,
   182  			prevEnd:   `2006-01-02T15:00:00Z`,
   183  		},
   184  		{
   185  			now:       `2006-01-02T15:04:05.123456789Z`,
   186  			size:      time.Hour,
   187  			offset:    time.Minute * 30,
   188  			start:     `2006-01-02T14:30:00.000000001Z`,
   189  			end:       `2006-01-02T15:30:00Z`,
   190  			prevStart: `2006-01-02T13:30:00.000000001Z`,
   191  			prevEnd:   `2006-01-02T14:30:00Z`,
   192  		},
   193  		{
   194  			now:       `2006-01-02T15:04:05.123456789Z`,
   195  			size:      time.Hour,
   196  			slide:     time.Minute * 10,
   197  			offset:    time.Minute * 5,
   198  			start:     `2006-01-02T14:05:00.000000001Z`,
   199  			end:       `2006-01-02T15:05:00Z`,
   200  			prevStart: `2006-01-02T13:55:00.000000001Z`,
   201  			prevEnd:   `2006-01-02T14:55:00Z`,
   202  		},
   203  		{
   204  			now:       `2006-01-02T15:09:59.123456789Z`,
   205  			size:      time.Hour,
   206  			slide:     time.Minute * 10,
   207  			offset:    time.Minute * 5,
   208  			start:     `2006-01-02T14:15:00.000000001Z`,
   209  			end:       `2006-01-02T15:15:00Z`,
   210  			prevStart: `2006-01-02T14:05:00.000000001Z`,
   211  			prevEnd:   `2006-01-02T15:05:00Z`,
   212  		},
   213  	}
   214  
   215  	for i, test := range tests {
   216  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   217  			w, err := newSystemWindowBuffer(nil, func() time.Time {
   218  				ts, err := time.Parse(time.RFC3339Nano, test.now)
   219  				require.NoError(t, err)
   220  				return ts.UTC()
   221  			}, test.size, test.slide, test.offset, 0, nil)
   222  			require.NoError(t, err)
   223  
   224  			prevStart, prevEnd, start, end := w.nextSystemWindow()
   225  
   226  			assert.Equal(t, test.start, start.Format(time.RFC3339Nano), "start")
   227  			assert.Equal(t, test.end, end.Format(time.RFC3339Nano), "end")
   228  			assert.Equal(t, test.prevStart, prevStart.Format(time.RFC3339Nano), "prevStart")
   229  			assert.Equal(t, test.prevEnd, prevEnd.Format(time.RFC3339Nano), "prevEnd")
   230  		})
   231  	}
   232  }
   233  
   234  func noopAck(context.Context, error) error {
   235  	return nil
   236  }
   237  
   238  func TestSystemWindowWritePurge(t *testing.T) {
   239  	mapping, err := bloblang.Parse(`root = this.ts`)
   240  	require.NoError(t, err)
   241  
   242  	currentTS := time.Unix(10, 1).UTC()
   243  	w, err := newSystemWindowBuffer(mapping, func() time.Time {
   244  		return currentTS
   245  	}, time.Second, 0, 0, 0, nil)
   246  	require.NoError(t, err)
   247  
   248  	err = w.WriteBatch(context.Background(), service.MessageBatch{
   249  		service.NewMessage([]byte(`{"id":"1","ts":7.999999999}`)),
   250  		service.NewMessage([]byte(`{"id":"2","ts":8.5}`)),
   251  		service.NewMessage([]byte(`{"id":"3","ts":9.5}`)),
   252  		service.NewMessage([]byte(`{"id":"4","ts":10.5}`)),
   253  	}, noopAck)
   254  	require.NoError(t, err)
   255  	assert.Len(t, w.pending, 4)
   256  
   257  	err = w.WriteBatch(context.Background(), service.MessageBatch{
   258  		service.NewMessage([]byte(`{"id":"5","ts":10.6}`)),
   259  		service.NewMessage([]byte(`{"id":"6","ts":10.7}`)),
   260  		service.NewMessage([]byte(`{"id":"7","ts":10.8}`)),
   261  		service.NewMessage([]byte(`{"id":"8","ts":10.9}`)),
   262  	}, noopAck)
   263  	require.NoError(t, err)
   264  	assert.Len(t, w.pending, 6)
   265  }
   266  
   267  func TestSystemWindowCreation(t *testing.T) {
   268  	mapping, err := bloblang.Parse(`root = this.ts`)
   269  	require.NoError(t, err)
   270  
   271  	currentTS := time.Unix(10, 1).UTC()
   272  	w, err := newSystemWindowBuffer(mapping, func() time.Time {
   273  		return currentTS
   274  	}, time.Second, 0, 0, 0, nil)
   275  	require.NoError(t, err)
   276  
   277  	err = w.WriteBatch(context.Background(), service.MessageBatch{
   278  		service.NewMessage([]byte(`{"id":"1","ts":7.999999999}`)),
   279  		service.NewMessage([]byte(`{"id":"2","ts":8.5}`)),
   280  		service.NewMessage([]byte(`{"id":"3","ts":9.5}`)),
   281  		service.NewMessage([]byte(`{"id":"4","ts":10.5}`)),
   282  	}, noopAck)
   283  	require.NoError(t, err)
   284  	assert.Len(t, w.pending, 4)
   285  
   286  	resBatch, _, err := w.ReadBatch(context.Background())
   287  	require.NoError(t, err)
   288  
   289  	require.Len(t, resBatch, 1)
   290  	msgBytes, err := resBatch[0].AsBytes()
   291  	require.NoError(t, err)
   292  	assert.Equal(t, `{"id":"3","ts":9.5}`, string(msgBytes))
   293  
   294  	assert.Len(t, w.pending, 1)
   295  	assert.Equal(t, "1970-01-01T00:00:10Z", w.latestFlushedWindowEnd.Format(time.RFC3339Nano))
   296  
   297  	currentTS = time.Unix(10, 999999100).UTC()
   298  
   299  	resBatch, _, err = w.ReadBatch(context.Background())
   300  	require.NoError(t, err)
   301  
   302  	require.Len(t, resBatch, 1)
   303  	msgBytes, err = resBatch[0].AsBytes()
   304  	require.NoError(t, err)
   305  	assert.Equal(t, `{"id":"4","ts":10.5}`, string(msgBytes))
   306  
   307  	assert.Len(t, w.pending, 0)
   308  	assert.Equal(t, "1970-01-01T00:00:11Z", w.latestFlushedWindowEnd.Format(time.RFC3339Nano))
   309  
   310  	currentTS = time.Unix(11, 999999100).UTC()
   311  
   312  	smallWaitCtx, done := context.WithTimeout(context.Background(), time.Millisecond*50)
   313  	resBatch, _, err = w.ReadBatch(smallWaitCtx)
   314  	done()
   315  	require.Error(t, err)
   316  	assert.Len(t, resBatch, 0)
   317  	assert.Equal(t, "1970-01-01T00:00:12Z", w.latestFlushedWindowEnd.Format(time.RFC3339Nano))
   318  
   319  	err = w.WriteBatch(context.Background(), service.MessageBatch{
   320  		service.NewMessage([]byte(`{"id":"5","ts":8.1}`)),
   321  		service.NewMessage([]byte(`{"id":"6","ts":9.999999999}`)),
   322  		service.NewMessage([]byte(`{"id":"7","ts":10}`)),
   323  		service.NewMessage([]byte(`{"id":"8","ts":11.999999999}`)),
   324  		service.NewMessage([]byte(`{"id":"9","ts":12.1}`)),
   325  		service.NewMessage([]byte(`{"id":"10","ts":13}`)),
   326  	}, noopAck)
   327  	require.NoError(t, err)
   328  	require.Len(t, w.pending, 2)
   329  
   330  	msgBytes, err = w.pending[0].m.AsBytes()
   331  	require.NoError(t, err)
   332  	assert.Equal(t, `{"id":"9","ts":12.1}`, string(msgBytes))
   333  
   334  	msgBytes, err = w.pending[1].m.AsBytes()
   335  	require.NoError(t, err)
   336  	assert.Equal(t, `{"id":"10","ts":13}`, string(msgBytes))
   337  }
   338  
   339  func TestSystemWindowCreationSliding(t *testing.T) {
   340  	mapping, err := bloblang.Parse(`root = this.ts`)
   341  	require.NoError(t, err)
   342  
   343  	currentTS := time.Unix(10, 0).UTC()
   344  	w, err := newSystemWindowBuffer(mapping, func() time.Time {
   345  		return currentTS
   346  	}, time.Second, time.Millisecond*500, 0, 0, nil)
   347  	require.NoError(t, err)
   348  	w.latestFlushedWindowEnd = time.Unix(9, 500_000_000)
   349  
   350  	err = w.WriteBatch(context.Background(), service.MessageBatch{
   351  		service.NewMessage([]byte(`{"id":"1","ts":9.85}`)),
   352  		service.NewMessage([]byte(`{"id":"2","ts":9.9}`)),
   353  		service.NewMessage([]byte(`{"id":"3","ts":10.15}`)),
   354  		service.NewMessage([]byte(`{"id":"4","ts":10.3}`)),
   355  		service.NewMessage([]byte(`{"id":"5","ts":10.5}`)),
   356  		service.NewMessage([]byte(`{"id":"6","ts":10.7}`)),
   357  		service.NewMessage([]byte(`{"id":"7","ts":10.9}`)),
   358  		service.NewMessage([]byte(`{"id":"8","ts":11.1}`)),
   359  		service.NewMessage([]byte(`{"id":"9","ts":11.35}`)),
   360  		service.NewMessage([]byte(`{"id":"10","ts":11.52}`)),
   361  		service.NewMessage([]byte(`{"id":"11","ts":11.8}`)),
   362  	}, noopAck)
   363  	require.NoError(t, err)
   364  	assert.Len(t, w.pending, 11)
   365  
   366  	assertBatchIndex := func(i int, batch service.MessageBatch, exp string) {
   367  		t.Helper()
   368  		require.True(t, len(batch) > i)
   369  		msgBytes, err := batch[i].AsBytes()
   370  		require.NoError(t, err)
   371  		assert.Equal(t, exp, string(msgBytes))
   372  	}
   373  
   374  	resBatch, _, err := w.ReadBatch(context.Background())
   375  	require.NoError(t, err)
   376  
   377  	assert.Len(t, resBatch, 2)
   378  	assertBatchIndex(0, resBatch, `{"id":"1","ts":9.85}`)
   379  	assertBatchIndex(1, resBatch, `{"id":"2","ts":9.9}`)
   380  
   381  	currentTS = time.Unix(10, 500000000).UTC()
   382  	resBatch, _, err = w.ReadBatch(context.Background())
   383  	require.NoError(t, err)
   384  
   385  	assert.Len(t, resBatch, 5)
   386  	assertBatchIndex(0, resBatch, `{"id":"1","ts":9.85}`)
   387  	assertBatchIndex(1, resBatch, `{"id":"2","ts":9.9}`)
   388  	assertBatchIndex(2, resBatch, `{"id":"3","ts":10.15}`)
   389  	assertBatchIndex(3, resBatch, `{"id":"4","ts":10.3}`)
   390  	assertBatchIndex(4, resBatch, `{"id":"5","ts":10.5}`)
   391  
   392  	currentTS = time.Unix(11, 0).UTC()
   393  	resBatch, _, err = w.ReadBatch(context.Background())
   394  	require.NoError(t, err)
   395  
   396  	assert.Len(t, resBatch, 5)
   397  	assertBatchIndex(0, resBatch, `{"id":"3","ts":10.15}`)
   398  	assertBatchIndex(1, resBatch, `{"id":"4","ts":10.3}`)
   399  	assertBatchIndex(2, resBatch, `{"id":"5","ts":10.5}`)
   400  	assertBatchIndex(3, resBatch, `{"id":"6","ts":10.7}`)
   401  	assertBatchIndex(4, resBatch, `{"id":"7","ts":10.9}`)
   402  
   403  	currentTS = time.Unix(11, 500_000_000).UTC()
   404  	resBatch, _, err = w.ReadBatch(context.Background())
   405  	require.NoError(t, err)
   406  
   407  	assert.Len(t, resBatch, 4)
   408  	assertBatchIndex(0, resBatch, `{"id":"6","ts":10.7}`)
   409  	assertBatchIndex(1, resBatch, `{"id":"7","ts":10.9}`)
   410  	assertBatchIndex(2, resBatch, `{"id":"8","ts":11.1}`)
   411  	assertBatchIndex(3, resBatch, `{"id":"9","ts":11.35}`)
   412  
   413  	currentTS = time.Unix(12, 0).UTC()
   414  	resBatch, _, err = w.ReadBatch(context.Background())
   415  	require.NoError(t, err)
   416  
   417  	assert.Len(t, resBatch, 4)
   418  	assertBatchIndex(0, resBatch, `{"id":"8","ts":11.1}`)
   419  	assertBatchIndex(1, resBatch, `{"id":"9","ts":11.35}`)
   420  	assertBatchIndex(2, resBatch, `{"id":"10","ts":11.52}`)
   421  	assertBatchIndex(3, resBatch, `{"id":"11","ts":11.8}`)
   422  }
   423  
   424  func TestSystemWindowAckOneToMany(t *testing.T) {
   425  	mapping, err := bloblang.Parse(`root = this.ts`)
   426  	require.NoError(t, err)
   427  
   428  	currentTS := time.Unix(10, 1).UTC()
   429  	w, err := newSystemWindowBuffer(mapping, func() time.Time {
   430  		return currentTS
   431  	}, time.Second, 0, 0, 0, nil)
   432  	require.NoError(t, err)
   433  
   434  	var ackCalled int
   435  	var ackErr error
   436  
   437  	require.NoError(t, w.WriteBatch(context.Background(), service.MessageBatch{
   438  		service.NewMessage([]byte(`{"id":"1","ts":9.5}`)),
   439  		service.NewMessage([]byte(`{"id":"2","ts":10.5}`)),
   440  		service.NewMessage([]byte(`{"id":"3","ts":11.5}`)),
   441  	}, func(ctx context.Context, err error) error {
   442  		ackCalled++
   443  		if err != nil {
   444  			ackErr = err
   445  		}
   446  		return nil
   447  	}))
   448  
   449  	ackFuncs := []service.AckFunc{}
   450  
   451  	resBatch, aFn, err := w.ReadBatch(context.Background())
   452  	require.NoError(t, err)
   453  	require.Len(t, resBatch, 1)
   454  	msgBytes, err := resBatch[0].AsBytes()
   455  	require.NoError(t, err)
   456  	assert.Equal(t, `{"id":"1","ts":9.5}`, string(msgBytes))
   457  	ackFuncs = append(ackFuncs, aFn)
   458  
   459  	currentTS = time.Unix(11, 0).UTC()
   460  
   461  	resBatch, aFn, err = w.ReadBatch(context.Background())
   462  	require.NoError(t, err)
   463  	require.Len(t, resBatch, 1)
   464  	msgBytes, err = resBatch[0].AsBytes()
   465  	require.NoError(t, err)
   466  	assert.Equal(t, `{"id":"2","ts":10.5}`, string(msgBytes))
   467  	ackFuncs = append(ackFuncs, aFn)
   468  
   469  	currentTS = time.Unix(12, 0).UTC()
   470  
   471  	resBatch, aFn, err = w.ReadBatch(context.Background())
   472  	require.NoError(t, err)
   473  	require.Len(t, resBatch, 1)
   474  	msgBytes, err = resBatch[0].AsBytes()
   475  	require.NoError(t, err)
   476  	assert.Equal(t, `{"id":"3","ts":11.5}`, string(msgBytes))
   477  	ackFuncs = append(ackFuncs, aFn)
   478  
   479  	require.Len(t, ackFuncs, 3)
   480  	assert.Equal(t, 0, ackCalled)
   481  	assert.NoError(t, ackErr)
   482  
   483  	require.NoError(t, ackFuncs[0](context.Background(), nil))
   484  	assert.Equal(t, 0, ackCalled)
   485  	assert.NoError(t, ackErr)
   486  
   487  	require.NoError(t, ackFuncs[1](context.Background(), errors.New("custom error")))
   488  	assert.Equal(t, 0, ackCalled)
   489  	assert.NoError(t, ackErr)
   490  
   491  	require.NoError(t, ackFuncs[2](context.Background(), nil))
   492  	assert.Equal(t, 1, ackCalled)
   493  	assert.EqualError(t, ackErr, "custom error")
   494  }
   495  
   496  func TestSystemWindowAckManyToOne(t *testing.T) {
   497  	mapping, err := bloblang.Parse(`root = this.ts`)
   498  	require.NoError(t, err)
   499  
   500  	currentTS := time.Unix(10, 1).UTC()
   501  	w, err := newSystemWindowBuffer(mapping, func() time.Time {
   502  		return currentTS
   503  	}, time.Second, 0, 0, 0, nil)
   504  	require.NoError(t, err)
   505  
   506  	ackCalls := map[int]error{}
   507  
   508  	require.NoError(t, w.WriteBatch(context.Background(), service.MessageBatch{
   509  		service.NewMessage([]byte(`{"id":"1","ts":9.5}`)),
   510  	}, func(ctx context.Context, err error) error {
   511  		ackCalls[0] = err
   512  		return nil
   513  	}))
   514  
   515  	require.NoError(t, w.WriteBatch(context.Background(), service.MessageBatch{
   516  		service.NewMessage([]byte(`{"id":"2","ts":9.6}`)),
   517  	}, func(ctx context.Context, err error) error {
   518  		ackCalls[1] = err
   519  		return nil
   520  	}))
   521  
   522  	require.NoError(t, w.WriteBatch(context.Background(), service.MessageBatch{
   523  		service.NewMessage([]byte(`{"id":"3","ts":9.7}`)),
   524  	}, func(ctx context.Context, err error) error {
   525  		ackCalls[2] = err
   526  		return nil
   527  	}))
   528  
   529  	resBatch, aFn, err := w.ReadBatch(context.Background())
   530  	require.NoError(t, err)
   531  	require.Len(t, resBatch, 3)
   532  
   533  	msgBytes, err := resBatch[0].AsBytes()
   534  	require.NoError(t, err)
   535  	assert.Equal(t, `{"id":"1","ts":9.5}`, string(msgBytes))
   536  
   537  	msgBytes, err = resBatch[1].AsBytes()
   538  	require.NoError(t, err)
   539  	assert.Equal(t, `{"id":"2","ts":9.6}`, string(msgBytes))
   540  
   541  	msgBytes, err = resBatch[2].AsBytes()
   542  	require.NoError(t, err)
   543  	assert.Equal(t, `{"id":"3","ts":9.7}`, string(msgBytes))
   544  
   545  	assert.Empty(t, ackCalls)
   546  	require.NoError(t, aFn(context.Background(), errors.New("custom error")))
   547  
   548  	assert.Equal(t, map[int]error{
   549  		0: errors.New("custom error"),
   550  		1: errors.New("custom error"),
   551  		2: errors.New("custom error"),
   552  	}, ackCalls)
   553  }
   554  
   555  func TestSystemWindowParallelReadAndWrites(t *testing.T) {
   556  	mapping, err := bloblang.Parse(`root = this.ts`)
   557  	require.NoError(t, err)
   558  
   559  	currentTS := time.Unix(10, 500000000).UTC()
   560  	w, err := newSystemWindowBuffer(mapping, func() time.Time {
   561  		return currentTS
   562  	}, time.Second, 0, 0, 0, nil)
   563  	require.NoError(t, err)
   564  
   565  	var wg sync.WaitGroup
   566  	wg.Add(2)
   567  
   568  	startChan := make(chan struct{})
   569  	go func() {
   570  		defer wg.Done()
   571  		<-startChan
   572  		for i := 0; i < 1000; i++ {
   573  			msg := fmt.Sprintf(`{"id":"%v","ts":10.5}`, i)
   574  			writeErr := w.WriteBatch(context.Background(), service.MessageBatch{
   575  				service.NewMessage([]byte(msg)),
   576  			}, func(ctx context.Context, err error) error {
   577  				return nil
   578  			})
   579  			require.NoError(t, writeErr)
   580  		}
   581  	}()
   582  	go func() {
   583  		defer wg.Done()
   584  		<-startChan
   585  		_, _, readErr := w.ReadBatch(context.Background())
   586  		require.NoError(t, readErr)
   587  	}()
   588  
   589  	close(startChan)
   590  	wg.Wait()
   591  }