github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ccl/changefeedccl/sink_cloudstorage_test.go (about) 1 // Copyright 2019 The Cockroach Authors. 2 // 3 // Licensed as a CockroachDB Enterprise file under the Cockroach Community 4 // License (the "License"); you may not use this file except in compliance with 5 // the License. You may obtain a copy of the License at 6 // 7 // https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt 8 9 package changefeedccl 10 11 import ( 12 "bytes" 13 "compress/gzip" 14 "context" 15 "fmt" 16 "io/ioutil" 17 "math" 18 "os" 19 "path/filepath" 20 "sort" 21 "strings" 22 "testing" 23 24 "github.com/cockroachdb/cockroach/pkg/base" 25 "github.com/cockroachdb/cockroach/pkg/blobs" 26 "github.com/cockroachdb/cockroach/pkg/ccl/changefeedccl/changefeedbase" 27 "github.com/cockroachdb/cockroach/pkg/roachpb" 28 "github.com/cockroachdb/cockroach/pkg/settings/cluster" 29 "github.com/cockroachdb/cockroach/pkg/sql/sqlbase" 30 "github.com/cockroachdb/cockroach/pkg/storage/cloud" 31 "github.com/cockroachdb/cockroach/pkg/testutils" 32 "github.com/cockroachdb/cockroach/pkg/util/hlc" 33 "github.com/cockroachdb/cockroach/pkg/util/leaktest" 34 "github.com/cockroachdb/cockroach/pkg/util/span" 35 "github.com/stretchr/testify/require" 36 ) 37 38 func TestCloudStorageSink(t *testing.T) { 39 defer leaktest.AfterTest(t)() 40 ctx := context.Background() 41 42 dir, dirCleanupFn := testutils.TempDir(t) 43 defer dirCleanupFn() 44 45 gzipDecompress := func(t *testing.T, compressed []byte) []byte { 46 r, err := gzip.NewReader(bytes.NewReader(compressed)) 47 if err != nil { 48 t.Fatal(err) 49 } 50 defer r.Close() 51 decompressed, err := ioutil.ReadAll(r) 52 if err != nil { 53 t.Fatal(err) 54 } 55 return decompressed 56 } 57 58 // slurpDir returns the contents of every file under root (relative to the 59 // temp dir created above), sorted by the name of the file. 60 slurpDir := func(t *testing.T, root string) []string { 61 var files []string 62 walkFn := func(path string, info os.FileInfo, err error) error { 63 if err != nil { 64 return err 65 } 66 if info.IsDir() { 67 return nil 68 } 69 file, err := ioutil.ReadFile(path) 70 if err != nil { 71 return err 72 } 73 if strings.HasSuffix(path, ".gz") { 74 file = gzipDecompress(t, file) 75 } 76 files = append(files, string(file)) 77 return nil 78 } 79 absRoot := filepath.Join(dir, root) 80 require.NoError(t, os.MkdirAll(absRoot, 0755)) 81 require.NoError(t, filepath.Walk(absRoot, walkFn)) 82 return files 83 } 84 85 const unlimitedFileSize = math.MaxInt64 86 var noKey []byte 87 settings := cluster.MakeTestingClusterSettings() 88 settings.ExternalIODir = dir 89 opts := map[string]string{ 90 changefeedbase.OptFormat: string(changefeedbase.OptFormatJSON), 91 changefeedbase.OptEnvelope: string(changefeedbase.OptEnvelopeWrapped), 92 changefeedbase.OptKeyInValue: ``, 93 changefeedbase.OptCompression: ``, // NB: overridden in single-node subtest. 94 } 95 ts := func(i int64) hlc.Timestamp { return hlc.Timestamp{WallTime: i} } 96 e, err := makeJSONEncoder(opts) 97 require.NoError(t, err) 98 99 clientFactory := blobs.TestBlobServiceClient(settings.ExternalIODir) 100 externalStorageFromURI := func(ctx context.Context, uri string) (cloud.ExternalStorage, error) { 101 return cloud.ExternalStorageFromURI(ctx, uri, base.ExternalIODirConfig{}, settings, clientFactory) 102 } 103 104 t.Run(`golden`, func(t *testing.T) { 105 t1 := &sqlbase.TableDescriptor{Name: `t1`} 106 testSpan := roachpb.Span{Key: []byte("a"), EndKey: []byte("b")} 107 sf := span.MakeFrontier(testSpan) 108 timestampOracle := &changeAggregatorLowerBoundOracle{sf: sf} 109 sinkDir := `golden` 110 s, err := makeCloudStorageSink( 111 ctx, `nodelocal://0/`+sinkDir, 1, unlimitedFileSize, 112 settings, opts, timestampOracle, externalStorageFromURI, 113 ) 114 require.NoError(t, err) 115 s.(*cloudStorageSink).sinkID = 7 // Force a deterministic sinkID. 116 117 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`v1`), ts(1))) 118 require.NoError(t, s.Flush(ctx)) 119 120 require.Equal(t, []string{ 121 "v1\n", 122 }, slurpDir(t, sinkDir)) 123 124 require.NoError(t, s.EmitResolvedTimestamp(ctx, e, ts(5))) 125 resolvedFile, err := ioutil.ReadFile(filepath.Join( 126 dir, sinkDir, `1970-01-01`, `197001010000000000000050000000000.RESOLVED`)) 127 require.NoError(t, err) 128 require.Equal(t, `{"resolved":"5.0000000000"}`, string(resolvedFile)) 129 }) 130 t.Run(`single-node`, func(t *testing.T) { 131 before := opts[changefeedbase.OptCompression] 132 // Compression codecs include buffering that interferes with other tests, 133 // e.g. the bucketing test that configures very small flush sizes. 134 defer func() { 135 opts[changefeedbase.OptCompression] = before 136 }() 137 for _, compression := range []string{"", "gzip"} { 138 opts[changefeedbase.OptCompression] = compression 139 t.Run("compress="+compression, func(t *testing.T) { 140 t1 := &sqlbase.TableDescriptor{Name: `t1`} 141 t2 := &sqlbase.TableDescriptor{Name: `t2`} 142 143 testSpan := roachpb.Span{Key: []byte("a"), EndKey: []byte("b")} 144 sf := span.MakeFrontier(testSpan) 145 timestampOracle := &changeAggregatorLowerBoundOracle{sf: sf} 146 dir := `single-node` + compression 147 s, err := makeCloudStorageSink( 148 ctx, `nodelocal://0/`+dir, 1, unlimitedFileSize, 149 settings, opts, timestampOracle, externalStorageFromURI, 150 ) 151 require.NoError(t, err) 152 s.(*cloudStorageSink).sinkID = 7 // Force a deterministic sinkID. 153 154 // Empty flush emits no files. 155 require.NoError(t, s.Flush(ctx)) 156 require.Equal(t, []string(nil), slurpDir(t, dir)) 157 158 // Emitting rows and flushing should write them out in one file per table. Note 159 // the ordering among these two files is non deterministic as either of them could 160 // be flushed first (and thus be assigned fileID 0). 161 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`v1`), ts(1))) 162 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`v2`), ts(1))) 163 require.NoError(t, s.EmitRow(ctx, t2, noKey, []byte(`w1`), ts(3))) 164 require.NoError(t, s.Flush(ctx)) 165 expected := []string{ 166 "v1\nv2\n", 167 "w1\n", 168 } 169 actual := slurpDir(t, dir) 170 sort.Strings(actual) 171 require.Equal(t, expected, actual) 172 173 // Flushing with no new emits writes nothing new. 174 require.NoError(t, s.Flush(ctx)) 175 actual = slurpDir(t, dir) 176 sort.Strings(actual) 177 require.Equal(t, expected, actual) 178 179 // Without a flush, nothing new shows up. 180 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`v3`), ts(3))) 181 actual = slurpDir(t, dir) 182 sort.Strings(actual) 183 require.Equal(t, expected, actual) 184 185 // Note that since we haven't forwarded `testSpan` yet, all files initiated until 186 // this point must have the same `frontier` timestamp. Since fileID increases 187 // monotonically, the last file emitted should be ordered as such. 188 require.NoError(t, s.Flush(ctx)) 189 require.Equal(t, []string{ 190 "v3\n", 191 }, slurpDir(t, dir)[2:]) 192 193 // Data from different versions of a table is put in different files, so that we 194 // can guarantee that all rows in any given file have the same schema. 195 // We also advance `testSpan` and `Flush` to make sure these new rows are read 196 // after the rows emitted above. 197 require.True(t, sf.Forward(testSpan, ts(4))) 198 require.NoError(t, s.Flush(ctx)) 199 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`v4`), ts(4))) 200 t1.Version = 2 201 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`v5`), ts(5))) 202 require.NoError(t, s.Flush(ctx)) 203 expected = []string{ 204 "v4\n", 205 "v5\n", 206 } 207 actual = slurpDir(t, dir) 208 actual = actual[len(actual)-2:] 209 sort.Strings(actual) 210 require.Equal(t, expected, actual) 211 }) 212 } 213 }) 214 215 t.Run(`multi-node`, func(t *testing.T) { 216 t1 := &sqlbase.TableDescriptor{Name: `t1`} 217 218 testSpan := roachpb.Span{Key: []byte("a"), EndKey: []byte("b")} 219 sf := span.MakeFrontier(testSpan) 220 timestampOracle := &changeAggregatorLowerBoundOracle{sf: sf} 221 dir := `multi-node` 222 s1, err := makeCloudStorageSink( 223 ctx, `nodelocal://0/`+dir, 1, unlimitedFileSize, 224 settings, opts, timestampOracle, externalStorageFromURI, 225 ) 226 require.NoError(t, err) 227 s2, err := makeCloudStorageSink( 228 ctx, `nodelocal://0/`+dir, 2, unlimitedFileSize, 229 settings, opts, timestampOracle, externalStorageFromURI, 230 ) 231 require.NoError(t, err) 232 // Hack into the sinks to pretend each is the first sink created on two 233 // different nodes, which is the worst case for them conflicting. 234 s1.(*cloudStorageSink).sinkID = 0 235 s2.(*cloudStorageSink).sinkID = 0 236 237 // Force deterministic job session IDs to force ordering of output files. 238 s1.(*cloudStorageSink).jobSessionID = "a" 239 s2.(*cloudStorageSink).jobSessionID = "b" 240 241 // Each node writes some data at the same timestamp. When this data is 242 // written out, the files have different names and don't conflict because 243 // the sinks have different job session IDs. 244 require.NoError(t, s1.EmitRow(ctx, t1, noKey, []byte(`v1`), ts(1))) 245 require.NoError(t, s2.EmitRow(ctx, t1, noKey, []byte(`w1`), ts(1))) 246 require.NoError(t, s1.Flush(ctx)) 247 require.NoError(t, s2.Flush(ctx)) 248 require.Equal(t, []string{ 249 "v1\n", 250 "w1\n", 251 }, slurpDir(t, dir)) 252 253 // If a node restarts then the entire distsql flow has to restart. If 254 // this happens before checkpointing, some data is written again but 255 // this is unavoidable. 256 s1R, err := makeCloudStorageSink( 257 ctx, `nodelocal://0/`+dir, 1, unlimitedFileSize, 258 settings, opts, timestampOracle, externalStorageFromURI, 259 ) 260 require.NoError(t, err) 261 s2R, err := makeCloudStorageSink( 262 ctx, `nodelocal://0/`+dir, 2, unlimitedFileSize, 263 settings, opts, timestampOracle, externalStorageFromURI, 264 ) 265 require.NoError(t, err) 266 // Nodes restart. s1 gets the same sink id it had last time but s2 267 // doesn't. 268 s1R.(*cloudStorageSink).sinkID = 0 269 s2R.(*cloudStorageSink).sinkID = 7 270 271 // Again, force deterministic job session IDs to force ordering of output 272 // files. Note that making s1R have the same job session ID as s1 should make 273 // its output overwrite s1's output. 274 s1R.(*cloudStorageSink).jobSessionID = "a" 275 s2R.(*cloudStorageSink).jobSessionID = "b" 276 // Each resends the data it did before. 277 require.NoError(t, s1R.EmitRow(ctx, t1, noKey, []byte(`v1`), ts(1))) 278 require.NoError(t, s2R.EmitRow(ctx, t1, noKey, []byte(`w1`), ts(1))) 279 require.NoError(t, s1R.Flush(ctx)) 280 require.NoError(t, s2R.Flush(ctx)) 281 // s1 data ends up being overwritten, s2 data ends up duplicated. 282 require.Equal(t, []string{ 283 "v1\n", 284 "w1\n", 285 "w1\n", 286 }, slurpDir(t, dir)) 287 }) 288 289 // The jobs system can't always clean up perfectly after itself and so there 290 // are situations where it will leave a zombie job coordinator for a bit. 291 // Make sure the zombie isn't writing the same filenames so that it can't 292 // overwrite good data with partial data. 293 // 294 // This test is also sufficient for verifying the behavior of a multi-node 295 // changefeed using this sink. Ditto job restarts. 296 t.Run(`zombie`, func(t *testing.T) { 297 t1 := &sqlbase.TableDescriptor{Name: `t1`} 298 testSpan := roachpb.Span{Key: []byte("a"), EndKey: []byte("b")} 299 sf := span.MakeFrontier(testSpan) 300 timestampOracle := &changeAggregatorLowerBoundOracle{sf: sf} 301 dir := `zombie` 302 s1, err := makeCloudStorageSink( 303 ctx, `nodelocal://0/`+dir, 1, unlimitedFileSize, 304 settings, opts, timestampOracle, externalStorageFromURI, 305 ) 306 require.NoError(t, err) 307 s1.(*cloudStorageSink).sinkID = 7 // Force a deterministic sinkID. 308 s1.(*cloudStorageSink).jobSessionID = "a" // Force deterministic job session ID. 309 s2, err := makeCloudStorageSink( 310 ctx, `nodelocal://0/`+dir, 1, unlimitedFileSize, 311 settings, opts, timestampOracle, externalStorageFromURI, 312 ) 313 require.NoError(t, err) 314 s2.(*cloudStorageSink).sinkID = 8 // Force a deterministic sinkID. 315 s2.(*cloudStorageSink).jobSessionID = "b" // Force deterministic job session ID. 316 317 // Good job writes 318 require.NoError(t, s1.EmitRow(ctx, t1, noKey, []byte(`v1`), ts(1))) 319 require.NoError(t, s1.EmitRow(ctx, t1, noKey, []byte(`v2`), ts(2))) 320 require.NoError(t, s1.Flush(ctx)) 321 322 // Zombie job writes partial duplicate data 323 require.NoError(t, s2.EmitRow(ctx, t1, noKey, []byte(`v1`), ts(1))) 324 require.NoError(t, s2.Flush(ctx)) 325 326 // Good job continues. There are duplicates in the data but nothing was 327 // lost. 328 require.NoError(t, s1.EmitRow(ctx, t1, noKey, []byte(`v3`), ts(3))) 329 require.NoError(t, s1.Flush(ctx)) 330 require.Equal(t, []string{ 331 "v1\nv2\n", 332 "v3\n", 333 "v1\n", 334 }, slurpDir(t, dir)) 335 }) 336 337 t.Run(`bucketing`, func(t *testing.T) { 338 t1 := &sqlbase.TableDescriptor{Name: `t1`} 339 testSpan := roachpb.Span{Key: []byte("a"), EndKey: []byte("b")} 340 sf := span.MakeFrontier(testSpan) 341 timestampOracle := &changeAggregatorLowerBoundOracle{sf: sf} 342 dir := `bucketing` 343 const targetMaxFileSize = 6 344 s, err := makeCloudStorageSink( 345 ctx, `nodelocal://0/`+dir, 1, targetMaxFileSize, 346 settings, opts, timestampOracle, externalStorageFromURI, 347 ) 348 require.NoError(t, err) 349 s.(*cloudStorageSink).sinkID = 7 // Force a deterministic sinkID. 350 351 // Writing more than the max file size chunks the file up and flushes it 352 // out as necessary. 353 for i := int64(1); i <= 5; i++ { 354 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(fmt.Sprintf(`v%d`, i)), ts(i))) 355 } 356 require.Equal(t, []string{ 357 "v1\nv2\nv3\n", 358 }, slurpDir(t, dir)) 359 360 // Flush then writes the rest. 361 require.NoError(t, s.Flush(ctx)) 362 require.Equal(t, []string{ 363 "v1\nv2\nv3\n", 364 "v4\nv5\n", 365 }, slurpDir(t, dir)) 366 367 // Forward the SpanFrontier here and trigger an empty flush to update 368 // the sink's `inclusiveLowerBoundTs` 369 sf.Forward(testSpan, ts(5)) 370 require.NoError(t, s.Flush(ctx)) 371 372 // Some more data is written. Some of it flushed out because of the max 373 // file size. 374 for i := int64(6); i < 10; i++ { 375 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(fmt.Sprintf(`v%d`, i)), ts(i))) 376 } 377 require.Equal(t, []string{ 378 "v1\nv2\nv3\n", 379 "v4\nv5\n", 380 "v6\nv7\nv8\n", 381 }, slurpDir(t, dir)) 382 383 // Resolved timestamps are periodically written. This happens 384 // asynchronously from a different node and can be given an earlier 385 // timestamp than what's been handed to EmitRow, but the system 386 // guarantees that Flush been called (and returned without error) with a 387 // ts at >= this one before this call starts. 388 // 389 // The resolved timestamp file should precede the data files that were 390 // started after the SpanFrontier was forwarded to ts(5). 391 require.NoError(t, s.EmitResolvedTimestamp(ctx, e, ts(5))) 392 require.Equal(t, []string{ 393 "v1\nv2\nv3\n", 394 "v4\nv5\n", 395 `{"resolved":"5.0000000000"}`, 396 "v6\nv7\nv8\n", 397 }, slurpDir(t, dir)) 398 399 // Flush then writes the rest. Since we use the time of the EmitRow 400 // or EmitResolvedTimestamp calls to order files, the resolved timestamp 401 // file should precede the last couple files since they started buffering 402 // after the SpanFrontier was forwarded to ts(5). 403 require.NoError(t, s.Flush(ctx)) 404 require.Equal(t, []string{ 405 "v1\nv2\nv3\n", 406 "v4\nv5\n", 407 `{"resolved":"5.0000000000"}`, 408 "v6\nv7\nv8\n", 409 "v9\n", 410 }, slurpDir(t, dir)) 411 412 // A resolved timestamp emitted with ts > 5 should follow everything 413 // emitted thus far. 414 require.NoError(t, s.EmitResolvedTimestamp(ctx, e, ts(6))) 415 require.Equal(t, []string{ 416 "v1\nv2\nv3\n", 417 "v4\nv5\n", 418 `{"resolved":"5.0000000000"}`, 419 "v6\nv7\nv8\n", 420 "v9\n", 421 `{"resolved":"6.0000000000"}`, 422 }, slurpDir(t, dir)) 423 }) 424 425 t.Run(`file-ordering`, func(t *testing.T) { 426 t1 := &sqlbase.TableDescriptor{Name: `t1`} 427 testSpan := roachpb.Span{Key: []byte("a"), EndKey: []byte("b")} 428 sf := span.MakeFrontier(testSpan) 429 timestampOracle := &changeAggregatorLowerBoundOracle{sf: sf} 430 dir := `file-ordering` 431 s, err := makeCloudStorageSink( 432 ctx, `nodelocal://0/`+dir, 1, unlimitedFileSize, 433 settings, opts, timestampOracle, externalStorageFromURI, 434 ) 435 436 require.NoError(t, err) 437 s.(*cloudStorageSink).sinkID = 7 // Force a deterministic sinkID. 438 439 // Simulate initial scan, which emits data at a timestamp, then an equal 440 // resolved timestamp. 441 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`is1`), ts(1))) 442 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`is2`), ts(1))) 443 require.NoError(t, s.Flush(ctx)) 444 require.NoError(t, s.EmitResolvedTimestamp(ctx, e, ts(1))) 445 446 // Test some edge cases. 447 448 // Forward the testSpan and trigger an empty `Flush` to have new rows 449 // be after the resolved timestamp emitted above. 450 require.True(t, sf.Forward(testSpan, ts(2))) 451 require.NoError(t, s.Flush(ctx)) 452 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`e2`), ts(2))) 453 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`e3prev`), ts(3).Prev())) 454 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`e3`), ts(3))) 455 require.True(t, sf.Forward(testSpan, ts(3))) 456 require.NoError(t, s.Flush(ctx)) 457 require.NoError(t, s.EmitResolvedTimestamp(ctx, e, ts(3))) 458 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`e3next`), ts(3).Next())) 459 require.NoError(t, s.Flush(ctx)) 460 require.NoError(t, s.EmitResolvedTimestamp(ctx, e, ts(4))) 461 462 require.Equal(t, []string{ 463 "is1\nis2\n", 464 `{"resolved":"1.0000000000"}`, 465 "e2\ne3prev\ne3\n", 466 `{"resolved":"3.0000000000"}`, 467 "e3next\n", 468 `{"resolved":"4.0000000000"}`, 469 }, slurpDir(t, dir)) 470 471 // Test that files with timestamp lower than the least resolved timestamp 472 // as of file creation time are ignored. 473 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`noemit`), ts(1).Next())) 474 require.Equal(t, []string{ 475 "is1\nis2\n", 476 `{"resolved":"1.0000000000"}`, 477 "e2\ne3prev\ne3\n", 478 `{"resolved":"3.0000000000"}`, 479 "e3next\n", 480 `{"resolved":"4.0000000000"}`, 481 }, slurpDir(t, dir)) 482 }) 483 484 t.Run(`ordering-among-schema-versions`, func(t *testing.T) { 485 t1 := &sqlbase.TableDescriptor{Name: `t1`} 486 testSpan := roachpb.Span{Key: []byte("a"), EndKey: []byte("b")} 487 sf := span.MakeFrontier(testSpan) 488 timestampOracle := &changeAggregatorLowerBoundOracle{sf: sf} 489 dir := `ordering-among-schema-versions` 490 var targetMaxFileSize int64 = 10 491 s, err := makeCloudStorageSink(ctx, `nodelocal://0/`+dir, 1, targetMaxFileSize, settings, 492 opts, timestampOracle, externalStorageFromURI) 493 require.NoError(t, err) 494 495 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`v1`), ts(1))) 496 t1.Version = 1 497 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`v3`), ts(1))) 498 // Make the first file exceed its file size threshold. This should trigger a flush 499 // for the first file but not the second one. 500 t1.Version = 0 501 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`trigger-flush-v1`), ts(1))) 502 require.Equal(t, []string{ 503 "v1\ntrigger-flush-v1\n", 504 }, slurpDir(t, dir)) 505 506 // Now make the file with the newer schema exceed its file size threshold and ensure 507 // that the file with the older schema is flushed (and ordered) before. 508 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`v2`), ts(1))) 509 t1.Version = 1 510 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`trigger-flush-v3`), ts(1))) 511 require.Equal(t, []string{ 512 "v1\ntrigger-flush-v1\n", 513 "v2\n", 514 "v3\ntrigger-flush-v3\n", 515 }, slurpDir(t, dir)) 516 517 // Calling `Flush()` on the sink should emit files in the order of their schema IDs. 518 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`w1`), ts(1))) 519 t1.Version = 0 520 require.NoError(t, s.EmitRow(ctx, t1, noKey, []byte(`x1`), ts(1))) 521 require.NoError(t, s.Flush(ctx)) 522 require.Equal(t, []string{ 523 "v1\ntrigger-flush-v1\n", 524 "v2\n", 525 "v3\ntrigger-flush-v3\n", 526 "x1\n", 527 "w1\n", 528 }, slurpDir(t, dir)) 529 }) 530 }