github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/colcontainer/partitionedqueue_test.go (about)

     1  // Copyright 2020 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package colcontainer_test
    12  
    13  import (
    14  	"context"
    15  	"fmt"
    16  	"testing"
    17  
    18  	"github.com/cockroachdb/cockroach/pkg/col/coldata"
    19  	"github.com/cockroachdb/cockroach/pkg/sql/colcontainer"
    20  	"github.com/cockroachdb/cockroach/pkg/sql/colexecbase"
    21  	"github.com/cockroachdb/cockroach/pkg/sql/types"
    22  	"github.com/cockroachdb/cockroach/pkg/storage/fs"
    23  	"github.com/cockroachdb/cockroach/pkg/testutils/colcontainerutils"
    24  	"github.com/cockroachdb/cockroach/pkg/util/leaktest"
    25  	"github.com/cockroachdb/cockroach/pkg/util/randutil"
    26  	"github.com/marusama/semaphore"
    27  	"github.com/stretchr/testify/require"
    28  )
    29  
    30  type fdCountingFSFile struct {
    31  	fs.File
    32  	onCloseCb func()
    33  }
    34  
    35  func (f *fdCountingFSFile) Close() error {
    36  	if err := f.File.Close(); err != nil {
    37  		return err
    38  	}
    39  	f.onCloseCb()
    40  	return nil
    41  }
    42  
    43  type fdCountingFS struct {
    44  	fs.FS
    45  	writeFDs int
    46  	readFDs  int
    47  }
    48  
    49  // assertOpenFDs is a helper function that checks that sem has the correct count
    50  // of open file descriptors, and the fs' open file descriptors match up with the
    51  // given expected number.
    52  func (f *fdCountingFS) assertOpenFDs(
    53  	t *testing.T, sem semaphore.Semaphore, expectedWriteFDs, expectedReadFDs int,
    54  ) {
    55  	t.Helper()
    56  	require.Equal(t, expectedWriteFDs+expectedReadFDs, sem.GetCount())
    57  	require.Equal(t, expectedWriteFDs, f.writeFDs)
    58  	require.Equal(t, expectedReadFDs, f.readFDs)
    59  }
    60  
    61  func (f *fdCountingFS) Create(name string) (fs.File, error) {
    62  	file, err := f.FS.Create(name)
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  	f.writeFDs++
    67  	return &fdCountingFSFile{File: file, onCloseCb: func() { f.writeFDs-- }}, nil
    68  }
    69  
    70  func (f *fdCountingFS) CreateWithSync(name string, bytesPerSync int) (fs.File, error) {
    71  	file, err := f.FS.CreateWithSync(name, bytesPerSync)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  	f.writeFDs++
    76  	return &fdCountingFSFile{File: file, onCloseCb: func() { f.writeFDs-- }}, nil
    77  }
    78  
    79  func (f *fdCountingFS) Open(name string) (fs.File, error) {
    80  	file, err := f.FS.Open(name)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	f.readFDs++
    85  	return &fdCountingFSFile{File: file, onCloseCb: func() { f.readFDs-- }}, nil
    86  }
    87  
    88  // TestPartitionedDiskQueue tests interesting scenarios that are different from
    89  // the simulated external algorithms below and don't make sense to add to that
    90  // test.
    91  func TestPartitionedDiskQueue(t *testing.T) {
    92  	defer leaktest.AfterTest(t)()
    93  
    94  	var (
    95  		ctx   = context.Background()
    96  		typs  = []*types.T{types.Int}
    97  		batch = testAllocator.NewMemBatch(typs)
    98  		sem   = &colexecbase.TestingSemaphore{}
    99  	)
   100  	batch.SetLength(coldata.BatchSize())
   101  
   102  	queueCfg, cleanup := colcontainerutils.NewTestingDiskQueueCfg(t, true /* inMem */)
   103  	defer cleanup()
   104  
   105  	countingFS := &fdCountingFS{FS: queueCfg.FS}
   106  	queueCfg.FS = countingFS
   107  
   108  	t.Run("ReopenReadPartition", func(t *testing.T) {
   109  		p := colcontainer.NewPartitionedDiskQueue(typs, queueCfg, sem, colcontainer.PartitionerStrategyDefault, testDiskAcc)
   110  
   111  		countingFS.assertOpenFDs(t, sem, 0, 0)
   112  		require.NoError(t, p.Enqueue(ctx, 0, batch))
   113  		countingFS.assertOpenFDs(t, sem, 1, 0)
   114  		require.NoError(t, p.Enqueue(ctx, 0, batch))
   115  		countingFS.assertOpenFDs(t, sem, 1, 0)
   116  		require.NoError(t, p.Dequeue(ctx, 0, batch))
   117  		require.True(t, batch.Length() != 0)
   118  		countingFS.assertOpenFDs(t, sem, 0, 1)
   119  		// There is still one batch to dequeue. Close all read files.
   120  		require.NoError(t, p.CloseAllOpenReadFileDescriptors())
   121  		countingFS.assertOpenFDs(t, sem, 0, 0)
   122  		require.NoError(t, p.Dequeue(ctx, 0, batch))
   123  		require.True(t, batch.Length() != 0)
   124  		// Here we do a manual check, since this is the case in which the semaphore
   125  		// will report an extra file open (the read happens from the in-memory
   126  		// buffer, not disk).
   127  		require.Equal(t, 1, sem.GetCount())
   128  		require.Equal(t, 0, countingFS.writeFDs+countingFS.readFDs)
   129  
   130  		// However, now the partition should be empty if Dequeued from again.
   131  		require.NoError(t, p.Dequeue(ctx, 0, batch))
   132  		require.True(t, batch.Length() == 0)
   133  		// And the file descriptor should be automatically closed.
   134  		countingFS.assertOpenFDs(t, sem, 0, 0)
   135  
   136  		require.NoError(t, p.Close(ctx))
   137  		countingFS.assertOpenFDs(t, sem, 0, 0)
   138  	})
   139  
   140  }
   141  
   142  func TestPartitionedDiskQueueSimulatedExternal(t *testing.T) {
   143  	defer leaktest.AfterTest(t)()
   144  
   145  	var (
   146  		ctx    = context.Background()
   147  		typs   = []*types.T{types.Int}
   148  		batch  = testAllocator.NewMemBatch(typs)
   149  		rng, _ = randutil.NewPseudoRand()
   150  		// maxPartitions is in [1, 10]. The maximum partitions on a single level.
   151  		maxPartitions = 1 + rng.Intn(10)
   152  		// numRepartitions is in [1, 5].
   153  		numRepartitions = 1 + rng.Intn(5)
   154  	)
   155  	batch.SetLength(coldata.BatchSize())
   156  
   157  	queueCfg, cleanup := colcontainerutils.NewTestingDiskQueueCfg(t, true /* inMem */)
   158  	defer cleanup()
   159  
   160  	// Wrap the FS with an FS that counts the file descriptors, to assert that
   161  	// they line up with the semaphore's count.
   162  	countingFS := &fdCountingFS{FS: queueCfg.FS}
   163  	queueCfg.FS = countingFS
   164  
   165  	// Sort simulates the use of a PartitionedDiskQueue during an external sort.
   166  	t.Run(fmt.Sprintf("Sort/maxPartitions=%d/numRepartitions=%d", maxPartitions, numRepartitions), func(t *testing.T) {
   167  		queueCfg.CacheMode = colcontainer.DiskQueueCacheModeReuseCache
   168  		queueCfg.SetDefaultBufferSizeBytesForCacheMode()
   169  		// Creating a new testing semaphore will assert that no more than
   170  		// maxPartitions+1 are created. The +1 is the file descriptor of the
   171  		// new partition being written to when closedForWrites from maxPartitions
   172  		// and writing the merged result to a single new partition.
   173  		sem := colexecbase.NewTestingSemaphore(maxPartitions + 1)
   174  		p := colcontainer.NewPartitionedDiskQueue(typs, queueCfg, sem, colcontainer.PartitionerStrategyCloseOnNewPartition, testDiskAcc)
   175  
   176  		// Define sortRepartition to be able to call this helper function
   177  		// recursively.
   178  		var sortRepartition func(int, int)
   179  		sortRepartition = func(curPartitionIdx, numRepartitionsLeft int) {
   180  			if numRepartitionsLeft == 0 {
   181  				return
   182  			}
   183  
   184  			firstPartitionIdx := curPartitionIdx
   185  			// Create maxPartitions partitions.
   186  			for ; curPartitionIdx < firstPartitionIdx+maxPartitions; curPartitionIdx++ {
   187  				require.NoError(t, p.Enqueue(ctx, curPartitionIdx, batch))
   188  				// Assert that there is only one write file descriptor open at a time.
   189  				countingFS.assertOpenFDs(t, sem, 1, 0)
   190  			}
   191  
   192  			// Make sure that an Enqueue attempt on a previously closed partition
   193  			// fails.
   194  			if maxPartitions > 1 {
   195  				require.Error(t, p.Enqueue(ctx, firstPartitionIdx, batch))
   196  			}
   197  
   198  			// Closing all open read descriptors will still leave us with one
   199  			// write descriptor, since we only ever wrote.
   200  			require.NoError(t, p.CloseAllOpenReadFileDescriptors())
   201  			countingFS.assertOpenFDs(t, sem, 1, 0)
   202  			// Closing all write descriptors will close all descriptors.
   203  			require.NoError(t, p.CloseAllOpenWriteFileDescriptors(ctx))
   204  			countingFS.assertOpenFDs(t, sem, 0, 0)
   205  
   206  			// Now, we simulate a repartition. Open all partitions for reads.
   207  			for readPartitionIdx := firstPartitionIdx; readPartitionIdx < firstPartitionIdx+maxPartitions; readPartitionIdx++ {
   208  				require.NoError(t, p.Dequeue(ctx, readPartitionIdx, batch))
   209  				// Make sure the number of file descriptors increases and all of these
   210  				// files are read file descriptors.
   211  				countingFS.assertOpenFDs(t, sem, 0, (readPartitionIdx-firstPartitionIdx)+1)
   212  			}
   213  
   214  			// Now, we simulate a write of the merged partitions.
   215  			curPartitionIdx++
   216  			require.NoError(t, p.Enqueue(ctx, curPartitionIdx, batch))
   217  			// All file descriptors should still be open in addition to the new write
   218  			// file descriptor.
   219  			countingFS.assertOpenFDs(t, sem, 1, maxPartitions)
   220  
   221  			// Simulate closing all read partitions.
   222  			require.NoError(t, p.CloseAllOpenReadFileDescriptors())
   223  			// Only the write file descriptor should remain open. Note that this
   224  			// file descriptor should be closed on the next new partition, i.e. the
   225  			// next iteration (if any, otherwise p.Close should close it) of this loop.
   226  			countingFS.assertOpenFDs(t, sem, 1, 0)
   227  			// Call CloseInactiveReadPartitions to reclaim space.
   228  			require.NoError(t, p.CloseInactiveReadPartitions(ctx))
   229  			// Try to enqueue to a partition that was just closed.
   230  			require.Error(t, p.Enqueue(ctx, firstPartitionIdx, batch))
   231  			countingFS.assertOpenFDs(t, sem, 1, 0)
   232  
   233  			numRepartitionsLeft--
   234  			sortRepartition(curPartitionIdx, numRepartitionsLeft)
   235  		}
   236  
   237  		sortRepartition(0, numRepartitions)
   238  		require.NoError(t, p.Close(ctx))
   239  		countingFS.assertOpenFDs(t, sem, 0, 0)
   240  	})
   241  
   242  	t.Run(fmt.Sprintf("HashJoin/maxPartitions=%d/numRepartitions=%d", maxPartitions, numRepartitions), func(t *testing.T) {
   243  		queueCfg.CacheMode = colcontainer.DiskQueueCacheModeClearAndReuseCache
   244  		queueCfg.SetDefaultBufferSizeBytesForCacheMode()
   245  		// Double maxPartitions to get an even number, half for the left input, half
   246  		// for the right input. We'll consider the even index the left side and the
   247  		// next partition index the right side.
   248  		maxPartitions *= 2
   249  
   250  		// The limit for a hash join is maxPartitions + 2. maxPartitions is the
   251  		// number of partitions partitioned to and 2 represents the file descriptors
   252  		// for the left and right side in the case of a repartition.
   253  		sem := colexecbase.NewTestingSemaphore(maxPartitions + 2)
   254  		p := colcontainer.NewPartitionedDiskQueue(typs, queueCfg, sem, colcontainer.PartitionerStrategyDefault, testDiskAcc)
   255  
   256  		// joinRepartition will perform the partitioning that happens during a hash
   257  		// join. expectedRepartitionReadFDs are the read file descriptors that are
   258  		// expected to be open during a repartitioning step. 0 in the first call,
   259  		// 2 otherwise (left + right side).
   260  		var joinRepartition func(int, int, int, int)
   261  		joinRepartition = func(curPartitionIdx, readPartitionIdx, numRepartitionsLeft, expectedRepartitionReadFDs int) {
   262  			if numRepartitionsLeft == 0 {
   263  				return
   264  			}
   265  
   266  			firstPartitionIdx := curPartitionIdx
   267  			// Partitioning phase.
   268  			partitionIdxs := make([]int, maxPartitions)
   269  			for i := 0; i < maxPartitions; i, curPartitionIdx = i+1, curPartitionIdx+1 {
   270  				partitionIdxs[i] = curPartitionIdx
   271  			}
   272  			// Since we set these partitions randomly, simulate that.
   273  			rng.Shuffle(len(partitionIdxs), func(i, j int) {
   274  				partitionIdxs[i], partitionIdxs[j] = partitionIdxs[j], partitionIdxs[i]
   275  			})
   276  
   277  			for i, idx := range partitionIdxs {
   278  				require.NoError(t, p.Enqueue(ctx, idx, batch))
   279  				// Assert that the open file descriptors keep increasing, this is the
   280  				// default partitioner strategy behavior.
   281  				countingFS.assertOpenFDs(t, sem, i+1, expectedRepartitionReadFDs)
   282  			}
   283  
   284  			// The input has been partitioned. All file descriptors should be closed.
   285  			require.NoError(t, p.CloseAllOpenWriteFileDescriptors(ctx))
   286  			countingFS.assertOpenFDs(t, sem, 0, expectedRepartitionReadFDs)
   287  			require.NoError(t, p.CloseAllOpenReadFileDescriptors())
   288  			countingFS.assertOpenFDs(t, sem, 0, 0)
   289  			require.NoError(t, p.CloseInactiveReadPartitions(ctx))
   290  			countingFS.assertOpenFDs(t, sem, 0, 0)
   291  			// Now that we closed (read: deleted) the partitions read to repartition,
   292  			// it should be illegal to enqueue to that index.
   293  			if expectedRepartitionReadFDs > 0 {
   294  				require.Error(t, p.Dequeue(ctx, readPartitionIdx, batch))
   295  			}
   296  
   297  			// Now we simulate that one partition has been found to be too large. Read
   298  			// the first two partitions (left + right side) and assert that these file
   299  			// descriptors are open.
   300  			require.NoError(t, p.Dequeue(ctx, firstPartitionIdx, batch))
   301  			// We shouldn't have Dequeued an empty batch.
   302  			require.True(t, batch.Length() != 0)
   303  			require.NoError(t, p.Dequeue(ctx, firstPartitionIdx+1, batch))
   304  			// We shouldn't have Dequeued an empty batch.
   305  			require.True(t, batch.Length() != 0)
   306  			countingFS.assertOpenFDs(t, sem, 0, 2)
   307  
   308  			// Increment curPartitionIdx to the next available slot.
   309  			curPartitionIdx++
   310  
   311  			// Now we repartition these two partitions.
   312  			numRepartitionsLeft--
   313  			joinRepartition(curPartitionIdx, firstPartitionIdx, numRepartitionsLeft, 2)
   314  		}
   315  
   316  		joinRepartition(0, 0, numRepartitions, 0)
   317  	})
   318  }