Skip to content

Commit 6ed8284

Browse files
committed
[server] Reconnect to spicedb without waiting 2 mins (+ fail on missing config)
1 parent 0c3eb9f commit 6ed8284

File tree

8 files changed

+124
-52
lines changed

8 files changed

+124
-52
lines changed

components/server/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"/src"
3535
],
3636
"dependencies": {
37-
"@authzed/authzed-node": "^0.10.0",
37+
"@authzed/authzed-node": "^0.12.1",
3838
"@bufbuild/connect": "^0.8.1",
3939
"@bufbuild/connect-express": "^0.8.1",
4040
"@gitbeaker/node": "^35.7.0",
@@ -66,6 +66,7 @@
6666
"express-http-proxy": "^1.0.7",
6767
"fs-extra": "^10.0.0",
6868
"google-protobuf": "^3.19.1",
69+
"@grpc/grpc-js_1_9_0": "npm:@grpc/[email protected]",
6970
"inversify": "^6.0.1",
7071
"ioredis": "^5.3.2",
7172
"ioredis-mock": "^8.7.0",

components/server/src/authorization/authorizer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ export class Authorizer {
475475
optionalSubjectId: relation.subject.object.objectId,
476476
},
477477
},
478+
optionalLimit: 0,
478479
});
479480
if (relationships.length === 0) {
480481
return undefined;
@@ -499,6 +500,7 @@ export class Authorizer {
499500
optionalSubjectId: relation.subject.object.objectId,
500501
},
501502
},
503+
optionalLimit: 0,
502504
});
503505
return relationships.map((r) => r.relationship!);
504506
}

components/server/src/authorization/spicedb-authorizer.ts

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ import { TrustedValue } from "@gitpod/gitpod-protocol/lib/util/scrubbing";
1111
import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
1212
import { inject, injectable } from "inversify";
1313
import { observeSpicedbClientLatency, spicedbClientLatency } from "../prometheus-metrics";
14-
import { SpiceDBClient } from "./spicedb";
14+
import { SpiceDBClientProvider } from "./spicedb";
15+
// TODO(gpl) Change to "@grpc/grpc-js" once we can use 1.9.0 (or higher) everywhere
16+
import * as grpc from "@grpc/grpc-js_1_9_0";
1517

1618
@injectable()
1719
export class SpiceDBAuthorizer {
1820
constructor(
19-
@inject(SpiceDBClient)
20-
private client: SpiceDBClient,
21+
@inject(SpiceDBClientProvider)
22+
private readonly clientProvider: SpiceDBClientProvider,
2123
) {}
2224

2325
async check(
@@ -26,10 +28,6 @@ export class SpiceDBAuthorizer {
2628
userId: string;
2729
},
2830
): Promise<boolean> {
29-
if (!this.client) {
30-
return true;
31-
}
32-
3331
const featureEnabled = await getExperimentsClientForBackend().getValueAsync("centralizedPermissions", false, {
3432
user: {
3533
id: experimentsFields.userId,
@@ -42,7 +40,8 @@ export class SpiceDBAuthorizer {
4240
const timer = spicedbClientLatency.startTimer();
4341
let error: Error | undefined;
4442
try {
45-
const response = await this.client.checkPermission(req);
43+
const client = this.clientProvider.getClient();
44+
const response = await client.checkPermission(req, this.callOptions);
4645
const permitted = response.permissionship === v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION;
4746

4847
return permitted;
@@ -58,17 +57,15 @@ export class SpiceDBAuthorizer {
5857
}
5958

6059
async writeRelationships(...updates: v1.RelationshipUpdate[]): Promise<v1.WriteRelationshipsResponse | undefined> {
61-
if (!this.client) {
62-
return undefined;
63-
}
64-
6560
const timer = spicedbClientLatency.startTimer();
6661
let error: Error | undefined;
6762
try {
68-
const response = await this.client.writeRelationships(
63+
const client = this.clientProvider.getClient();
64+
const response = await client.writeRelationships(
6965
v1.WriteRelationshipsRequest.create({
7066
updates,
7167
}),
68+
this.callOptions,
7269
);
7370
log.info("[spicedb] Successfully wrote relationships.", { response, updates });
7471

@@ -82,17 +79,14 @@ export class SpiceDBAuthorizer {
8279
}
8380

8481
async deleteRelationships(req: v1.DeleteRelationshipsRequest): Promise<v1.ReadRelationshipsResponse[]> {
85-
if (!this.client) {
86-
return [];
87-
}
88-
8982
const timer = spicedbClientLatency.startTimer();
9083
let error: Error | undefined;
9184
try {
92-
const existing = await this.client.readRelationships(v1.ReadRelationshipsRequest.create(req));
85+
const client = this.clientProvider.getClient();
86+
const existing = await client.readRelationships(v1.ReadRelationshipsRequest.create(req), this.callOptions);
9387
if (existing.length > 0) {
94-
const response = await this.client.deleteRelationships(req);
95-
const after = await this.client.readRelationships(v1.ReadRelationshipsRequest.create(req));
88+
const response = await client.deleteRelationships(req, this.callOptions);
89+
const after = await client.readRelationships(v1.ReadRelationshipsRequest.create(req), this.callOptions);
9690
if (after.length > 0) {
9791
log.error("[spicedb] Failed to delete relationships.", { existing, after, request: req });
9892
}
@@ -115,9 +109,22 @@ export class SpiceDBAuthorizer {
115109
}
116110

117111
async readRelationships(req: v1.ReadRelationshipsRequest): Promise<v1.ReadRelationshipsResponse[]> {
118-
if (!this.client) {
112+
const client = this.clientProvider.getClient();
113+
if (!client) {
119114
return [];
120115
}
121-
return this.client.readRelationships(req);
116+
return client.readRelationships(req, this.callOptions);
117+
}
118+
119+
/**
120+
* permission_service.grpc-client.d.ts has all methods overloaded with this pattern:
121+
* - xyzRelationships(input: Request, metadata?: grpc.Metadata | grpc.CallOptions, options?: grpc.CallOptions): grpc.ClientReadableStream<ReadRelationshipsResponse>;
122+
* But the promisified client somehow does not have the same overloads. Thus we convince it here that options may be passed as 2nd argument.
123+
*/
124+
private get callOptions(): grpc.Metadata {
125+
return (<grpc.CallOptions>{
126+
deadline: Date.now() + 5000,
127+
credentials: grpc.CallCredentials.createEmpty(),
128+
}) as any as grpc.Metadata;
122129
}
123130
}

components/server/src/authorization/spicedb.ts

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,22 @@
66

77
import { v1 } from "@authzed/authzed-node";
88
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
9+
import * as grpc from "@grpc/grpc-js_1_9_0";
910

10-
export const SpiceDBClient = Symbol("SpiceDBClient");
11-
export type SpiceDBClient = v1.ZedPromiseClientInterface | undefined;
11+
export const SpiceDBClientProvider = Symbol("SpiceDBClientProvider");
1212

13-
export function spicedbClientFromEnv(): SpiceDBClient {
13+
export interface SpiceDBClientConfig {
14+
address: string;
15+
token: string;
16+
}
17+
18+
export type SpiceDBClient = v1.ZedPromiseClientInterface;
19+
type Client = v1.ZedClientInterface & grpc.Client;
20+
export interface SpiceDBClientProvider {
21+
getClient(): SpiceDBClient;
22+
}
23+
24+
export function spiceDBConfigFromEnv(): SpiceDBClientConfig | undefined {
1425
const token = process.env["SPICEDB_PRESHARED_KEY"];
1526
if (!token) {
1627
log.error("[spicedb] No preshared key configured.");
@@ -22,6 +33,47 @@ export function spicedbClientFromEnv(): SpiceDBClient {
2233
log.error("[spicedb] No service address configured.");
2334
return undefined;
2435
}
36+
return {
37+
address,
38+
token,
39+
};
40+
}
41+
42+
function spicedbClientFromConfig(config: SpiceDBClientConfig): Client {
43+
const clientOptions: grpc.ClientOptions = {
44+
"grpc.client_idle_timeout_ms": 60000,
45+
"grpc.max_reconnect_backoff_ms": 5000, // default: 12000
46+
};
47+
48+
return v1.NewClient(
49+
config.token,
50+
config.address,
51+
v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS,
52+
undefined,
53+
clientOptions,
54+
) as Client;
55+
}
56+
57+
export class CachingSpiceDBClientProvider implements SpiceDBClientProvider {
58+
private client: Client | undefined;
59+
60+
constructor(private readonly clientConfig: SpiceDBClientConfig) {}
61+
62+
getClient(): SpiceDBClient {
63+
let client = this.client;
64+
if (!client) {
65+
client = spicedbClientFromConfig(this.clientConfig);
66+
} else if (client.getChannel().getConnectivityState(true) !== grpc.connectivityState.READY) {
67+
// (gpl): We need this check and explicit re-connect because we observe a ~120s connection timeout without it.
68+
// It's not entirely clear where that timeout comes from, but likely from the underlying transport, that is not exposed by grpc/grpc-js
69+
client.close();
2570

26-
return v1.NewClient(token, address, v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS).promises;
71+
log.warn("[spicedb] Lost connection to SpiceDB - reconnecting...");
72+
73+
client = spicedbClientFromConfig(this.clientConfig);
74+
}
75+
this.client = client;
76+
77+
return client.promises;
78+
}
2779
}

components/server/src/container-module.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import { AuthJWT, SignInJWT } from "./auth/jwt";
4646
import { LoginCompletionHandler } from "./auth/login-completion-handler";
4747
import { VerificationService } from "./auth/verification-service";
4848
import { Authorizer, createInitializingAuthorizer } from "./authorization/authorizer";
49-
import { SpiceDBClient, spicedbClientFromEnv } from "./authorization/spicedb";
49+
import { CachingSpiceDBClientProvider, SpiceDBClientProvider, spiceDBConfigFromEnv } from "./authorization/spicedb";
5050
import { BillingModes } from "./billing/billing-mode";
5151
import { EntitlementService, EntitlementServiceImpl } from "./billing/entitlement-service";
5252
import { EntitlementServiceUBP } from "./billing/entitlement-service-ubp";
@@ -302,8 +302,14 @@ export const productionContainerModule = new ContainerModule(
302302
bind(IamSessionApp).toSelf().inSingletonScope();
303303

304304
// Authorization & Perms
305-
bind(SpiceDBClient)
306-
.toDynamicValue(() => spicedbClientFromEnv())
305+
bind(SpiceDBClientProvider)
306+
.toDynamicValue((ctx) => {
307+
const config = spiceDBConfigFromEnv();
308+
if (!config) {
309+
throw new Error("[spicedb] Missing configuration expected in env vars!");
310+
}
311+
return new CachingSpiceDBClientProvider(config);
312+
})
307313
.inSingletonScope();
308314
bind(SpiceDBAuthorizer).toSelf().inSingletonScope();
309315
bind(Authorizer)

components/server/src/test/service-testing-container-module.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*/
66

77
import * as grpc from "@grpc/grpc-js";
8-
import { v1 } from "@authzed/authzed-node";
98
import { IAnalyticsWriter, NullAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics";
109
import { IDEServiceClient, IDEServiceDefinition } from "@gitpod/ide-service-api/lib/ide.pb";
1110
import { UsageServiceDefinition } from "@gitpod/usage-api/lib/usage/v1/usage.pb";
@@ -14,7 +13,7 @@ import { v4 } from "uuid";
1413
import { AuthProviderParams } from "../auth/auth-provider";
1514
import { HostContextProvider, HostContextProviderFactory } from "../auth/host-context-provider";
1615
import { HostContextProviderImpl } from "../auth/host-context-provider-impl";
17-
import { SpiceDBClient } from "../authorization/spicedb";
16+
import { CachingSpiceDBClientProvider, SpiceDBClientProvider } from "../authorization/spicedb";
1817
import { Config } from "../config";
1918
import { StorageClient } from "../storage/storage-client";
2019
import { testContainer } from "@gitpod/gitpod-db/lib";
@@ -189,10 +188,13 @@ const mockApplyingContainerModule = new ContainerModule((bind, unbound, isbound,
189188
}))
190189
.inSingletonScope();
191190

192-
rebind(SpiceDBClient)
191+
rebind(SpiceDBClientProvider)
193192
.toDynamicValue(() => {
194-
const token = v4();
195-
return v1.NewClient(token, "localhost:50051", v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS).promises;
193+
const config = {
194+
token: v4(),
195+
address: "localhost:50051",
196+
};
197+
return new CachingSpiceDBClientProvider(config);
196198
})
197199
.inSingletonScope();
198200
});

install/installer/pkg/components/spicedb/deployment.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,12 @@ func deployment(ctx *common.RenderContext) ([]runtime.Object, error) {
155155
},
156156
},
157157
InitialDelaySeconds: 5,
158-
PeriodSeconds: 30,
159-
FailureThreshold: 5,
160-
SuccessThreshold: 1,
161-
TimeoutSeconds: 3,
158+
// try again every 10 seconds
159+
PeriodSeconds: 10,
160+
// fail after 6 * 10 + 5 = 65 seconds
161+
FailureThreshold: 6,
162+
SuccessThreshold: 1,
163+
TimeoutSeconds: 3,
162164
},
163165
VolumeMounts: []v1.VolumeMount{
164166
bootstrapVolumeMount,

yarn.lock

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
# yarn lockfile v1
33

44

5-
"@authzed/authzed-node@^0.10.0":
6-
version "0.10.0"
7-
resolved "https://registry.yarnpkg.com/@authzed/authzed-node/-/authzed-node-0.10.0.tgz#623e4911fde221bb526e7f2e9ca335d9f3b9072d"
8-
integrity sha512-TnAnatcU5dHvyGqrWoZzPNaO1opPpVU1y7P5LrJsV2j54y0xvx/OFhYtfeguMxHSz2kpbdCuIvIKJuB8WFbRRA==
5+
"@authzed/authzed-node@^0.12.1":
6+
version "0.12.1"
7+
resolved "https://registry.yarnpkg.com/@authzed/authzed-node/-/authzed-node-0.12.1.tgz#0c28395a64f9b1ecf33faf67259e32a9a3bce300"
8+
integrity sha512-BVHLaPfiHQw1Vz+199m9i4xltT3YyFhqVHtkYPIQ28q8a7iJpnXmFRZIWuTMJcxJI01wtAxJYFuRJq3ktFe6qw==
99
dependencies:
10-
"@grpc/grpc-js" "^1.2.8"
10+
"@grpc/grpc-js" "^1.8.3"
1111
"@protobuf-ts/runtime" "^2.8.1"
1212
"@protobuf-ts/runtime-rpc" "^2.8.1"
1313
google-protobuf "^3.15.3"
@@ -1407,14 +1407,6 @@
14071407
"@grpc/proto-loader" "^0.7.0"
14081408
"@types/node" ">=12.12.47"
14091409

1410-
"@grpc/grpc-js@^1.2.8":
1411-
version "1.8.7"
1412-
resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.8.7.tgz#2154fc0134462ad45f4134e8b54682a25ed05956"
1413-
integrity sha512-dRAWjRFN1Zy9mzPNLkFFIWT8T6C9euwluzCHZUKuhC+Bk3MayNPcpgDRyG+sg+n2sitEUySKxUynirVpu9ItKw==
1414-
dependencies:
1415-
"@grpc/proto-loader" "^0.7.0"
1416-
"@types/node" ">=12.12.47"
1417-
14181410
"@grpc/grpc-js@^1.6.1":
14191411
version "1.7.0"
14201412
resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.7.0.tgz#5a96bdbe51cce23faa38a4db6e43595a5c584849"
@@ -1423,6 +1415,14 @@
14231415
"@grpc/proto-loader" "^0.7.0"
14241416
"@types/node" ">=12.12.47"
14251417

1418+
"@grpc/grpc-js@^1.8.3", "@grpc/grpc-js_1_9_0@npm:@grpc/[email protected]":
1419+
version "1.9.0"
1420+
resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.0.tgz#bdb599e339adabb16aa7243e70c311f75a572867"
1421+
integrity sha512-H8+iZh+kCE6VR/Krj6W28Y/ZlxoZ1fOzsNt77nrdE3knkbSelW1Uus192xOFCxHyeszLj8i4APQkSIXjAoOxXg==
1422+
dependencies:
1423+
"@grpc/proto-loader" "^0.7.0"
1424+
"@types/node" ">=12.12.47"
1425+
14261426
"@grpc/proto-loader@^0.7.0":
14271427
version "0.7.2"
14281428
resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.2.tgz#fa63178853afe1473c50cff89fe572f7c8b20154"

0 commit comments

Comments
 (0)