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 }