11import * as Sentry from "@sentry/node" ;
2- import { PrismaClient , Repo , RepoPermissionSyncStatus } from "@sourcebot/db" ;
2+ import { PrismaClient , Repo , RepoPermissionSyncJobStatus } from "@sourcebot/db" ;
33import { createLogger } from "@sourcebot/logger" ;
44import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/bitbucket.type" ;
55import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type" ;
@@ -10,16 +10,16 @@ import { Redis } from 'ioredis';
1010import { env } from "./env.js" ;
1111import { createOctokitFromConfig , getUserIdsWithReadAccessToRepo } from "./github.js" ;
1212import { RepoWithConnections } from "./types.js" ;
13+ import { PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES } from "./constants.js" ;
1314
1415type RepoPermissionSyncJob = {
15- repoId : number ;
16+ jobId : string ;
1617}
1718
1819const QUEUE_NAME = 'repoPermissionSyncQueue' ;
1920
20- const logger = createLogger ( 'permission-syncer' ) ;
21+ const logger = createLogger ( 'repo- permission-syncer' ) ;
2122
22- const SUPPORTED_CODE_HOST_TYPES = [ 'github' ] ;
2323
2424export class RepoPermissionSyncer {
2525 private queue : Queue < RepoPermissionSyncJob > ;
@@ -46,47 +46,55 @@ export class RepoPermissionSyncer {
4646 return setInterval ( async ( ) => {
4747 // @todo : make this configurable
4848 const thresholdDate = new Date ( Date . now ( ) - 1000 * 60 * 60 * 24 ) ;
49+
4950 const repos = await this . db . repo . findMany ( {
5051 // Repos need their permissions to be synced against the code host when...
5152 where : {
5253 // They belong to a code host that supports permissions syncing
5354 AND : [
5455 {
5556 external_codeHostType : {
56- in : SUPPORTED_CODE_HOST_TYPES ,
57+ in : PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES ,
5758 }
5859 } ,
59- // and, they either require a sync (SYNC_NEEDED) or have been in a completed state (SYNCED or FAILED)
60- // for > some duration (default 24 hours)
6160 {
6261 OR : [
63- {
64- permissionSyncStatus : RepoPermissionSyncStatus . SYNC_NEEDED
65- } ,
66- {
67- AND : [
68- {
69- OR : [
70- { permissionSyncStatus : RepoPermissionSyncStatus . SYNCED } ,
71- { permissionSyncStatus : RepoPermissionSyncStatus . FAILED } ,
72- ]
73- } ,
74- {
75- OR : [
76- { permissionSyncJobLastCompletedAt : null } ,
77- { permissionSyncJobLastCompletedAt : { lt : thresholdDate } }
78- ]
79- }
80- ]
62+ { permissionSyncedAt : null } ,
63+ { permissionSyncedAt : { lt : thresholdDate } } ,
64+ ] ,
65+ } ,
66+ {
67+ NOT : {
68+ permissionSyncJobs : {
69+ some : {
70+ OR : [
71+ // Don't schedule if there are active jobs
72+ {
73+ status : {
74+ in : [
75+ RepoPermissionSyncJobStatus . PENDING ,
76+ RepoPermissionSyncJobStatus . IN_PROGRESS ,
77+ ] ,
78+ }
79+ } ,
80+ // Don't schedule if there are recent failed jobs (within the threshold date). Note `gt` is used here since this is a inverse condition.
81+ {
82+ AND : [
83+ { status : RepoPermissionSyncJobStatus . FAILED } ,
84+ { completedAt : { gt : thresholdDate } } ,
85+ ]
86+ }
87+ ]
88+ }
8189 }
82- ]
90+ }
8391 } ,
8492 ]
8593 }
8694 } ) ;
8795
8896 await this . schedulePermissionSync ( repos ) ;
89- } , 1000 * 30 ) ;
97+ } , 1000 * 5 ) ;
9098 }
9199
92100 public dispose ( ) {
@@ -96,15 +104,16 @@ export class RepoPermissionSyncer {
96104
97105 private async schedulePermissionSync ( repos : Repo [ ] ) {
98106 await this . db . $transaction ( async ( tx ) => {
99- await tx . repo . updateMany ( {
100- where : { id : { in : repos . map ( repo => repo . id ) } } ,
101- data : { permissionSyncStatus : RepoPermissionSyncStatus . IN_SYNC_QUEUE } ,
107+ const jobs = await tx . repoPermissionSyncJob . createManyAndReturn ( {
108+ data : repos . map ( repo => ( {
109+ repoId : repo . id ,
110+ } ) ) ,
102111 } ) ;
103112
104- await this . queue . addBulk ( repos . map ( repo => ( {
113+ await this . queue . addBulk ( jobs . map ( ( job ) => ( {
105114 name : 'repoPermissionSyncJob' ,
106115 data : {
107- repoId : repo . id ,
116+ jobId : job . id ,
108117 } ,
109118 opts : {
110119 removeOnComplete : env . REDIS_REMOVE_ON_COMPLETE ,
@@ -115,21 +124,25 @@ export class RepoPermissionSyncer {
115124 }
116125
117126 private async runJob ( job : Job < RepoPermissionSyncJob > ) {
118- const id = job . data . repoId ;
119- const repo = await this . db . repo . update ( {
127+ const id = job . data . jobId ;
128+ const { repo } = await this . db . repoPermissionSyncJob . update ( {
120129 where : {
121- id
130+ id,
122131 } ,
123132 data : {
124- permissionSyncStatus : RepoPermissionSyncStatus . SYNCING ,
133+ status : RepoPermissionSyncJobStatus . IN_PROGRESS ,
125134 } ,
126- include : {
127- connections : {
135+ select : {
136+ repo : {
128137 include : {
129- connection : true ,
130- } ,
131- } ,
132- } ,
138+ connections : {
139+ include : {
140+ connection : true ,
141+ }
142+ }
143+ }
144+ }
145+ }
133146 } ) ;
134147
135148 if ( ! repo ) {
@@ -171,34 +184,43 @@ export class RepoPermissionSyncer {
171184 return [ ] ;
172185 } ) ( ) ;
173186
174- await this . db . repo . update ( {
175- where : {
176- id : repo . id ,
177- } ,
178- data : {
179- permittedUsers : {
180- deleteMany : { } ,
187+ await this . db . $transaction ( [
188+ this . db . repo . update ( {
189+ where : {
190+ id : repo . id ,
191+ } ,
192+ data : {
193+ permittedUsers : {
194+ deleteMany : { } ,
195+ }
181196 }
182- }
183- } ) ;
184-
185- await this . db . userToRepoPermission . createMany ( {
186- data : userIds . map ( userId => ( {
187- userId,
188- repoId : repo . id ,
189- } ) ) ,
190- } ) ;
197+ } ) ,
198+ this . db . userToRepoPermission . createMany ( {
199+ data : userIds . map ( userId => ( {
200+ userId,
201+ repoId : repo . id ,
202+ } ) ) ,
203+ } )
204+ ] ) ;
191205 }
192206
193207 private async onJobCompleted ( job : Job < RepoPermissionSyncJob > ) {
194- const repo = await this . db . repo . update ( {
208+ const { repo } = await this . db . repoPermissionSyncJob . update ( {
195209 where : {
196- id : job . data . repoId ,
210+ id : job . data . jobId ,
197211 } ,
198212 data : {
199- permissionSyncStatus : RepoPermissionSyncStatus . SYNCED ,
200- permissionSyncJobLastCompletedAt : new Date ( ) ,
213+ status : RepoPermissionSyncJobStatus . COMPLETED ,
214+ repo : {
215+ update : {
216+ permissionSyncedAt : new Date ( ) ,
217+ }
218+ } ,
219+ completedAt : new Date ( ) ,
201220 } ,
221+ select : {
222+ repo : true
223+ }
202224 } ) ;
203225
204226 logger . info ( `Permissions synced for repo ${ repo . displayName ?? repo . name } ` ) ;
@@ -207,21 +229,25 @@ export class RepoPermissionSyncer {
207229 private async onJobFailed ( job : Job < RepoPermissionSyncJob > | undefined , err : Error ) {
208230 Sentry . captureException ( err , {
209231 tags : {
210- repoId : job ?. data . repoId ,
232+ jobId : job ?. data . jobId ,
211233 queue : QUEUE_NAME ,
212234 }
213235 } ) ;
214236
215- const errorMessage = ( repoName : string ) => `Repo permission sync job failed for repo ${ repoName } : ${ err } ` ;
237+ const errorMessage = ( repoName : string ) => `Repo permission sync job failed for repo ${ repoName } : ${ err . message } ` ;
216238
217239 if ( job ) {
218- const repo = await this . db . repo . update ( {
240+ const { repo } = await this . db . repoPermissionSyncJob . update ( {
219241 where : {
220- id : job ? .data . repoId ,
242+ id : job . data . jobId ,
221243 } ,
222244 data : {
223- permissionSyncStatus : RepoPermissionSyncStatus . FAILED ,
224- permissionSyncJobLastCompletedAt : new Date ( ) ,
245+ status : RepoPermissionSyncJobStatus . FAILED ,
246+ completedAt : new Date ( ) ,
247+ errorMessage : err . message ,
248+ } ,
249+ select : {
250+ repo : true
225251 } ,
226252 } ) ;
227253 logger . error ( errorMessage ( repo . displayName ?? repo . name ) ) ;
0 commit comments