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 }