github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/colflow/vectorized_flow_test.go (about) 1 // Copyright 2019 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 colflow 12 13 import ( 14 "context" 15 "path/filepath" 16 "sync" 17 "testing" 18 19 "github.com/cockroachdb/cockroach/pkg/base" 20 "github.com/cockroachdb/cockroach/pkg/settings/cluster" 21 "github.com/cockroachdb/cockroach/pkg/sql/colcontainer" 22 "github.com/cockroachdb/cockroach/pkg/sql/colexec" 23 "github.com/cockroachdb/cockroach/pkg/sql/colexecbase" 24 "github.com/cockroachdb/cockroach/pkg/sql/colflow/colrpc" 25 "github.com/cockroachdb/cockroach/pkg/sql/colmem" 26 "github.com/cockroachdb/cockroach/pkg/sql/execinfra" 27 "github.com/cockroachdb/cockroach/pkg/sql/execinfrapb" 28 "github.com/cockroachdb/cockroach/pkg/sql/flowinfra" 29 "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" 30 "github.com/cockroachdb/cockroach/pkg/sql/types" 31 "github.com/cockroachdb/cockroach/pkg/storage" 32 "github.com/cockroachdb/cockroach/pkg/testutils" 33 "github.com/cockroachdb/cockroach/pkg/util/leaktest" 34 "github.com/stretchr/testify/require" 35 ) 36 37 type callbackRemoteComponentCreator struct { 38 newOutboxFn func(*colmem.Allocator, colexecbase.Operator, []*types.T, []execinfrapb.MetadataSource) (*colrpc.Outbox, error) 39 newInboxFn func(allocator *colmem.Allocator, typs []*types.T, streamID execinfrapb.StreamID) (*colrpc.Inbox, error) 40 } 41 42 func (c callbackRemoteComponentCreator) newOutbox( 43 allocator *colmem.Allocator, 44 input colexecbase.Operator, 45 typs []*types.T, 46 metadataSources []execinfrapb.MetadataSource, 47 toClose []colexec.IdempotentCloser, 48 ) (*colrpc.Outbox, error) { 49 return c.newOutboxFn(allocator, input, typs, metadataSources) 50 } 51 52 func (c callbackRemoteComponentCreator) newInbox( 53 allocator *colmem.Allocator, typs []*types.T, streamID execinfrapb.StreamID, 54 ) (*colrpc.Inbox, error) { 55 return c.newInboxFn(allocator, typs, streamID) 56 } 57 58 func intCols(numCols int) []*types.T { 59 cols := make([]*types.T, numCols) 60 for i := range cols { 61 cols[i] = types.Int 62 } 63 return cols 64 } 65 66 // TestDrainOnlyInputDAG is a regression test for #39137 to ensure 67 // that queries don't hang using the following scenario: 68 // Consider two nodes n1 and n2, an outbox (o1) and inbox (i1) on n1, and an 69 // arbitrary flow on n2. 70 // At the end of the query, o1 will drain its metadata sources when it 71 // encounters a zero-length batch from its input. If one of these metadata 72 // sources is i1, there is a possibility that a cycle is unknowingly created 73 // since i1 (as an example) could be pulling from a remote operator that itself 74 // is pulling from o1, which is at this moment attempting to drain i1. 75 // This test verifies that no metadata sources are added to an outbox that are 76 // not explicitly known to be in its input DAG. The diagram below outlines 77 // the main point of this test. The outbox's input ends up being some inbox 78 // pulling from somewhere upstream (in this diagram, node 3, but this detail is 79 // not important). If it drains the depicted inbox, that is pulling from node 2 80 // which is in turn pulling from an outbox, a cycle is created and the flow is 81 // blocked. 82 // +------------+ 83 // | Node 3 | 84 // +-----+------+ 85 // ^ 86 // Node 1 | Node 2 87 // +------------------------+-----------------+ 88 // +------------+ | 89 // Spec C +--------+ | | 90 // | | noop | | | 91 // | +---+----+ | | 92 // | ^ | | 93 // | +--+---+ | | 94 // | |outbox| +<----------+ 95 // | +------+ | | | 96 // +------------+ | | 97 // Drain cycle!---+ | +----+-----------------+ 98 // v | |Any group of operators| 99 // +------------+ | +----+-----------------+ 100 // | +------+ | | ^ 101 // Spec A |inbox +--------------+ 102 // | +------+ | | 103 // +------------+ | 104 // ^ | 105 // | | 106 // +-----+------+ | 107 // Spec B noop | | 108 // |materializer| + 109 // +------------+ 110 func TestDrainOnlyInputDAG(t *testing.T) { 111 defer leaktest.AfterTest(t)() 112 113 const ( 114 numInputTypesToOutbox = 3 115 numInputTypesToMaterializer = 1 116 ) 117 // procs are the ProcessorSpecs that we pass in to create the flow. Note that 118 // we order the inbox first so that the flow creator instantiates it before 119 // anything else. 120 procs := []execinfrapb.ProcessorSpec{ 121 { 122 // This is i1, the inbox which should be drained by the materializer, not 123 // o1. 124 // Spec A in the diagram. 125 Input: []execinfrapb.InputSyncSpec{ 126 { 127 Streams: []execinfrapb.StreamEndpointSpec{{Type: execinfrapb.StreamEndpointSpec_REMOTE, StreamID: 1}}, 128 ColumnTypes: intCols(numInputTypesToMaterializer), 129 }, 130 }, 131 Core: execinfrapb.ProcessorCoreUnion{Noop: &execinfrapb.NoopCoreSpec{}}, 132 Output: []execinfrapb.OutputRouterSpec{ 133 { 134 Type: execinfrapb.OutputRouterSpec_PASS_THROUGH, 135 // We set up a local output so that the inbox is created independently. 136 Streams: []execinfrapb.StreamEndpointSpec{ 137 {Type: execinfrapb.StreamEndpointSpec_LOCAL, StreamID: 2}, 138 }, 139 }, 140 }, 141 }, 142 // This is the root of the flow. The noop operator that will read from i1 143 // and the materializer. 144 // Spec B in the diagram. 145 { 146 Input: []execinfrapb.InputSyncSpec{ 147 { 148 Streams: []execinfrapb.StreamEndpointSpec{{Type: execinfrapb.StreamEndpointSpec_LOCAL, StreamID: 2}}, 149 ColumnTypes: intCols(numInputTypesToMaterializer), 150 }, 151 }, 152 Core: execinfrapb.ProcessorCoreUnion{Noop: &execinfrapb.NoopCoreSpec{}}, 153 Output: []execinfrapb.OutputRouterSpec{ 154 { 155 Type: execinfrapb.OutputRouterSpec_PASS_THROUGH, 156 Streams: []execinfrapb.StreamEndpointSpec{{Type: execinfrapb.StreamEndpointSpec_SYNC_RESPONSE}}, 157 }, 158 }, 159 }, 160 { 161 // Because creating a table reader is too complex (you need to create a 162 // bunch of other state) we simulate this by creating a noop operator with 163 // a remote input, which is treated as having no local edges during 164 // topological processing. 165 // Spec C in the diagram. 166 Input: []execinfrapb.InputSyncSpec{ 167 { 168 Streams: []execinfrapb.StreamEndpointSpec{{Type: execinfrapb.StreamEndpointSpec_REMOTE}}, 169 // Use three Int columns as the types to be able to distinguish 170 // between input DAGs when creating the inbox. 171 ColumnTypes: intCols(numInputTypesToOutbox), 172 }, 173 }, 174 Core: execinfrapb.ProcessorCoreUnion{Noop: &execinfrapb.NoopCoreSpec{}}, 175 // This is o1, the outbox that will drain metadata. 176 Output: []execinfrapb.OutputRouterSpec{ 177 { 178 Type: execinfrapb.OutputRouterSpec_PASS_THROUGH, 179 Streams: []execinfrapb.StreamEndpointSpec{{Type: execinfrapb.StreamEndpointSpec_REMOTE}}, 180 }, 181 }, 182 }, 183 } 184 185 inboxToNumInputTypes := make(map[*colrpc.Inbox][]*types.T) 186 outboxCreated := false 187 componentCreator := callbackRemoteComponentCreator{ 188 newOutboxFn: func( 189 allocator *colmem.Allocator, 190 op colexecbase.Operator, 191 typs []*types.T, 192 sources []execinfrapb.MetadataSource, 193 ) (*colrpc.Outbox, error) { 194 require.False(t, outboxCreated) 195 outboxCreated = true 196 // Verify that there is only one metadata source: the inbox that is the 197 // input to the noop operator. This is verified by first checking the 198 // number of metadata sources and then that the input types are what we 199 // expect from the input DAG. 200 require.Len(t, sources, 1) 201 require.Len(t, inboxToNumInputTypes[sources[0].(*colrpc.Inbox)], numInputTypesToOutbox) 202 return colrpc.NewOutbox(allocator, op, typs, sources, nil /* toClose */) 203 }, 204 newInboxFn: func(allocator *colmem.Allocator, typs []*types.T, streamID execinfrapb.StreamID) (*colrpc.Inbox, error) { 205 inbox, err := colrpc.NewInbox(allocator, typs, streamID) 206 inboxToNumInputTypes[inbox] = typs 207 return inbox, err 208 }, 209 } 210 211 st := cluster.MakeTestingClusterSettings() 212 evalCtx := tree.MakeTestingEvalContext(st) 213 ctx := context.Background() 214 defer evalCtx.Stop(ctx) 215 f := &flowinfra.FlowBase{FlowCtx: execinfra.FlowCtx{EvalCtx: &evalCtx, NodeID: base.TestingIDContainer}} 216 var wg sync.WaitGroup 217 vfc := newVectorizedFlowCreator(&vectorizedFlowCreatorHelper{f: f}, componentCreator, false, &wg, &execinfra.RowChannel{}, nil, execinfrapb.FlowID{}, colcontainer.DiskQueueCfg{}, nil) 218 219 _, err := vfc.setupFlow(ctx, &f.FlowCtx, procs, flowinfra.FuseNormally) 220 defer func() { 221 for _, memAcc := range vfc.streamingMemAccounts { 222 memAcc.Close(ctx) 223 } 224 }() 225 require.NoError(t, err) 226 227 // Verify that an outbox was actually created. 228 require.True(t, outboxCreated) 229 } 230 231 // TestVectorizedFlowTempDirectory tests a flow's interactions with the 232 // temporary directory that will be used when spilling execution. Refer to 233 // subtests for a more thorough explanation. 234 func TestVectorizedFlowTempDirectory(t *testing.T) { 235 defer leaktest.AfterTest(t)() 236 237 st := cluster.MakeTestingClusterSettings() 238 evalCtx := tree.MakeTestingEvalContext(st) 239 ctx := context.Background() 240 defer evalCtx.Stop(ctx) 241 242 // We use an on-disk engine for this test since we're testing FS interactions 243 // and want to get the same behavior as a non-testing environment. 244 tempPath, dirCleanup := testutils.TempDir(t) 245 ngn, err := storage.NewDefaultEngine(0 /* cacheSize */, base.StorageConfig{Dir: tempPath}) 246 require.NoError(t, err) 247 defer ngn.Close() 248 defer dirCleanup() 249 250 newVectorizedFlow := func() *vectorizedFlow { 251 return NewVectorizedFlow( 252 &flowinfra.FlowBase{ 253 FlowCtx: execinfra.FlowCtx{ 254 Cfg: &execinfra.ServerConfig{ 255 TempFS: ngn, 256 TempStoragePath: tempPath, 257 VecFDSemaphore: &colexecbase.TestingSemaphore{}, 258 Metrics: &execinfra.DistSQLMetrics{}, 259 }, 260 EvalCtx: &evalCtx, 261 NodeID: base.TestingIDContainer, 262 }, 263 }, 264 ).(*vectorizedFlow) 265 } 266 267 dirs, err := ngn.List(tempPath) 268 require.NoError(t, err) 269 numDirsTheTestStartedWith := len(dirs) 270 checkDirs := func(t *testing.T, numExtraDirs int) { 271 t.Helper() 272 dirs, err := ngn.List(tempPath) 273 require.NoError(t, err) 274 expectedNumDirs := numDirsTheTestStartedWith + numExtraDirs 275 require.Equal(t, expectedNumDirs, len(dirs), "expected %d directories but found %d: %s", expectedNumDirs, len(dirs), dirs) 276 } 277 278 // LazilyCreated asserts that a directory is not created during flow Setup 279 // but is done so when an operator spills to disk. 280 t.Run("LazilyCreated", func(t *testing.T) { 281 vf := newVectorizedFlow() 282 var creator *vectorizedFlowCreator 283 vf.testingKnobs.onSetupFlow = func(c *vectorizedFlowCreator) { 284 creator = c 285 } 286 287 _, err := vf.Setup(ctx, &execinfrapb.FlowSpec{}, flowinfra.FuseNormally) 288 require.NoError(t, err) 289 290 // No directory should have been created. 291 checkDirs(t, 0) 292 293 // After the call to Setup, creator should be non-nil (i.e. the testing knob 294 // should have been called). 295 require.NotNil(t, creator) 296 297 // Now simulate an operator spilling to disk. The flow should have set this 298 // up to create its directory. 299 creator.diskQueueCfg.OnNewDiskQueueCb() 300 301 // We should now have one directory, the flow's temporary storage directory. 302 checkDirs(t, 1) 303 304 // Another operator calling OnNewDiskQueueCb again should not create a new 305 // directory 306 creator.diskQueueCfg.OnNewDiskQueueCb() 307 checkDirs(t, 1) 308 309 // When the flow is Cleaned up, this directory should be removed. 310 vf.Cleanup(ctx) 311 checkDirs(t, 0) 312 }) 313 314 // This subtest verifies that two local flows with the same ID create 315 // different directories. This case happens regularly with local flows, since 316 // they have an unset ID. 317 t.Run("DirCreationHandlesUnsetIDCollisions", func(t *testing.T) { 318 flowID := execinfrapb.FlowID{} 319 vf1 := newVectorizedFlow() 320 var creator1 *vectorizedFlowCreator 321 vf1.testingKnobs.onSetupFlow = func(c *vectorizedFlowCreator) { 322 creator1 = c 323 } 324 // Explicitly set an empty ID. 325 vf1.ID = flowID 326 _, err := vf1.Setup(ctx, &execinfrapb.FlowSpec{}, flowinfra.FuseNormally) 327 require.NoError(t, err) 328 329 checkDirs(t, 0) 330 creator1.diskQueueCfg.OnNewDiskQueueCb() 331 checkDirs(t, 1) 332 333 // Now a new flow with the same ID gets set up. 334 vf2 := newVectorizedFlow() 335 var creator2 *vectorizedFlowCreator 336 vf2.testingKnobs.onSetupFlow = func(c *vectorizedFlowCreator) { 337 creator2 = c 338 } 339 vf2.ID = flowID 340 _, err = vf2.Setup(ctx, &execinfrapb.FlowSpec{}, flowinfra.FuseNormally) 341 require.NoError(t, err) 342 343 // Still only 1 directory. 344 checkDirs(t, 1) 345 creator2.diskQueueCfg.OnNewDiskQueueCb() 346 // A new directory should have been created for this flow. 347 checkDirs(t, 2) 348 349 vf1.Cleanup(ctx) 350 checkDirs(t, 1) 351 vf2.Cleanup(ctx) 352 checkDirs(t, 0) 353 }) 354 355 t.Run("DirCreationRace", func(t *testing.T) { 356 vf := newVectorizedFlow() 357 var creator *vectorizedFlowCreator 358 vf.testingKnobs.onSetupFlow = func(c *vectorizedFlowCreator) { 359 creator = c 360 } 361 362 _, err := vf.Setup(ctx, &execinfrapb.FlowSpec{}, flowinfra.FuseNormally) 363 require.NoError(t, err) 364 365 createTempDir := creator.diskQueueCfg.OnNewDiskQueueCb 366 errCh := make(chan error) 367 go func() { 368 createTempDir() 369 errCh <- ngn.MkdirAll(filepath.Join(vf.tempStorage.path, "async")) 370 }() 371 createTempDir() 372 // Both goroutines should be able to create their subdirectories within the 373 // flow's temporary directory. 374 require.NoError(t, ngn.MkdirAll(filepath.Join(vf.tempStorage.path, "main_goroutine"))) 375 require.NoError(t, <-errCh) 376 vf.Cleanup(ctx) 377 checkDirs(t, 0) 378 }) 379 }