Skip to content

Commit 1bc6b86

Browse files
committed
[spicedb] fix stale connection handling
1 parent 687f337 commit 1bc6b86

File tree

3 files changed

+54
-71
lines changed

3 files changed

+54
-71
lines changed

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

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export class SpiceDBAuthorizer {
2121
private readonly clientProvider: SpiceDBClientProvider,
2222
) {}
2323

24+
private get client(): v1.ZedPromiseClientInterface {
25+
return this.clientProvider.getClient();
26+
}
27+
2428
async check(
2529
req: v1.CheckPermissionRequest,
2630
experimentsFields: {
@@ -39,8 +43,7 @@ export class SpiceDBAuthorizer {
3943
const timer = spicedbClientLatency.startTimer();
4044
let error: Error | undefined;
4145
try {
42-
const client = this.clientProvider.getClient();
43-
const response = await client.checkPermission(req, this.callOptions);
46+
const response = await this.client.checkPermission(req, this.callOptions);
4447
const permitted = response.permissionship === v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION;
4548

4649
return permitted;
@@ -59,8 +62,7 @@ export class SpiceDBAuthorizer {
5962
const timer = spicedbClientLatency.startTimer();
6063
let error: Error | undefined;
6164
try {
62-
const client = this.clientProvider.getClient();
63-
const response = await client.writeRelationships(
65+
const response = await this.client.writeRelationships(
6466
v1.WriteRelationshipsRequest.create({
6567
updates,
6668
}),
@@ -81,11 +83,16 @@ export class SpiceDBAuthorizer {
8183
const timer = spicedbClientLatency.startTimer();
8284
let error: Error | undefined;
8385
try {
84-
const client = this.clientProvider.getClient();
85-
const existing = await client.readRelationships(v1.ReadRelationshipsRequest.create(req), this.callOptions);
86+
const existing = await this.client.readRelationships(
87+
v1.ReadRelationshipsRequest.create(req),
88+
this.callOptions,
89+
);
8690
if (existing.length > 0) {
87-
const response = await client.deleteRelationships(req, this.callOptions);
88-
const after = await client.readRelationships(v1.ReadRelationshipsRequest.create(req), this.callOptions);
91+
const response = await this.client.deleteRelationships(req, this.callOptions);
92+
const after = await this.client.readRelationships(
93+
v1.ReadRelationshipsRequest.create(req),
94+
this.callOptions,
95+
);
8996
if (after.length > 0) {
9097
log.error("[spicedb] Failed to delete relationships.", { existing, after, request: req });
9198
}
@@ -108,11 +115,7 @@ export class SpiceDBAuthorizer {
108115
}
109116

110117
async readRelationships(req: v1.ReadRelationshipsRequest): Promise<v1.ReadRelationshipsResponse[]> {
111-
const client = this.clientProvider.getClient();
112-
if (!client) {
113-
return [];
114-
}
115-
return client.readRelationships(req, this.callOptions);
118+
return this.client.readRelationships(req, this.callOptions);
116119
}
117120

118121
/**

components/server/src/authorization/spicedb.ts

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

77
import { v1 } from "@authzed/authzed-node";
8-
import { IClientCallMetrics, createClientCallMetricsInterceptor } from "@gitpod/gitpod-protocol/lib/util/grpc";
8+
import { defaultGRPCOptions } from "@gitpod/gitpod-protocol/lib/util/grpc";
99
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
1010
import * as grpc from "@grpc/grpc-js";
1111

12-
export const SpiceDBClientProvider = Symbol("SpiceDBClientProvider");
13-
1412
export interface SpiceDBClientConfig {
1513
address: string;
1614
token: string;
1715
}
1816

1917
export type SpiceDBClient = v1.ZedPromiseClientInterface;
2018
type Client = v1.ZedClientInterface & grpc.Client;
21-
export interface SpiceDBClientProvider {
22-
getClient(): SpiceDBClient;
23-
}
2419

2520
export function spiceDBConfigFromEnv(): SpiceDBClientConfig | undefined {
2621
const token = process.env["SPICEDB_PRESHARED_KEY"];
@@ -40,63 +35,41 @@ export function spiceDBConfigFromEnv(): SpiceDBClientConfig | undefined {
4035
};
4136
}
4237

43-
function spicedbClientFromConfig(config: SpiceDBClientConfig): Client {
44-
const clientOptions: grpc.ClientOptions = {
45-
"grpc.client_idle_timeout_ms": 10000, // this ensures a connection is not stuck in the "READY" state for too long. Required for the reconnect logic below.
46-
"grpc.max_reconnect_backoff_ms": 5000, // default: 12000
47-
};
48-
49-
return v1.NewClient(
50-
config.token,
51-
config.address,
52-
v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS,
53-
undefined,
54-
clientOptions,
55-
) as Client;
56-
}
57-
58-
export class CachingSpiceDBClientProvider implements SpiceDBClientProvider {
38+
export class SpiceDBClientProvider {
5939
private client: Client | undefined;
6040

61-
private readonly interceptors: grpc.Interceptor[] = [];
62-
6341
constructor(
64-
private readonly _clientConfig: SpiceDBClientConfig,
65-
private readonly clientCallMetrics?: IClientCallMetrics | undefined,
66-
) {
67-
if (this.clientCallMetrics) {
68-
this.interceptors.push(createClientCallMetricsInterceptor(this.clientCallMetrics));
69-
}
70-
}
42+
private readonly clientConfig: SpiceDBClientConfig,
43+
private readonly interceptors: grpc.Interceptor[] = [],
44+
) {}
7145

7246
getClient(): SpiceDBClient {
73-
let client = this.client;
74-
if (!client) {
75-
client = spicedbClientFromConfig(this.clientConfig);
76-
} else if (client.getChannel().getConnectivityState(true) !== grpc.connectivityState.READY) {
77-
// (gpl): We need this check and explicit re-connect because we observe a ~120s connection timeout without it.
78-
// It's not entirely clear where that timeout comes from, but likely from the underlying transport, that is not exposed by grpc/grpc-js
79-
client.close();
80-
81-
log.warn("[spicedb] Lost connection to SpiceDB - reconnecting...");
82-
83-
client = spicedbClientFromConfig(this.clientConfig);
47+
if (this.client) {
48+
const state = this.client.getChannel().getConnectivityState(true);
49+
if (state === grpc.connectivityState.TRANSIENT_FAILURE || state === grpc.connectivityState.SHUTDOWN) {
50+
log.warn("[spicedb] Lost connection to SpiceDB - reconnecting...");
51+
try {
52+
this.client.close();
53+
} catch (error) {
54+
log.error("[spicedb] Failed to close client", error);
55+
} finally {
56+
this.client = undefined;
57+
}
58+
}
8459
}
85-
this.client = client;
86-
87-
return client.promises;
88-
}
8960

90-
protected get clientConfig() {
91-
const config = this._clientConfig;
92-
if (this.interceptors) {
93-
return {
94-
...config,
95-
options: {
96-
interceptors: [...this.interceptors],
61+
if (!this.client) {
62+
this.client = v1.NewClient(
63+
this.clientConfig.token,
64+
this.clientConfig.address,
65+
v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS,
66+
undefined, //
67+
{
68+
...defaultGRPCOptions,
69+
interceptors: this.interceptors,
9770
},
98-
};
71+
) as Client;
9972
}
100-
return config;
73+
return this.client.promises;
10174
}
10275
}

components/server/src/container-module.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import { GitpodFileParser } from "@gitpod/gitpod-protocol/lib/gitpod-file-parser
1111
import { PrometheusClientCallMetrics } from "@gitpod/gitpod-protocol/lib/messaging/client-call-metrics";
1212
import { newAnalyticsWriterFromEnv } from "@gitpod/gitpod-protocol/lib/util/analytics";
1313
import { DebugApp } from "@gitpod/gitpod-protocol/lib/util/debug-app";
14-
import { IClientCallMetrics, defaultGRPCOptions } from "@gitpod/gitpod-protocol/lib/util/grpc";
14+
import {
15+
IClientCallMetrics,
16+
createClientCallMetricsInterceptor,
17+
defaultGRPCOptions,
18+
} from "@gitpod/gitpod-protocol/lib/util/grpc";
1519
import { prometheusClientMiddleware } from "@gitpod/gitpod-protocol/lib/util/nice-grpc";
1620
import { IDEServiceClient, IDEServiceDefinition } from "@gitpod/ide-service-api/lib/ide.pb";
1721
import { ImageBuilderClientCallMetrics, ImageBuilderClientProvider } from "@gitpod/image-builder/lib";
@@ -46,7 +50,7 @@ import { AuthJWT, SignInJWT } from "./auth/jwt";
4650
import { LoginCompletionHandler } from "./auth/login-completion-handler";
4751
import { VerificationService } from "./auth/verification-service";
4852
import { Authorizer, createInitializingAuthorizer } from "./authorization/authorizer";
49-
import { CachingSpiceDBClientProvider, SpiceDBClientProvider, spiceDBConfigFromEnv } from "./authorization/spicedb";
53+
import { SpiceDBClientProvider, spiceDBConfigFromEnv } from "./authorization/spicedb";
5054
import { BillingModes } from "./billing/billing-mode";
5155
import { EntitlementService, EntitlementServiceImpl } from "./billing/entitlement-service";
5256
import { EntitlementServiceUBP } from "./billing/entitlement-service-ubp";
@@ -309,7 +313,10 @@ export const productionContainerModule = new ContainerModule(
309313
throw new Error("[spicedb] Missing configuration expected in env vars!");
310314
}
311315
const clientCallMetrics = ctx.container.get<IClientCallMetrics>(IClientCallMetrics);
312-
return new CachingSpiceDBClientProvider(config, clientCallMetrics);
316+
return new SpiceDBClientProvider(
317+
config, //
318+
[createClientCallMetricsInterceptor(clientCallMetrics)],
319+
);
313320
})
314321
.inSingletonScope();
315322
bind(SpiceDBAuthorizer).toSelf().inSingletonScope();

0 commit comments

Comments
 (0)