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  }