github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/dispatch/remote/cluster_test.go (about)

     1  package remote
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/authzed/spicedb/internal/dispatch"
    11  
    12  	humanize "github.com/dustin/go-humanize"
    13  	"github.com/stretchr/testify/require"
    14  	"google.golang.org/grpc"
    15  	"google.golang.org/grpc/credentials/insecure"
    16  	"google.golang.org/grpc/test/bufconn"
    17  
    18  	"github.com/authzed/spicedb/internal/dispatch/keys"
    19  	corev1 "github.com/authzed/spicedb/pkg/proto/core/v1"
    20  	v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
    21  )
    22  
    23  type fakeDispatchSvc struct {
    24  	v1.UnimplementedDispatchServiceServer
    25  
    26  	sleepTime     time.Duration
    27  	dispatchCount uint32
    28  }
    29  
    30  func (fds *fakeDispatchSvc) DispatchCheck(context.Context, *v1.DispatchCheckRequest) (*v1.DispatchCheckResponse, error) {
    31  	time.Sleep(fds.sleepTime)
    32  	return &v1.DispatchCheckResponse{
    33  		Metadata: &v1.ResponseMeta{
    34  			DispatchCount: fds.dispatchCount,
    35  		},
    36  	}, nil
    37  }
    38  
    39  func (fds *fakeDispatchSvc) DispatchLookupSubjects(_ *v1.DispatchLookupSubjectsRequest, srv v1.DispatchService_DispatchLookupSubjectsServer) error {
    40  	time.Sleep(fds.sleepTime)
    41  	return srv.Send(&v1.DispatchLookupSubjectsResponse{
    42  		Metadata: emptyMetadata,
    43  	})
    44  }
    45  
    46  func TestDispatchTimeout(t *testing.T) {
    47  	for _, tc := range []struct {
    48  		timeout   time.Duration
    49  		sleepTime time.Duration
    50  	}{
    51  		{
    52  			10 * time.Millisecond,
    53  			20 * time.Millisecond,
    54  		},
    55  		{
    56  			100 * time.Millisecond,
    57  			20 * time.Millisecond,
    58  		},
    59  	} {
    60  		tc := tc
    61  		t.Run(fmt.Sprintf("%v", tc.timeout > tc.sleepTime), func(t *testing.T) {
    62  			// Configure a fake dispatcher service and an associated buffconn-based
    63  			// connection to it.
    64  			listener := bufconn.Listen(humanize.MiByte)
    65  			s := grpc.NewServer()
    66  
    67  			fakeDispatch := &fakeDispatchSvc{sleepTime: tc.sleepTime}
    68  			v1.RegisterDispatchServiceServer(s, fakeDispatch)
    69  
    70  			go func() {
    71  				// Ignore any errors
    72  				_ = s.Serve(listener)
    73  			}()
    74  
    75  			conn, err := grpc.DialContext(
    76  				context.Background(),
    77  				"",
    78  				grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
    79  					return listener.Dial()
    80  				}),
    81  				grpc.WithTransportCredentials(insecure.NewCredentials()),
    82  				grpc.WithBlock(),
    83  			)
    84  			require.NoError(t, err)
    85  
    86  			t.Cleanup(func() {
    87  				conn.Close()
    88  				listener.Close()
    89  				s.Stop()
    90  			})
    91  
    92  			// Configure a dispatcher with a very low timeout.
    93  			dispatcher := NewClusterDispatcher(v1.NewDispatchServiceClient(conn), conn, ClusterDispatcherConfig{
    94  				KeyHandler:             &keys.DirectKeyHandler{},
    95  				DispatchOverallTimeout: tc.timeout,
    96  			}, nil, nil)
    97  			require.True(t, dispatcher.ReadyState().IsReady)
    98  
    99  			// Invoke a dispatched "check" and ensure it times out, as the fake dispatch will wait
   100  			// longer than the configured timeout.
   101  			resp, err := dispatcher.DispatchCheck(context.Background(), &v1.DispatchCheckRequest{
   102  				ResourceRelation: &corev1.RelationReference{Namespace: "sometype", Relation: "somerel"},
   103  				ResourceIds:      []string{"foo"},
   104  				Metadata:         &v1.ResolverMeta{DepthRemaining: 50},
   105  				Subject:          &corev1.ObjectAndRelation{Namespace: "foo", ObjectId: "bar", Relation: "..."},
   106  			})
   107  			if tc.sleepTime > tc.timeout {
   108  				require.Error(t, err)
   109  				require.ErrorContains(t, err, "context deadline exceeded")
   110  			} else {
   111  				require.NoError(t, err)
   112  				require.NotNil(t, resp)
   113  				require.GreaterOrEqual(t, resp.Metadata.DispatchCount, uint32(1))
   114  			}
   115  
   116  			// Invoke a dispatched "LookupSubjects" and test as well.
   117  			stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupSubjectsResponse](context.Background())
   118  			err = dispatcher.DispatchLookupSubjects(&v1.DispatchLookupSubjectsRequest{
   119  				ResourceRelation: &corev1.RelationReference{Namespace: "sometype", Relation: "somerel"},
   120  				ResourceIds:      []string{"foo"},
   121  				Metadata:         &v1.ResolverMeta{DepthRemaining: 50},
   122  				SubjectRelation:  &corev1.RelationReference{Namespace: "sometype", Relation: "somerel"},
   123  			}, stream)
   124  			if tc.sleepTime > tc.timeout {
   125  				require.Error(t, err)
   126  				require.ErrorContains(t, err, "context deadline exceeded")
   127  			} else {
   128  				require.NoError(t, err)
   129  				require.NotEmpty(t, stream.Results())
   130  				require.GreaterOrEqual(t, stream.Results()[0].Metadata.DispatchCount, uint32(1))
   131  			}
   132  		})
   133  	}
   134  }
   135  
   136  func TestSecondaryDispatch(t *testing.T) {
   137  	for _, tc := range []struct {
   138  		name             string
   139  		expr             string
   140  		request          *v1.DispatchCheckRequest
   141  		primarySleepTime time.Duration
   142  		expectedResult   uint32
   143  	}{
   144  		{
   145  			"no multidispatch",
   146  			"['invalid']",
   147  			&v1.DispatchCheckRequest{
   148  				ResourceRelation: &corev1.RelationReference{
   149  					Namespace: "somenamespace",
   150  					Relation:  "somerelation",
   151  				},
   152  				ResourceIds: []string{"foo"},
   153  				Metadata:    &v1.ResolverMeta{DepthRemaining: 50},
   154  				Subject:     &corev1.ObjectAndRelation{Namespace: "foo", ObjectId: "bar", Relation: "..."},
   155  			},
   156  			0 * time.Millisecond,
   157  			1,
   158  		},
   159  		{
   160  			"basic multidispatch",
   161  			"['secondary']",
   162  			&v1.DispatchCheckRequest{
   163  				ResourceRelation: &corev1.RelationReference{
   164  					Namespace: "somenamespace",
   165  					Relation:  "somerelation",
   166  				},
   167  				ResourceIds: []string{"foo"},
   168  				Metadata:    &v1.ResolverMeta{DepthRemaining: 50},
   169  				Subject:     &corev1.ObjectAndRelation{Namespace: "foo", ObjectId: "bar", Relation: "..."},
   170  			},
   171  			1 * time.Second,
   172  			2,
   173  		},
   174  		{
   175  			"basic multidispatch, expr doesn't call secondary",
   176  			"['notconfigured']",
   177  			&v1.DispatchCheckRequest{
   178  				ResourceRelation: &corev1.RelationReference{
   179  					Namespace: "somenamespace",
   180  					Relation:  "somerelation",
   181  				},
   182  				ResourceIds: []string{"foo"},
   183  				Metadata:    &v1.ResolverMeta{DepthRemaining: 50},
   184  				Subject:     &corev1.ObjectAndRelation{Namespace: "foo", ObjectId: "bar", Relation: "..."},
   185  			},
   186  			1 * time.Second,
   187  			1,
   188  		},
   189  		{
   190  			"expr matches request",
   191  			"request.resource_relation.namespace == 'somenamespace' ? ['secondary'] : []",
   192  			&v1.DispatchCheckRequest{
   193  				ResourceRelation: &corev1.RelationReference{
   194  					Namespace: "somenamespace",
   195  					Relation:  "somerelation",
   196  				},
   197  				ResourceIds: []string{"foo"},
   198  				Metadata:    &v1.ResolverMeta{DepthRemaining: 50},
   199  				Subject:     &corev1.ObjectAndRelation{Namespace: "foo", ObjectId: "bar", Relation: "..."},
   200  			},
   201  			1 * time.Second,
   202  			2,
   203  		},
   204  		{
   205  			"expr does not match request",
   206  			"request.resource_relation.namespace == 'somenamespace' ? ['secondary'] : []",
   207  			&v1.DispatchCheckRequest{
   208  				ResourceRelation: &corev1.RelationReference{
   209  					Namespace: "someothernamespace",
   210  					Relation:  "somerelation",
   211  				},
   212  				ResourceIds: []string{"foo"},
   213  				Metadata:    &v1.ResolverMeta{DepthRemaining: 50},
   214  				Subject:     &corev1.ObjectAndRelation{Namespace: "foo", ObjectId: "bar", Relation: "..."},
   215  			},
   216  			1 * time.Second,
   217  			1,
   218  		},
   219  	} {
   220  		tc := tc
   221  		t.Run(tc.name, func(t *testing.T) {
   222  			conn := connectionForDispatching(t, &fakeDispatchSvc{dispatchCount: 1, sleepTime: tc.primarySleepTime})
   223  			secondaryConn := connectionForDispatching(t, &fakeDispatchSvc{dispatchCount: 2, sleepTime: 0 * time.Millisecond})
   224  
   225  			parsed, err := ParseDispatchExpression("check", tc.expr)
   226  			require.NoError(t, err)
   227  
   228  			dispatcher := NewClusterDispatcher(v1.NewDispatchServiceClient(conn), conn, ClusterDispatcherConfig{
   229  				KeyHandler:             &keys.DirectKeyHandler{},
   230  				DispatchOverallTimeout: 30 * time.Second,
   231  			}, map[string]SecondaryDispatch{
   232  				"secondary": {Name: "secondary", Client: v1.NewDispatchServiceClient(secondaryConn)},
   233  			}, map[string]*DispatchExpr{
   234  				"check": parsed,
   235  			})
   236  			require.True(t, dispatcher.ReadyState().IsReady)
   237  
   238  			resp, err := dispatcher.DispatchCheck(context.Background(), tc.request)
   239  			require.NoError(t, err)
   240  			require.Equal(t, tc.expectedResult, resp.Metadata.DispatchCount)
   241  		})
   242  	}
   243  }
   244  
   245  func connectionForDispatching(t *testing.T, svc v1.DispatchServiceServer) *grpc.ClientConn {
   246  	listener := bufconn.Listen(humanize.MiByte)
   247  	s := grpc.NewServer()
   248  
   249  	v1.RegisterDispatchServiceServer(s, svc)
   250  
   251  	go func() {
   252  		// Ignore any errors
   253  		_ = s.Serve(listener)
   254  	}()
   255  
   256  	conn, err := grpc.DialContext(
   257  		context.Background(),
   258  		"",
   259  		grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
   260  			return listener.Dial()
   261  		}),
   262  		grpc.WithTransportCredentials(insecure.NewCredentials()),
   263  		grpc.WithBlock(),
   264  	)
   265  	require.NoError(t, err)
   266  
   267  	t.Cleanup(func() {
   268  		conn.Close()
   269  		listener.Close()
   270  		s.Stop()
   271  	})
   272  
   273  	return conn
   274  }