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 }