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 }