77import { v1 } from "@authzed/authzed-node" ;
88import { log } from "@gitpod/gitpod-protocol/lib/util/logging" ;
99import * 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
1113export interface SpiceDBClientConfig {
1214 address : string ;
@@ -15,6 +17,34 @@ export interface SpiceDBClientConfig {
1517
1618export type SpiceDBClient = v1 . ZedPromiseClientInterface ;
1719type 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
1949export function spiceDBConfigFromEnv ( ) : SpiceDBClientConfig | undefined {
2050 const token = process . env [ "SPICEDB_PRESHARED_KEY" ] ;
@@ -35,49 +65,105 @@ export function spiceDBConfigFromEnv(): SpiceDBClientConfig | undefined {
3565}
3666
3767export 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