github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/colexec/external_sort_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 colexec
    12  
    13  import (
    14  	"context"
    15  	"fmt"
    16  	"testing"
    17  
    18  	"github.com/cockroachdb/cockroach/pkg/col/coldata"
    19  	"github.com/cockroachdb/cockroach/pkg/settings/cluster"
    20  	"github.com/cockroachdb/cockroach/pkg/sql/colcontainer"
    21  	"github.com/cockroachdb/cockroach/pkg/sql/colexecbase"
    22  	"github.com/cockroachdb/cockroach/pkg/sql/colmem"
    23  	"github.com/cockroachdb/cockroach/pkg/sql/execinfra"
    24  	"github.com/cockroachdb/cockroach/pkg/sql/execinfrapb"
    25  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    26  	"github.com/cockroachdb/cockroach/pkg/sql/types"
    27  	"github.com/cockroachdb/cockroach/pkg/testutils/colcontainerutils"
    28  	"github.com/cockroachdb/cockroach/pkg/util/humanizeutil"
    29  	"github.com/cockroachdb/cockroach/pkg/util/leaktest"
    30  	"github.com/cockroachdb/cockroach/pkg/util/mon"
    31  	"github.com/cockroachdb/cockroach/pkg/util/randutil"
    32  	"github.com/marusama/semaphore"
    33  	"github.com/stretchr/testify/require"
    34  )
    35  
    36  func TestExternalSort(t *testing.T) {
    37  	defer leaktest.AfterTest(t)()
    38  	ctx := context.Background()
    39  	st := cluster.MakeTestingClusterSettings()
    40  	evalCtx := tree.MakeTestingEvalContext(st)
    41  	defer evalCtx.Stop(ctx)
    42  	flowCtx := &execinfra.FlowCtx{
    43  		EvalCtx: &evalCtx,
    44  		Cfg: &execinfra.ServerConfig{
    45  			Settings:    st,
    46  			DiskMonitor: testDiskMonitor,
    47  		},
    48  	}
    49  
    50  	const numForcedRepartitions = 3
    51  	queueCfg, cleanup := colcontainerutils.NewTestingDiskQueueCfg(t, true /* inMem */)
    52  	defer cleanup()
    53  
    54  	var (
    55  		accounts []*mon.BoundAccount
    56  		monitors []*mon.BytesMonitor
    57  	)
    58  	// Test the case in which the default memory is used as well as the case in
    59  	// which the joiner spills to disk.
    60  	for _, spillForced := range []bool{false, true} {
    61  		flowCtx.Cfg.TestingKnobs.ForceDiskSpill = spillForced
    62  		if spillForced {
    63  			// In order to increase test coverage of recursive merging, we have the
    64  			// lowest possible memory limit - this will force creating partitions
    65  			// consisting of a single batch.
    66  			flowCtx.Cfg.TestingKnobs.MemoryLimitBytes = 1
    67  		} else {
    68  			flowCtx.Cfg.TestingKnobs.MemoryLimitBytes = 0
    69  		}
    70  		for _, tcs := range [][]sortTestCase{sortAllTestCases, topKSortTestCases, sortChunksTestCases} {
    71  			for _, tc := range tcs {
    72  				t.Run(fmt.Sprintf("spillForced=%t/%s", spillForced, tc.description), func(t *testing.T) {
    73  					var semsToCheck []semaphore.Semaphore
    74  					runTestsWithTyps(
    75  						t,
    76  						[]tuples{tc.tuples},
    77  						[][]*types.T{tc.typs},
    78  						tc.expected,
    79  						orderedVerifier,
    80  						func(input []colexecbase.Operator) (colexecbase.Operator, error) {
    81  							// A sorter should never exceed externalSorterMinPartitions, even
    82  							// during repartitioning. A panic will happen if a sorter requests
    83  							// more than this number of file descriptors.
    84  							sem := colexecbase.NewTestingSemaphore(externalSorterMinPartitions)
    85  							// If a limit is satisfied before the sorter is drained of all its
    86  							// tuples, the sorter will not close its partitioner. During a
    87  							// flow this will happen in a downstream materializer/outbox,
    88  							// since there is no way to tell an operator that Next won't be
    89  							// called again.
    90  							if tc.k == 0 || tc.k >= len(tc.tuples) {
    91  								semsToCheck = append(semsToCheck, sem)
    92  							}
    93  							// TODO(asubiotto): Pass in the testing.T of the caller to this
    94  							//  function and do substring matching on the test name to
    95  							//  conditionally explicitly call Close() on the sorter (through
    96  							//  result.ToClose) in cases where it is know the sorter will not
    97  							//  be drained.
    98  							sorter, newAccounts, newMonitors, closers, err := createDiskBackedSorter(
    99  								ctx, flowCtx, input, tc.typs, tc.ordCols, tc.matchLen, tc.k, func() {},
   100  								numForcedRepartitions, false /* delegateFDAcquisition */, queueCfg, sem,
   101  							)
   102  							// Check that the sort was added as a Closer.
   103  							// TODO(asubiotto): Explicitly Close when testing.T is passed into
   104  							//  this constructor and we do a substring match.
   105  							require.Equal(t, 1, len(closers))
   106  							accounts = append(accounts, newAccounts...)
   107  							monitors = append(monitors, newMonitors...)
   108  							return sorter, err
   109  						})
   110  					for i, sem := range semsToCheck {
   111  						require.Equal(t, 0, sem.GetCount(), "sem still reports open FDs at index %d", i)
   112  					}
   113  				})
   114  			}
   115  		}
   116  	}
   117  	for _, acc := range accounts {
   118  		acc.Close(ctx)
   119  	}
   120  	for _, mon := range monitors {
   121  		mon.Stop(ctx)
   122  	}
   123  }
   124  
   125  func TestExternalSortRandomized(t *testing.T) {
   126  	defer leaktest.AfterTest(t)()
   127  	ctx := context.Background()
   128  	st := cluster.MakeTestingClusterSettings()
   129  	evalCtx := tree.MakeTestingEvalContext(st)
   130  	defer evalCtx.Stop(ctx)
   131  	flowCtx := &execinfra.FlowCtx{
   132  		EvalCtx: &evalCtx,
   133  		Cfg: &execinfra.ServerConfig{
   134  			Settings:    st,
   135  			DiskMonitor: testDiskMonitor,
   136  		},
   137  	}
   138  	rng, _ := randutil.NewPseudoRand()
   139  	nTups := coldata.BatchSize()*4 + 1
   140  	maxCols := 2
   141  	// TODO(yuzefovich): randomize types as well.
   142  	typs := make([]*types.T, maxCols)
   143  	for i := range typs {
   144  		typs[i] = types.Int
   145  	}
   146  
   147  	const numForcedRepartitions = 3
   148  	queueCfg, cleanup := colcontainerutils.NewTestingDiskQueueCfg(t, true /* inMem */)
   149  	defer cleanup()
   150  
   151  	var (
   152  		accounts []*mon.BoundAccount
   153  		monitors []*mon.BytesMonitor
   154  	)
   155  	// Interesting disk spilling scenarios:
   156  	// 1) The sorter is forced to spill to disk as soon as possible.
   157  	// 2) The memory limit is dynamically set to repartition twice, this will also
   158  	//    allow the in-memory sorter to spool several batches before hitting the
   159  	//    memory limit.
   160  	// memoryToSort is the total amount of memory that will be sorted in this
   161  	// test.
   162  	memoryToSort := (nTups / coldata.BatchSize()) * colmem.EstimateBatchSizeBytes(typs, coldata.BatchSize())
   163  	// partitionSize will be the memory limit passed in to tests with a memory
   164  	// limit. With a maximum number of partitions of 2 this will result in
   165  	// repartitioning twice. To make this a total amount of memory, we also need
   166  	// to add the cache sizes of the queues.
   167  	partitionSize := int64(memoryToSort/4) + int64(externalSorterMinPartitions*queueCfg.BufferSizeBytes)
   168  	for _, tk := range []execinfra.TestingKnobs{{ForceDiskSpill: true}, {MemoryLimitBytes: partitionSize}} {
   169  		flowCtx.Cfg.TestingKnobs = tk
   170  		for nCols := 1; nCols <= maxCols; nCols++ {
   171  			for nOrderingCols := 1; nOrderingCols <= nCols; nOrderingCols++ {
   172  				namePrefix := "MemoryLimit=" + humanizeutil.IBytes(tk.MemoryLimitBytes)
   173  				if tk.ForceDiskSpill {
   174  					namePrefix = "ForceDiskSpill=true"
   175  				}
   176  				delegateFDAcquisition := rng.Float64() < 0.5
   177  				name := fmt.Sprintf("%s/nCols=%d/nOrderingCols=%d/delegateFDAcquisition=%t", namePrefix, nCols, nOrderingCols, delegateFDAcquisition)
   178  				t.Run(name, func(t *testing.T) {
   179  					// Unfortunately, there is currently no better way to check that a
   180  					// sorter does not have leftover file descriptors other than appending
   181  					// each semaphore used to this slice on construction. This is because
   182  					// some tests don't fully drain the input, making intercepting the
   183  					// sorter.Close() method not a useful option, since it is impossible
   184  					// to check between an expected case where more than 0 FDs are open
   185  					// (e.g. in verifySelAndNullResets, where the sorter is not fully
   186  					// drained so Close must be called explicitly) and an unexpected one.
   187  					// These cases happen during normal execution when a limit is
   188  					// satisfied, but flows will call Close explicitly on Cleanup.
   189  					// TODO(asubiotto): Not implemented yet, currently we rely on the
   190  					//  flow tracking open FDs and releasing any leftovers.
   191  					var semsToCheck []semaphore.Semaphore
   192  					tups, expected, ordCols := generateRandomDataForTestSort(rng, nTups, nCols, nOrderingCols)
   193  					runTests(
   194  						t,
   195  						[]tuples{tups},
   196  						expected,
   197  						orderedVerifier,
   198  						func(input []colexecbase.Operator) (colexecbase.Operator, error) {
   199  							sem := colexecbase.NewTestingSemaphore(externalSorterMinPartitions)
   200  							semsToCheck = append(semsToCheck, sem)
   201  							sorter, newAccounts, newMonitors, closers, err := createDiskBackedSorter(
   202  								ctx, flowCtx, input, typs[:nCols], ordCols,
   203  								0 /* matchLen */, 0 /* k */, func() {},
   204  								numForcedRepartitions, delegateFDAcquisition, queueCfg, sem)
   205  							// TODO(asubiotto): Explicitly Close when testing.T is passed into
   206  							//  this constructor and we do a substring match.
   207  							require.Equal(t, 1, len(closers))
   208  							accounts = append(accounts, newAccounts...)
   209  							monitors = append(monitors, newMonitors...)
   210  							return sorter, err
   211  						})
   212  					for i, sem := range semsToCheck {
   213  						require.Equal(t, 0, sem.GetCount(), "sem still reports open FDs at index %d", i)
   214  					}
   215  				})
   216  			}
   217  		}
   218  	}
   219  	for _, acc := range accounts {
   220  		acc.Close(ctx)
   221  	}
   222  	for _, m := range monitors {
   223  		m.Stop(ctx)
   224  	}
   225  }
   226  
   227  func BenchmarkExternalSort(b *testing.B) {
   228  	defer leaktest.AfterTest(b)()
   229  	ctx := context.Background()
   230  	st := cluster.MakeTestingClusterSettings()
   231  	evalCtx := tree.MakeTestingEvalContext(st)
   232  	defer evalCtx.Stop(ctx)
   233  	flowCtx := &execinfra.FlowCtx{
   234  		EvalCtx: &evalCtx,
   235  		Cfg: &execinfra.ServerConfig{
   236  			Settings:    st,
   237  			DiskMonitor: testDiskMonitor,
   238  		},
   239  	}
   240  	rng, _ := randutil.NewPseudoRand()
   241  	var (
   242  		memAccounts []*mon.BoundAccount
   243  		memMonitors []*mon.BytesMonitor
   244  	)
   245  
   246  	queueCfg, cleanup := colcontainerutils.NewTestingDiskQueueCfg(b, false /* inMem */)
   247  	defer cleanup()
   248  
   249  	for _, nBatches := range []int{1 << 1, 1 << 4, 1 << 8} {
   250  		for _, nCols := range []int{1, 2, 4} {
   251  			for _, spillForced := range []bool{false, true} {
   252  				flowCtx.Cfg.TestingKnobs.ForceDiskSpill = spillForced
   253  				name := fmt.Sprintf("rows=%d/cols=%d/spilled=%t", nBatches*coldata.BatchSize(), nCols, spillForced)
   254  				b.Run(name, func(b *testing.B) {
   255  					// 8 (bytes / int64) * nBatches (number of batches) * coldata.BatchSize() (rows /
   256  					// batch) * nCols (number of columns / row).
   257  					b.SetBytes(int64(8 * nBatches * coldata.BatchSize() * nCols))
   258  					typs := make([]*types.T, nCols)
   259  					for i := range typs {
   260  						typs[i] = types.Int
   261  					}
   262  					batch := testAllocator.NewMemBatch(typs)
   263  					batch.SetLength(coldata.BatchSize())
   264  					ordCols := make([]execinfrapb.Ordering_Column, nCols)
   265  					for i := range ordCols {
   266  						ordCols[i].ColIdx = uint32(i)
   267  						ordCols[i].Direction = execinfrapb.Ordering_Column_Direction(rng.Int() % 2)
   268  						col := batch.ColVec(i).Int64()
   269  						for j := 0; j < coldata.BatchSize(); j++ {
   270  							col[j] = rng.Int63() % int64((i*1024)+1)
   271  						}
   272  					}
   273  					b.ResetTimer()
   274  					for n := 0; n < b.N; n++ {
   275  						source := newFiniteBatchSource(batch, typs, nBatches)
   276  						var spilled bool
   277  						sorter, accounts, monitors, _, err := createDiskBackedSorter(
   278  							ctx, flowCtx, []colexecbase.Operator{source}, typs, ordCols,
   279  							0 /* matchLen */, 0 /* k */, func() { spilled = true },
   280  							0 /* numForcedRepartitions */, false /* delegateFDAcquisitions */, queueCfg, &colexecbase.TestingSemaphore{},
   281  						)
   282  						memAccounts = append(memAccounts, accounts...)
   283  						memMonitors = append(memMonitors, monitors...)
   284  						if err != nil {
   285  							b.Fatal(err)
   286  						}
   287  						sorter.Init()
   288  						for out := sorter.Next(ctx); out.Length() != 0; out = sorter.Next(ctx) {
   289  						}
   290  						require.Equal(b, spillForced, spilled, fmt.Sprintf(
   291  							"expected: spilled=%t\tactual: spilled=%t", spillForced, spilled,
   292  						))
   293  					}
   294  				})
   295  			}
   296  		}
   297  	}
   298  	for _, account := range memAccounts {
   299  		account.Close(ctx)
   300  	}
   301  	for _, monitor := range memMonitors {
   302  		monitor.Stop(ctx)
   303  	}
   304  }
   305  
   306  // createDiskBackedSorter is a helper function that instantiates a disk-backed
   307  // sort operator. The desired memory limit must have been already set on
   308  // flowCtx. It returns an operator and an error as well as memory monitors and
   309  // memory accounts that will need to be closed once the caller is done with the
   310  // operator.
   311  func createDiskBackedSorter(
   312  	ctx context.Context,
   313  	flowCtx *execinfra.FlowCtx,
   314  	input []colexecbase.Operator,
   315  	typs []*types.T,
   316  	ordCols []execinfrapb.Ordering_Column,
   317  	matchLen int,
   318  	k int,
   319  	spillingCallbackFn func(),
   320  	numForcedRepartitions int,
   321  	delegateFDAcquisitions bool,
   322  	diskQueueCfg colcontainer.DiskQueueCfg,
   323  	testingSemaphore semaphore.Semaphore,
   324  ) (colexecbase.Operator, []*mon.BoundAccount, []*mon.BytesMonitor, []IdempotentCloser, error) {
   325  	sorterSpec := &execinfrapb.SorterSpec{
   326  		OutputOrdering:   execinfrapb.Ordering{Columns: ordCols},
   327  		OrderingMatchLen: uint32(matchLen),
   328  	}
   329  	spec := &execinfrapb.ProcessorSpec{
   330  		Input: []execinfrapb.InputSyncSpec{{ColumnTypes: typs}},
   331  		Core: execinfrapb.ProcessorCoreUnion{
   332  			Sorter: sorterSpec,
   333  		},
   334  		Post: execinfrapb.PostProcessSpec{
   335  			Limit: uint64(k),
   336  		},
   337  	}
   338  	args := NewColOperatorArgs{
   339  		Spec:                spec,
   340  		Inputs:              input,
   341  		StreamingMemAccount: testMemAcc,
   342  		DiskQueueCfg:        diskQueueCfg,
   343  		FDSemaphore:         testingSemaphore,
   344  	}
   345  	// External sorter relies on different memory accounts to
   346  	// understand when to start a new partition, so we will not use
   347  	// the streaming memory account.
   348  	args.TestingKnobs.SpillingCallbackFn = spillingCallbackFn
   349  	args.TestingKnobs.NumForcedRepartitions = numForcedRepartitions
   350  	args.TestingKnobs.DelegateFDAcquisitions = delegateFDAcquisitions
   351  	result, err := NewColOperator(ctx, flowCtx, args)
   352  	return result.Op, result.OpAccounts, result.OpMonitors, result.ToClose, err
   353  }