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_VAULE = "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" ] ;
@@ -36,48 +66,80 @@ export function spiceDBConfigFromEnv(): SpiceDBClientConfig | undefined {
3666
3767export class SpiceDBClientProvider {
3868 private client : Client | undefined ;
69+ private previousClientOptionsString : string = DEFAULT_FEATURE_FLAG_VAULE ;
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+ . then ( ( ) => { } )
79+ . catch ( ( e ) => {
80+ log . error ( "[spicedb] Failed to reconcile client options" , e ) ;
81+ } ) ;
82+ }
4483
45- getClient ( ) : SpiceDBClient {
46- if ( ! this . client ) {
47- this . client = v1 . NewClient (
48- this . clientConfig . token ,
49- this . clientConfig . address ,
50- v1 . ClientSecurity . INSECURE_PLAINTEXT_CREDENTIALS ,
51- undefined , //
52- {
53- // we ping frequently to check if the connection is still alive
54- "grpc.keepalive_time_ms" : 1000 ,
55- "grpc.keepalive_timeout_ms" : 1000 ,
84+ private async reconcileClientOptions ( ) {
85+ const doReconcileClientOptions = async ( ) => {
86+ const customClientOptions = await getExperimentsClientForBackend ( ) . getValueAsync (
87+ "spicedb_client_options" ,
88+ DEFAULT_FEATURE_FLAG_VAULE ,
89+ { } ,
90+ ) ;
91+ if ( customClientOptions === this . previousClientOptionsString ) {
92+ return ;
93+ }
94+ try {
95+ let clientOptions = DefaultClientOptions ;
96+ if ( customClientOptions && customClientOptions != DEFAULT_FEATURE_FLAG_VAULE ) {
97+ clientOptions = JSON . parse ( customClientOptions ) ;
98+ }
99+ if ( this . client != null ) {
100+ const newClient = this . createClient ( clientOptions ) ;
101+ const oldClient = this . client ;
102+ this . client = newClient ;
56103
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
104+ log . info ( "[spicedb] client options changes" , {
105+ clientOptions : new TrustedValue ( clientOptions ) ,
106+ } ) ;
74107
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 ,
78- interceptors : this . interceptors ,
79- } ,
80- ) as Client ;
108+ setTimeout ( ( ) => {
109+ oldClient . close ( ) ;
110+ } , 10 * 1000 ) ;
111+ }
112+ this . clientOptions = clientOptions ;
113+ this . previousClientOptionsString = customClientOptions ;
114+ } catch ( e ) {
115+ log . error ( "[spicedb] Failed to parse custom client options" , e ) ;
116+ }
117+ } ;
118+ while ( true ) {
119+ await doReconcileClientOptions ( ) ;
120+ await new Promise ( ( resolve ) => setTimeout ( resolve , 60 * 1000 ) ) ;
121+ }
122+ }
123+
124+ private createClient ( clientOptions : grpc . ClientOptions ) : Client {
125+ log . debug ( "[spicedb] creating client" , {
126+ clientOptions : new TrustedValue ( clientOptions ) ,
127+ } ) ;
128+ return v1 . NewClient (
129+ this . clientConfig . token ,
130+ this . clientConfig . address ,
131+ v1 . ClientSecurity . INSECURE_PLAINTEXT_CREDENTIALS ,
132+ undefined , //
133+ {
134+ ...clientOptions ,
135+ interceptors : this . interceptors ,
136+ } ,
137+ ) as Client ;
138+ }
139+
140+ getClient ( ) : SpiceDBClient {
141+ if ( ! this . client ) {
142+ this . client = this . createClient ( this . clientOptions ) ;
81143 }
82144 return this . client . promises ;
83145 }
0 commit comments