github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/colexec/external_hash_joiner_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/execinfra"
    23  	"github.com/cockroachdb/cockroach/pkg/sql/execinfrapb"
    24  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    25  	"github.com/cockroachdb/cockroach/pkg/sql/sqlbase"
    26  	"github.com/cockroachdb/cockroach/pkg/sql/types"
    27  	"github.com/cockroachdb/cockroach/pkg/testutils/colcontainerutils"
    28  	"github.com/cockroachdb/cockroach/pkg/util/leaktest"
    29  	"github.com/cockroachdb/cockroach/pkg/util/mon"
    30  	"github.com/cockroachdb/cockroach/pkg/util/randutil"
    31  	"github.com/marusama/semaphore"
    32  	"github.com/stretchr/testify/require"
    33  )
    34  
    35  func TestExternalHashJoiner(t *testing.T) {
    36  	defer leaktest.AfterTest(t)()
    37  
    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  	queueCfg, cleanup := colcontainerutils.NewTestingDiskQueueCfg(t, true /* inMem */)
    51  	defer cleanup()
    52  
    53  	var (
    54  		accounts []*mon.BoundAccount
    55  		monitors []*mon.BytesMonitor
    56  	)
    57  	rng, _ := randutil.NewPseudoRand()
    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  		for _, tcs := range [][]*joinTestCase{hjTestCases, mjTestCases} {
    63  			for _, tc := range tcs {
    64  				delegateFDAcquisitions := rng.Float64() < 0.5
    65  				t.Run(fmt.Sprintf("spillForced=%t/%s/delegateFDAcquisitions=%t", spillForced, tc.description, delegateFDAcquisitions), func(t *testing.T) {
    66  					var semsToCheck []semaphore.Semaphore
    67  					if !tc.onExpr.Empty() {
    68  						// When we have ON expression, there might be other operators (like
    69  						// selections) on top of the external hash joiner in
    70  						// diskSpiller.diskBackedOp chain. This will not allow for Close()
    71  						// call to propagate to the external hash joiner, so we will skip
    72  						// allNullsInjection test for now.
    73  						defer func(oldValue bool) {
    74  							tc.skipAllNullsInjection = oldValue
    75  						}(tc.skipAllNullsInjection)
    76  						tc.skipAllNullsInjection = true
    77  					}
    78  					runHashJoinTestCase(t, tc, func(sources []colexecbase.Operator) (colexecbase.Operator, error) {
    79  						sem := colexecbase.NewTestingSemaphore(externalHJMinPartitions)
    80  						semsToCheck = append(semsToCheck, sem)
    81  						spec := createSpecForHashJoiner(tc)
    82  						// TODO(asubiotto): Pass in the testing.T of the caller to this
    83  						//  function and do substring matching on the test name to
    84  						//  conditionally explicitly call Close() on the hash joiner
    85  						//  (through result.ToClose) in cases where it is known the sorter
    86  						//  will not be drained.
    87  						hjOp, newAccounts, newMonitors, closers, err := createDiskBackedHashJoiner(
    88  							ctx, flowCtx, spec, sources, func() {}, queueCfg,
    89  							2 /* numForcedPartitions */, delegateFDAcquisitions, sem,
    90  						)
    91  						// Expect three closers. These are the external hash joiner, and
    92  						// one external sorter for each input.
    93  						// TODO(asubiotto): Explicitly Close when testing.T is passed into
    94  						//  this constructor and we do a substring match.
    95  						require.Equal(t, 3, len(closers))
    96  						accounts = append(accounts, newAccounts...)
    97  						monitors = append(monitors, newMonitors...)
    98  						return hjOp, err
    99  					})
   100  					for i, sem := range semsToCheck {
   101  						require.Equal(t, 0, sem.GetCount(), "sem still reports open FDs at index %d", i)
   102  					}
   103  				})
   104  			}
   105  		}
   106  	}
   107  	for _, acc := range accounts {
   108  		acc.Close(ctx)
   109  	}
   110  	for _, mon := range monitors {
   111  		mon.Stop(ctx)
   112  	}
   113  }
   114  
   115  // TestExternalHashJoinerFallbackToSortMergeJoin tests that the external hash
   116  // joiner falls back to using sort + merge join when repartitioning doesn't
   117  // decrease the size of the partition. We instantiate two sources that contain
   118  // the same tuple many times.
   119  func TestExternalHashJoinerFallbackToSortMergeJoin(t *testing.T) {
   120  	defer leaktest.AfterTest(t)()
   121  	ctx := context.Background()
   122  	st := cluster.MakeTestingClusterSettings()
   123  	evalCtx := tree.MakeTestingEvalContext(st)
   124  	defer evalCtx.Stop(ctx)
   125  	flowCtx := &execinfra.FlowCtx{
   126  		EvalCtx: &evalCtx,
   127  		Cfg: &execinfra.ServerConfig{
   128  			Settings: st,
   129  			TestingKnobs: execinfra.TestingKnobs{
   130  				ForceDiskSpill:   true,
   131  				MemoryLimitBytes: 1,
   132  			},
   133  			DiskMonitor: testDiskMonitor,
   134  		},
   135  	}
   136  	sourceTypes := []*types.T{types.Int}
   137  	batch := testAllocator.NewMemBatch(sourceTypes)
   138  	// We don't need to set the data since zero values in the columns work.
   139  	batch.SetLength(coldata.BatchSize())
   140  	nBatches := 2
   141  	leftSource := newFiniteBatchSource(batch, sourceTypes, nBatches)
   142  	rightSource := newFiniteBatchSource(batch, sourceTypes, nBatches)
   143  	tc := &joinTestCase{
   144  		joinType:     sqlbase.InnerJoin,
   145  		leftTypes:    sourceTypes,
   146  		leftOutCols:  []uint32{0},
   147  		leftEqCols:   []uint32{0},
   148  		rightTypes:   sourceTypes,
   149  		rightOutCols: []uint32{0},
   150  		rightEqCols:  []uint32{0},
   151  	}
   152  	tc.init()
   153  	spec := createSpecForHashJoiner(tc)
   154  	var spilled bool
   155  	queueCfg, cleanup := colcontainerutils.NewTestingDiskQueueCfg(t, true /* inMem */)
   156  	defer cleanup()
   157  	sem := colexecbase.NewTestingSemaphore(externalHJMinPartitions)
   158  	// Ignore closers since the sorter should close itself when it is drained of
   159  	// all tuples. We assert this by checking that the semaphore reports a count
   160  	// of 0.
   161  	hj, accounts, monitors, _, err := createDiskBackedHashJoiner(
   162  		ctx, flowCtx, spec, []colexecbase.Operator{leftSource, rightSource},
   163  		func() { spilled = true }, queueCfg, 0 /* numForcedRepartitions */, true, /* delegateFDAcquisitions */
   164  		sem,
   165  	)
   166  	defer func() {
   167  		for _, acc := range accounts {
   168  			acc.Close(ctx)
   169  		}
   170  		for _, mon := range monitors {
   171  			mon.Stop(ctx)
   172  		}
   173  	}()
   174  	require.NoError(t, err)
   175  	hj.Init()
   176  	// We have a full cross-product, so we should get the number of tuples
   177  	// squared in the output.
   178  	expectedTuplesCount := nBatches * nBatches * coldata.BatchSize() * coldata.BatchSize()
   179  	actualTuplesCount := 0
   180  	for b := hj.Next(ctx); b.Length() > 0; b = hj.Next(ctx) {
   181  		actualTuplesCount += b.Length()
   182  	}
   183  	require.True(t, spilled)
   184  	require.Equal(t, expectedTuplesCount, actualTuplesCount)
   185  	require.Equal(t, 0, sem.GetCount())
   186  }
   187  
   188  func BenchmarkExternalHashJoiner(b *testing.B) {
   189  	ctx := context.Background()
   190  	st := cluster.MakeTestingClusterSettings()
   191  	evalCtx := tree.MakeTestingEvalContext(st)
   192  	defer evalCtx.Stop(ctx)
   193  	flowCtx := &execinfra.FlowCtx{
   194  		EvalCtx: &evalCtx,
   195  		Cfg: &execinfra.ServerConfig{
   196  			Settings:    st,
   197  			DiskMonitor: testDiskMonitor,
   198  		},
   199  	}
   200  	nCols := 4
   201  	sourceTypes := make([]*types.T, nCols)
   202  
   203  	for colIdx := 0; colIdx < nCols; colIdx++ {
   204  		sourceTypes[colIdx] = types.Int
   205  	}
   206  
   207  	batch := testAllocator.NewMemBatch(sourceTypes)
   208  	for colIdx := 0; colIdx < nCols; colIdx++ {
   209  		col := batch.ColVec(colIdx).Int64()
   210  		for i := 0; i < coldata.BatchSize(); i++ {
   211  			col[i] = int64(i)
   212  		}
   213  	}
   214  	batch.SetLength(coldata.BatchSize())
   215  
   216  	var (
   217  		memAccounts []*mon.BoundAccount
   218  		memMonitors []*mon.BytesMonitor
   219  	)
   220  	for _, hasNulls := range []bool{false, true} {
   221  		if hasNulls {
   222  			for colIdx := 0; colIdx < nCols; colIdx++ {
   223  				vec := batch.ColVec(colIdx)
   224  				vec.Nulls().SetNull(0)
   225  			}
   226  		} else {
   227  			for colIdx := 0; colIdx < nCols; colIdx++ {
   228  				vec := batch.ColVec(colIdx)
   229  				vec.Nulls().UnsetNulls()
   230  			}
   231  		}
   232  		queueCfg, cleanup := colcontainerutils.NewTestingDiskQueueCfg(b, false /* inMem */)
   233  		defer cleanup()
   234  		leftSource := newFiniteBatchSource(batch, sourceTypes, 0)
   235  		rightSource := newFiniteBatchSource(batch, sourceTypes, 0)
   236  		for _, fullOuter := range []bool{false, true} {
   237  			for _, nBatches := range []int{1 << 2, 1 << 7} {
   238  				for _, spillForced := range []bool{false, true} {
   239  					flowCtx.Cfg.TestingKnobs.ForceDiskSpill = spillForced
   240  					name := fmt.Sprintf(
   241  						"nulls=%t/fullOuter=%t/batches=%d/spillForced=%t",
   242  						hasNulls, fullOuter, nBatches, spillForced)
   243  					joinType := sqlbase.InnerJoin
   244  					if fullOuter {
   245  						joinType = sqlbase.FullOuterJoin
   246  					}
   247  					tc := &joinTestCase{
   248  						joinType:     joinType,
   249  						leftTypes:    sourceTypes,
   250  						leftOutCols:  []uint32{0, 1},
   251  						leftEqCols:   []uint32{0, 2},
   252  						rightTypes:   sourceTypes,
   253  						rightOutCols: []uint32{2, 3},
   254  						rightEqCols:  []uint32{0, 1},
   255  					}
   256  					tc.init()
   257  					spec := createSpecForHashJoiner(tc)
   258  					b.Run(name, func(b *testing.B) {
   259  						// 8 (bytes / int64) * nBatches (number of batches) * col.BatchSize() (rows /
   260  						// batch) * nCols (number of columns / row) * 2 (number of sources).
   261  						b.SetBytes(int64(8 * nBatches * coldata.BatchSize() * nCols * 2))
   262  						b.ResetTimer()
   263  						for i := 0; i < b.N; i++ {
   264  							leftSource.reset(nBatches)
   265  							rightSource.reset(nBatches)
   266  							hj, accounts, monitors, _, err := createDiskBackedHashJoiner(
   267  								ctx, flowCtx, spec, []colexecbase.Operator{leftSource, rightSource},
   268  								func() {}, queueCfg, 0 /* numForcedRepartitions */, false, /* delegateFDAcquisitions */
   269  								colexecbase.NewTestingSemaphore(VecMaxOpenFDsLimit),
   270  							)
   271  							memAccounts = append(memAccounts, accounts...)
   272  							memMonitors = append(memMonitors, monitors...)
   273  							require.NoError(b, err)
   274  							hj.Init()
   275  							for b := hj.Next(ctx); b.Length() > 0; b = hj.Next(ctx) {
   276  							}
   277  						}
   278  					})
   279  				}
   280  			}
   281  		}
   282  	}
   283  	for _, memAccount := range memAccounts {
   284  		memAccount.Close(ctx)
   285  	}
   286  	for _, memMonitor := range memMonitors {
   287  		memMonitor.Stop(ctx)
   288  	}
   289  }
   290  
   291  // createDiskBackedHashJoiner is a helper function that instantiates a
   292  // disk-backed hash join operator. The desired memory limit must have been
   293  // already set on flowCtx. It returns an operator and an error as well as
   294  // memory monitors and memory accounts that will need to be closed once the
   295  // caller is done with the operator.
   296  func createDiskBackedHashJoiner(
   297  	ctx context.Context,
   298  	flowCtx *execinfra.FlowCtx,
   299  	spec *execinfrapb.ProcessorSpec,
   300  	inputs []colexecbase.Operator,
   301  	spillingCallbackFn func(),
   302  	diskQueueCfg colcontainer.DiskQueueCfg,
   303  	numForcedRepartitions int,
   304  	delegateFDAcquisitions bool,
   305  	testingSemaphore semaphore.Semaphore,
   306  ) (colexecbase.Operator, []*mon.BoundAccount, []*mon.BytesMonitor, []IdempotentCloser, error) {
   307  	args := NewColOperatorArgs{
   308  		Spec:                spec,
   309  		Inputs:              inputs,
   310  		StreamingMemAccount: testMemAcc,
   311  		DiskQueueCfg:        diskQueueCfg,
   312  		FDSemaphore:         testingSemaphore,
   313  	}
   314  	// We will not use streaming memory account for the external hash join so
   315  	// that the in-memory hash join operator could hit the memory limit set on
   316  	// flowCtx.
   317  	args.TestingKnobs.SpillingCallbackFn = spillingCallbackFn
   318  	args.TestingKnobs.NumForcedRepartitions = numForcedRepartitions
   319  	args.TestingKnobs.DelegateFDAcquisitions = delegateFDAcquisitions
   320  	result, err := NewColOperator(ctx, flowCtx, args)
   321  	return result.Op, result.OpAccounts, result.OpMonitors, result.ToClose, err
   322  }