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" ] ;
@@ -36,49 +66,103 @@ export function spiceDBConfigFromEnv(): SpiceDBClientConfig | undefined {
36
66
37
67
export class SpiceDBClientProvider {
38
68
private client : Client | 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
+ . then ( ( ) => { } )
79
+ . catch ( ( e ) => {
80
+ log . error ( "[spicedb] Failed to reconcile client options" , e ) ;
81
+ } ) ;
82
+ }
44
83
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 (
48
138
this . clientConfig . token ,
49
139
this . clientConfig . address ,
50
140
v1 . ClientSecurity . INSECURE_PLAINTEXT_CREDENTIALS ,
51
- undefined , //
141
+ undefined ,
52
142
{
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 ,
78
156
interceptors : this . interceptors ,
79
157
} ,
80
158
) as Client ;
81
159
}
160
+ }
161
+
162
+ getClient ( ) : SpiceDBClient {
163
+ if ( ! this . client ) {
164
+ this . client = this . createClient ( this . clientOptions ) ;
165
+ }
82
166
return this . client . promises ;
83
167
}
84
168
}
0 commit comments