Skip to content

Commit f5eda87

Browse files
mustard-mhgeropl
andauthored
[server] add feature flags for spicedb client options (#20613)
* add feature flags for spicedb client options Tool: gitpod/catfood.gitpod.cloud * Add comments Tool: gitpod/catfood.gitpod.cloud * fixup Tool: gitpod/catfood.gitpod.cloud * address feedback Co-authored-by: Gero Posmyk-Leinemann <[email protected]> Tool: gitpod/catfood.gitpod.cloud * fixup Tool: gitpod/catfood.gitpod.cloud --------- Co-authored-by: Gero Posmyk-Leinemann <[email protected]>
1 parent 1bc35eb commit f5eda87

File tree

1 file changed

+117
-31
lines changed

1 file changed

+117
-31
lines changed

components/server/src/authorization/spicedb.ts

+117-31
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import { v1 } from "@authzed/authzed-node";
88
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
99
import * as grpc from "@grpc/grpc-js";
10+
import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
11+
import { TrustedValue } from "@gitpod/gitpod-protocol/lib/util/scrubbing";
1012

1113
export interface SpiceDBClientConfig {
1214
address: string;
@@ -15,6 +17,34 @@ export interface SpiceDBClientConfig {
1517

1618
export type SpiceDBClient = v1.ZedPromiseClientInterface;
1719
type Client = v1.ZedClientInterface & grpc.Client;
20+
const DEFAULT_FEATURE_FLAG_VALUE = "undefined";
21+
const DefaultClientOptions: grpc.ClientOptions = {
22+
// we ping frequently to check if the connection is still alive
23+
"grpc.keepalive_time_ms": 1000,
24+
"grpc.keepalive_timeout_ms": 1000,
25+
26+
"grpc.max_reconnect_backoff_ms": 5000,
27+
"grpc.initial_reconnect_backoff_ms": 500,
28+
"grpc.service_config": JSON.stringify({
29+
methodConfig: [
30+
{
31+
name: [{}],
32+
retryPolicy: {
33+
maxAttempts: 10,
34+
initialBackoff: "0.1s",
35+
maxBackoff: "5s",
36+
backoffMultiplier: 2.0,
37+
retryableStatusCodes: ["UNAVAILABLE", "DEADLINE_EXCEEDED"],
38+
},
39+
},
40+
],
41+
}),
42+
"grpc.enable_retries": 1, //TODO enabled by default
43+
44+
// Governs how log DNS resolution results are cached (at minimum!)
45+
// default is 30s, which is too long for us during rollouts (where service DNS entries are updated)
46+
"grpc.dns_min_time_between_resolutions_ms": 2000,
47+
};
1848

1949
export function spiceDBConfigFromEnv(): SpiceDBClientConfig | undefined {
2050
const token = process.env["SPICEDB_PRESHARED_KEY"];
@@ -35,49 +65,105 @@ export function spiceDBConfigFromEnv(): SpiceDBClientConfig | undefined {
3565
}
3666

3767
export class SpiceDBClientProvider {
38-
private client: Client | undefined;
68+
private client: Client | undefined = undefined;
69+
private previousClientOptionsString: string = DEFAULT_FEATURE_FLAG_VALUE;
70+
private clientOptions: grpc.ClientOptions;
3971

4072
constructor(
4173
private readonly clientConfig: SpiceDBClientConfig,
4274
private readonly interceptors: grpc.Interceptor[] = [],
43-
) {}
75+
) {
76+
this.clientOptions = DefaultClientOptions;
77+
this.reconcileClientOptions();
78+
}
4479

45-
getClient(): SpiceDBClient {
46-
if (!this.client) {
47-
this.client = v1.NewClient(
80+
private reconcileClientOptions(): void {
81+
const doReconcileClientOptions = async () => {
82+
const customClientOptions = await getExperimentsClientForBackend().getValueAsync(
83+
"spicedb_client_options",
84+
DEFAULT_FEATURE_FLAG_VALUE,
85+
{},
86+
);
87+
if (customClientOptions === this.previousClientOptionsString) {
88+
return;
89+
}
90+
let clientOptions = DefaultClientOptions;
91+
if (customClientOptions && customClientOptions != DEFAULT_FEATURE_FLAG_VALUE) {
92+
clientOptions = JSON.parse(customClientOptions);
93+
}
94+
if (this.client !== undefined) {
95+
const newClient = this.createClient(clientOptions);
96+
const oldClient = this.client;
97+
this.client = newClient;
98+
99+
log.info("[spicedb] Client options changes", {
100+
clientOptions: new TrustedValue(clientOptions),
101+
});
102+
103+
// close client after 2 minutes to make sure most pending requests on the previous client are finished.
104+
setTimeout(() => {
105+
this.closeClient(oldClient);
106+
}, 2 * 60 * 1000);
107+
}
108+
this.clientOptions = clientOptions;
109+
// `createClient` will use the `DefaultClientOptions` to create client if the value on Feature Flag is not able to create a client
110+
// but we will still write `previousClientOptionsString` here to prevent retry loops.
111+
this.previousClientOptionsString = customClientOptions;
112+
};
113+
// eslint-disable-next-line no-void
114+
void (async () => {
115+
while (true) {
116+
try {
117+
await doReconcileClientOptions();
118+
await new Promise((resolve) => setTimeout(resolve, 60 * 1000));
119+
} catch (e) {
120+
log.error("[spicedb] Failed to reconcile client options", e);
121+
}
122+
}
123+
})();
124+
}
125+
126+
private closeClient(client: Client) {
127+
try {
128+
client.close();
129+
} catch (error) {
130+
log.error("[spicedb] Error closing client", error);
131+
}
132+
}
133+
134+
private createClient(clientOptions: grpc.ClientOptions): Client {
135+
log.debug("[spicedb] Creating client", {
136+
clientOptions: new TrustedValue(clientOptions),
137+
});
138+
try {
139+
return v1.NewClient(
48140
this.clientConfig.token,
49141
this.clientConfig.address,
50142
v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS,
51-
undefined, //
143+
undefined,
52144
{
53-
// we ping frequently to check if the connection is still alive
54-
"grpc.keepalive_time_ms": 1000,
55-
"grpc.keepalive_timeout_ms": 1000,
56-
57-
"grpc.max_reconnect_backoff_ms": 5000,
58-
"grpc.initial_reconnect_backoff_ms": 500,
59-
"grpc.service_config": JSON.stringify({
60-
methodConfig: [
61-
{
62-
name: [{}],
63-
retryPolicy: {
64-
maxAttempts: 10,
65-
initialBackoff: "0.1s",
66-
maxBackoff: "5s",
67-
backoffMultiplier: 2.0,
68-
retryableStatusCodes: ["UNAVAILABLE", "DEADLINE_EXCEEDED"],
69-
},
70-
},
71-
],
72-
}),
73-
"grpc.enable_retries": 1, //TODO enabled by default
74-
75-
// Governs how log DNS resolution results are cached (at minimum!)
76-
// default is 30s, which is too long for us during rollouts (where service DNS entries are updated)
77-
"grpc.dns_min_time_between_resolutions_ms": 2000,
145+
...clientOptions,
78146
interceptors: this.interceptors,
79147
},
80148
) as Client;
149+
} catch (error) {
150+
log.error("[spicedb] Error create client, fallback to default options", error);
151+
return v1.NewClient(
152+
this.clientConfig.token,
153+
this.clientConfig.address,
154+
v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS,
155+
undefined,
156+
{
157+
...DefaultClientOptions,
158+
interceptors: this.interceptors,
159+
},
160+
) as Client;
161+
}
162+
}
163+
164+
getClient(): SpiceDBClient {
165+
if (!this.client) {
166+
this.client = this.createClient(this.clientOptions);
81167
}
82168
return this.client.promises;
83169
}

0 commit comments

Comments
 (0)