@@ -14,6 +14,30 @@ import { SpiceDBClientProvider } from "./spicedb";
14
14
import * as grpc from "@grpc/grpc-js" ;
15
15
import { isFgaChecksEnabled } from "./authorizer" ;
16
16
17
+ async function tryThree < T > ( errMessage : string , code : ( attempt : number ) => Promise < T > ) : Promise < T > {
18
+ let attempt = 0 ;
19
+ // we do sometimes see INTERNAL errors from SpiceDB, so we retry a few times
20
+ // last time we checked it was 15 times per day (check logs)
21
+ while ( attempt ++ < 3 ) {
22
+ try {
23
+ return await code ( attempt ) ;
24
+ } catch ( err ) {
25
+ if ( err . code === grpc . status . INTERNAL && attempt < 3 ) {
26
+ log . warn ( errMessage , err , {
27
+ attempt,
28
+ } ) ;
29
+ } else {
30
+ log . error ( errMessage , err , {
31
+ attempt,
32
+ } ) ;
33
+ // we don't try again on other errors
34
+ throw err ;
35
+ }
36
+ }
37
+ }
38
+ throw new Error ( "unreachable" ) ;
39
+ }
40
+
17
41
@injectable ( )
18
42
export class SpiceDBAuthorizer {
19
43
constructor (
@@ -35,11 +59,12 @@ export class SpiceDBAuthorizer {
35
59
if ( ! featureEnabled ) {
36
60
return true ;
37
61
}
38
-
39
62
const timer = spicedbClientLatency . startTimer ( ) ;
40
63
let error : Error | undefined ;
41
64
try {
42
- const response = await this . client . checkPermission ( req , this . callOptions ) ;
65
+ const response = await tryThree ( "[spicedb] Failed to perform authorization check." , ( ) =>
66
+ this . client . checkPermission ( req , this . callOptions ) ,
67
+ ) ;
43
68
const permitted = response . permissionship === v1 . CheckPermissionResponse_Permissionship . HAS_PERMISSION ;
44
69
45
70
return permitted ;
@@ -55,56 +80,38 @@ export class SpiceDBAuthorizer {
55
80
}
56
81
57
82
async writeRelationships ( ...updates : v1 . RelationshipUpdate [ ] ) : Promise < v1 . WriteRelationshipsResponse | undefined > {
58
- let tries = 0 ;
59
- // we do sometimes see INTERNAL errors from SpiceDB, so we retry a few times
60
- // last time we checked it was 15 times per day (check logs)
61
- while ( tries ++ < 3 ) {
62
- const timer = spicedbClientLatency . startTimer ( ) ;
63
- let error : Error | undefined ;
64
- try {
65
- const response = await this . client . writeRelationships (
83
+ const timer = spicedbClientLatency . startTimer ( ) ;
84
+ let error : Error | undefined ;
85
+ try {
86
+ const response = await tryThree ( "[spicedb] Failed to write relationships." , ( ) =>
87
+ this . client . writeRelationships (
66
88
v1 . WriteRelationshipsRequest . create ( {
67
89
updates,
68
90
} ) ,
69
91
this . callOptions ,
70
- ) ;
71
- log . info ( "[spicedb] Successfully wrote relationships." , { response, updates, tries } ) ;
92
+ ) ,
93
+ ) ;
94
+ log . info ( "[spicedb] Successfully wrote relationships." , { response, updates } ) ;
72
95
73
- return response ;
74
- } catch ( err ) {
75
- error = err ;
76
- if ( err . code === grpc . status . INTERNAL && tries < 3 ) {
77
- log . warn ( "[spicedb] Failed to write relationships." , err , {
78
- updates : new TrustedValue ( updates ) ,
79
- tries,
80
- } ) ;
81
- } else {
82
- log . error ( "[spicedb] Failed to write relationships." , err , {
83
- updates : new TrustedValue ( updates ) ,
84
- tries,
85
- } ) ;
86
- // we don't try again on other errors
87
- return ;
88
- }
89
- } finally {
90
- observeSpicedbClientLatency ( "write" , error , timer ( ) ) ;
91
- }
96
+ return response ;
97
+ } finally {
98
+ observeSpicedbClientLatency ( "write" , error , timer ( ) ) ;
92
99
}
93
100
}
94
101
95
102
async deleteRelationships ( req : v1 . DeleteRelationshipsRequest ) : Promise < v1 . ReadRelationshipsResponse [ ] > {
96
103
const timer = spicedbClientLatency . startTimer ( ) ;
97
104
let error : Error | undefined ;
98
105
try {
99
- const existing = await this . client . readRelationships (
100
- v1 . ReadRelationshipsRequest . create ( req ) ,
101
- this . callOptions ,
106
+ const existing = await tryThree ( "readRelationships before deleteRelationships failed." , ( ) =>
107
+ this . client . readRelationships ( v1 . ReadRelationshipsRequest . create ( req ) , this . callOptions ) ,
102
108
) ;
103
109
if ( existing . length > 0 ) {
104
- const response = await this . client . deleteRelationships ( req , this . callOptions ) ;
105
- const after = await this . client . readRelationships (
106
- v1 . ReadRelationshipsRequest . create ( req ) ,
107
- this . callOptions ,
110
+ const response = await tryThree ( "deleteRelationships failed." , ( ) =>
111
+ this . client . deleteRelationships ( req , this . callOptions ) ,
112
+ ) ;
113
+ const after = await tryThree ( "readRelationships failed." , ( ) =>
114
+ this . client . readRelationships ( v1 . ReadRelationshipsRequest . create ( req ) , this . callOptions ) ,
108
115
) ;
109
116
if ( after . length > 0 ) {
110
117
log . error ( "[spicedb] Failed to delete relationships." , { existing, after, request : req } ) ;
@@ -128,7 +135,7 @@ export class SpiceDBAuthorizer {
128
135
}
129
136
130
137
async readRelationships ( req : v1 . ReadRelationshipsRequest ) : Promise < v1 . ReadRelationshipsResponse [ ] > {
131
- return this . client . readRelationships ( req , this . callOptions ) ;
138
+ return tryThree ( "readRelationships failed." , ( ) => this . client . readRelationships ( req , this . callOptions ) ) ;
132
139
}
133
140
134
141
/**
0 commit comments