7
7
import { v1 } from "@authzed/authzed-node" ;
8
8
import { log } from "@gitpod/gitpod-protocol/lib/util/logging" ;
9
9
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" ;
10
12
11
13
export interface SpiceDBClientConfig {
12
14
address : string ;
@@ -15,6 +17,34 @@ export interface SpiceDBClientConfig {
15
17
16
18
export type SpiceDBClient = v1 . ZedPromiseClientInterface ;
17
19
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
+ } ;
18
48
19
49
export function spiceDBConfigFromEnv ( ) : SpiceDBClientConfig | undefined {
20
50
const token = process . env [ "SPICEDB_PRESHARED_KEY" ] ;
@@ -35,49 +65,105 @@ export function spiceDBConfigFromEnv(): SpiceDBClientConfig | undefined {
35
65
}
36
66
37
67
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 ;
39
71
40
72
constructor (
41
73
private readonly clientConfig : SpiceDBClientConfig ,
42
74
private readonly interceptors : grpc . Interceptor [ ] = [ ] ,
43
- ) { }
75
+ ) {
76
+ this . clientOptions = DefaultClientOptions ;
77
+ this . reconcileClientOptions ( ) ;
78
+ }
44
79
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 (
48
140
this . clientConfig . token ,
49
141
this . clientConfig . address ,
50
142
v1 . ClientSecurity . INSECURE_PLAINTEXT_CREDENTIALS ,
51
- undefined , //
143
+ undefined ,
52
144
{
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 ,
78
146
interceptors : this . interceptors ,
79
147
} ,
80
148
) 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 ) ;
81
167
}
82
168
return this . client . promises ;
83
169
}
0 commit comments