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" ] ;
@@ -36,49 +66,103 @@ export function spiceDBConfigFromEnv(): SpiceDBClientConfig | undefined {
3666
3767export class SpiceDBClientProvider {
3868 private client : Client | 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+ . 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 (
84+ private async reconcileClientOptions ( ) {
85+ const doReconcileClientOptions = async ( ) => {
86+ try {
87+ const customClientOptions = await getExperimentsClientForBackend ( ) . getValueAsync (
88+ "spicedb_client_options" ,
89+ DEFAULT_FEATURE_FLAG_VALUE ,
90+ { } ,
91+ ) ;
92+ if ( customClientOptions === this . previousClientOptionsString ) {
93+ return ;
94+ }
95+ let clientOptions = DefaultClientOptions ;
96+ if ( customClientOptions && customClientOptions != DEFAULT_FEATURE_FLAG_VALUE ) {
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 ;
103+
104+ log . info ( "[spicedb] Client options changes" , {
105+ clientOptions : new TrustedValue ( clientOptions ) ,
106+ } ) ;
107+
108+ setTimeout ( ( ) => {
109+ this . closeClient ( oldClient ) ;
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 closeClient ( client : Client ) {
125+ try {
126+ client . close ( ) ;
127+ } catch ( error ) {
128+ log . error ( "[spicedb] Error closing client" , error ) ;
129+ }
130+ }
131+
132+ private createClient ( clientOptions : grpc . ClientOptions ) : Client {
133+ log . debug ( "[spicedb] Creating client" , {
134+ clientOptions : new TrustedValue ( clientOptions ) ,
135+ } ) ;
136+ try {
137+ return v1 . NewClient (
48138 this . clientConfig . token ,
49139 this . clientConfig . address ,
50140 v1 . ClientSecurity . INSECURE_PLAINTEXT_CREDENTIALS ,
51- undefined , //
141+ undefined ,
52142 {
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 ,
143+ ...clientOptions ,
144+ interceptors : this . interceptors ,
145+ } ,
146+ ) as Client ;
147+ } catch ( error ) {
148+ log . error ( "[spicedb] Error create client, fallback to default options" , error ) ;
149+ return v1 . NewClient (
150+ this . clientConfig . token ,
151+ this . clientConfig . address ,
152+ v1 . ClientSecurity . INSECURE_PLAINTEXT_CREDENTIALS ,
153+ undefined ,
154+ {
155+ ...DefaultClientOptions ,
78156 interceptors : this . interceptors ,
79157 } ,
80158 ) as Client ;
81159 }
160+ }
161+
162+ getClient ( ) : SpiceDBClient {
163+ if ( ! this . client ) {
164+ this . client = this . createClient ( this . clientOptions ) ;
165+ }
82166 return this . client . promises ;
83167 }
84168}
0 commit comments