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  }