1
- import { PrismaClient , Repo , RepoIndexingStatus } from '@sourcebot/db' ;
1
+ import { ConfigSyncStatus , PrismaClient , Repo , Config , RepoIndexingStatus , Prisma } from '@sourcebot/db' ;
2
2
import { existsSync , watch } from 'fs' ;
3
- import { syncConfig } from "./config.js" ;
3
+ import { fetchConfigFromPath , syncConfig } from "./config.js" ;
4
4
import { cloneRepository , fetchRepository } from "./git.js" ;
5
5
import { createLogger } from "./logger.js" ;
6
6
import { captureEvent } from "./posthog.js" ;
@@ -11,6 +11,8 @@ import { DEFAULT_SETTINGS } from './constants.js';
11
11
import { Queue , Worker , Job } from 'bullmq' ;
12
12
import { Redis } from 'ioredis' ;
13
13
import * as os from 'os' ;
14
+ import { SOURCEBOT_TENANT_MODE } from './environment.js' ;
15
+ import { SourcebotConfigurationSchema } from './schemas/v2.js' ;
14
16
15
17
const logger = createLogger ( 'main' ) ;
16
18
@@ -56,6 +58,23 @@ const syncGitRepository = async (repo: Repo, ctx: AppContext) => {
56
58
}
57
59
}
58
60
61
+ async function addConfigsToQueue ( db : PrismaClient , queue : Queue , configs : Config [ ] ) {
62
+ for ( const config of configs ) {
63
+ await db . $transaction ( async ( tx ) => {
64
+ await tx . config . update ( {
65
+ where : { id : config . id } ,
66
+ data : { syncStatus : ConfigSyncStatus . IN_SYNC_QUEUE } ,
67
+ } ) ;
68
+
69
+ // Add the job to the queue
70
+ await queue . add ( 'configSyncJob' , config ) ;
71
+ logger . info ( `Added job to queue for config ${ config . id } ` ) ;
72
+ } ) . catch ( ( err : unknown ) => {
73
+ logger . error ( `Failed to add job to queue for config ${ config . id } : ${ err } ` ) ;
74
+ } ) ;
75
+ }
76
+ }
77
+
59
78
async function addReposToQueue ( db : PrismaClient , queue : Queue , repos : Repo [ ] ) {
60
79
for ( const repo of repos ) {
61
80
await db . $transaction ( async ( tx ) => {
@@ -67,7 +86,7 @@ async function addReposToQueue(db: PrismaClient, queue: Queue, repos: Repo[]) {
67
86
// Add the job to the queue
68
87
await queue . add ( 'indexJob' , repo ) ;
69
88
logger . info ( `Added job to queue for repo ${ repo . id } ` ) ;
70
- } ) . catch ( ( err ) => {
89
+ } ) . catch ( ( err : unknown ) => {
71
90
logger . error ( `Failed to add job to queue for repo ${ repo . id } : ${ err } ` ) ;
72
91
} ) ;
73
92
}
@@ -76,66 +95,166 @@ async function addReposToQueue(db: PrismaClient, queue: Queue, repos: Repo[]) {
76
95
export const main = async ( db : PrismaClient , context : AppContext ) => {
77
96
let abortController = new AbortController ( ) ;
78
97
let isSyncing = false ;
79
- const _syncConfig = async ( ) => {
80
- if ( isSyncing ) {
81
- abortController . abort ( ) ;
82
- abortController = new AbortController ( ) ;
83
- }
98
+ const _syncConfig = async ( dbConfig ?: Config | undefined ) => {
84
99
85
- logger . info ( `Syncing configuration file ${ context . configPath } ...` ) ;
86
- isSyncing = true ;
100
+ // Fetch config object and update syncing status
101
+ let config : SourcebotConfigurationSchema ;
102
+ switch ( SOURCEBOT_TENANT_MODE ) {
103
+ case 'single' :
104
+ logger . info ( `Syncing configuration file ${ context . configPath } ...` ) ;
87
105
106
+ if ( isSyncing ) {
107
+ abortController . abort ( ) ;
108
+ abortController = new AbortController ( ) ;
109
+ }
110
+ config = await fetchConfigFromPath ( context . configPath , abortController . signal ) ;
111
+ isSyncing = true ;
112
+ break ;
113
+ case 'multi' :
114
+ if ( ! dbConfig ) {
115
+ throw new Error ( 'config object is required in multi tenant mode' ) ;
116
+ }
117
+ config = dbConfig . data as SourcebotConfigurationSchema
118
+ db . config . update ( {
119
+ where : {
120
+ id : dbConfig . id ,
121
+ } ,
122
+ data : {
123
+ syncStatus : ConfigSyncStatus . SYNCING ,
124
+ }
125
+ } )
126
+ break ;
127
+ default :
128
+ throw new Error ( `Invalid SOURCEBOT_TENANT_MODE: ${ SOURCEBOT_TENANT_MODE } ` ) ;
129
+ }
130
+
131
+ // Attempt to sync the config, handle failure cases
88
132
try {
89
- const { durationMs } = await measure ( ( ) => syncConfig ( context . configPath , db , abortController . signal , context ) )
90
- logger . info ( `Synced configuration file ${ context . configPath } in ${ durationMs / 1000 } s` ) ;
133
+ const { durationMs } = await measure ( ( ) => syncConfig ( config , db , abortController . signal , context ) )
134
+ logger . info ( `Synced configuration in ${ durationMs / 1000 } s` ) ;
91
135
isSyncing = false ;
92
136
} catch ( err : any ) {
93
- if ( err . name === "AbortError" ) {
94
- // @note : If we're aborting, we don't want to set isSyncing to false
95
- // since it implies another sync is in progress.
96
- } else {
97
- isSyncing = false ;
98
- logger . error ( `Failed to sync configuration file ${ context . configPath } with error:` ) ;
99
- console . log ( err ) ;
137
+ switch ( SOURCEBOT_TENANT_MODE ) {
138
+ case 'single' :
139
+ if ( err . name === "AbortError" ) {
140
+ // @note : If we're aborting, we don't want to set isSyncing to false
141
+ // since it implies another sync is in progress.
142
+ } else {
143
+ isSyncing = false ;
144
+ logger . error ( `Failed to sync configuration file with error:` ) ;
145
+ console . log ( err ) ;
146
+ }
147
+ break ;
148
+ case 'multi' :
149
+ if ( dbConfig ) {
150
+ await db . config . update ( {
151
+ where : {
152
+ id : dbConfig . id ,
153
+ } ,
154
+ data : {
155
+ syncStatus : ConfigSyncStatus . FAILED ,
156
+ }
157
+ } )
158
+ logger . error ( `Failed to sync configuration ${ dbConfig . id } with error: ${ err } ` ) ;
159
+ } else {
160
+ logger . error ( `DB config undefined. Failed to sync configuration with error: ${ err } ` ) ;
161
+ }
162
+ break ;
163
+ default :
164
+ throw new Error ( `Invalid SOURCEBOT_TENANT_MODE: ${ SOURCEBOT_TENANT_MODE } ` ) ;
100
165
}
101
166
}
102
167
}
103
168
104
- // Re-sync on file changes if the config file is local
105
- if ( ! isRemotePath ( context . configPath ) ) {
106
- watch ( context . configPath , ( ) => {
107
- logger . info ( `Config file ${ context . configPath } changed. Re-syncing...` ) ;
108
- _syncConfig ( ) ;
109
- } ) ;
110
- }
111
-
112
- // Re-sync at a fixed interval
113
- setInterval ( ( ) => {
114
- _syncConfig ( ) ;
115
- } , DEFAULT_SETTINGS . resyncIntervalMs ) ;
116
-
117
- // Sync immediately on startup
118
- await _syncConfig ( ) ;
119
-
169
+ /////////////////////////////
170
+ // Init Redis
171
+ /////////////////////////////
120
172
const redis = new Redis ( {
121
173
host : 'localhost' ,
122
174
port : 6379 ,
123
175
maxRetriesPerRequest : null
124
176
} ) ;
125
177
redis . ping ( ) . then ( ( ) => {
126
178
logger . info ( 'Connected to redis' ) ;
127
- } ) . catch ( ( err ) => {
179
+ } ) . catch ( ( err : unknown ) => {
128
180
logger . error ( 'Failed to connect to redis' ) ;
129
181
console . error ( err ) ;
130
182
process . exit ( 1 ) ;
131
183
} ) ;
132
184
185
+ /////////////////////////////
186
+ // Setup config sync watchers
187
+ /////////////////////////////
188
+ switch ( SOURCEBOT_TENANT_MODE ) {
189
+ case 'single' :
190
+ // Re-sync on file changes if the config file is local
191
+ if ( ! isRemotePath ( context . configPath ) ) {
192
+ watch ( context . configPath , ( ) => {
193
+ logger . info ( `Config file ${ context . configPath } changed. Re-syncing...` ) ;
194
+ _syncConfig ( ) ;
195
+ } ) ;
196
+ }
197
+
198
+ // Re-sync at a fixed interval
199
+ setInterval ( ( ) => {
200
+ _syncConfig ( ) ;
201
+ } , DEFAULT_SETTINGS . resyncIntervalMs ) ;
202
+
203
+ // Sync immediately on startup
204
+ await _syncConfig ( ) ;
205
+ break ;
206
+ case 'multi' :
207
+ // Setup config sync queue and workers
208
+ const configSyncQueue = new Queue ( 'configSyncQueue' ) ;
209
+ const numCores = os . cpus ( ) . length ;
210
+ const numWorkers = numCores * DEFAULT_SETTINGS . configSyncConcurrencyMultiple ;
211
+ logger . info ( `Detected ${ numCores } cores. Setting config sync max concurrency to ${ numWorkers } ` ) ;
212
+ const configSyncWorker = new Worker ( 'configSyncQueue' , async ( job : Job ) => {
213
+ const config = job . data as Config ;
214
+ await _syncConfig ( config ) ;
215
+ } , { connection : redis , concurrency : numWorkers } ) ;
216
+ configSyncWorker . on ( 'completed' , async ( job : Job ) => {
217
+ logger . info ( `Config sync job ${ job . id } completed` ) ;
218
+
219
+ const config = job . data as Config ;
220
+ await db . config . update ( {
221
+ where : {
222
+ id : config . id ,
223
+ } ,
224
+ data : {
225
+ syncStatus : ConfigSyncStatus . SYNCED ,
226
+ }
227
+ } )
228
+ } ) ;
229
+ configSyncWorker . on ( 'failed' , ( job : Job | undefined , err : unknown ) => {
230
+ logger . info ( `Config sync job failed with error: ${ err } ` ) ;
231
+ } ) ;
232
+
233
+ setInterval ( async ( ) => {
234
+ const configs = await db . config . findMany ( {
235
+ where : {
236
+ syncStatus : ConfigSyncStatus . SYNC_NEEDED ,
237
+ }
238
+ } ) ;
239
+
240
+ logger . info ( `Found ${ configs . length } configs to sync...` ) ;
241
+ addConfigsToQueue ( db , configSyncQueue , configs ) ;
242
+ } , 1000 ) ;
243
+ break ;
244
+ default :
245
+ throw new Error ( `Invalid SOURCEBOT_TENANT_MODE: ${ SOURCEBOT_TENANT_MODE } ` ) ;
246
+ }
247
+
248
+
249
+ /////////////////////////
250
+ // Setup repo indexing
251
+ /////////////////////////
133
252
const indexQueue = new Queue ( 'indexQueue' ) ;
134
253
135
254
const numCores = os . cpus ( ) . length ;
136
255
const numWorkers = numCores * DEFAULT_SETTINGS . indexConcurrencyMultiple ;
137
- logger . info ( `Detected ${ numCores } cores. Setting max concurrency to ${ numWorkers } ` ) ;
138
- const worker = new Worker ( 'indexQueue' , async ( job ) => {
256
+ logger . info ( `Detected ${ numCores } cores. Setting repo index max concurrency to ${ numWorkers } ` ) ;
257
+ const worker = new Worker ( 'indexQueue' , async ( job : Job ) => {
139
258
const repo = job . data as Repo ;
140
259
141
260
let indexDuration_s : number | undefined ;
@@ -166,10 +285,10 @@ export const main = async (db: PrismaClient, context: AppContext) => {
166
285
} ) ;
167
286
} , { connection : redis , concurrency : numWorkers } ) ;
168
287
169
- worker . on ( 'completed' , ( job ) => {
288
+ worker . on ( 'completed' , ( job : Job ) => {
170
289
logger . info ( `Job ${ job . id } completed` ) ;
171
290
} ) ;
172
- worker . on ( 'failed' , async ( job : Job | undefined , err ) => {
291
+ worker . on ( 'failed' , async ( job : Job | undefined , err : unknown ) => {
173
292
logger . info ( `Job failed with error: ${ err } ` ) ;
174
293
if ( job ) {
175
294
await db . repo . update ( {
@@ -183,6 +302,7 @@ export const main = async (db: PrismaClient, context: AppContext) => {
183
302
}
184
303
} ) ;
185
304
305
+ // Repo indexing loop
186
306
while ( true ) {
187
307
const thresholdDate = new Date ( Date . now ( ) - DEFAULT_SETTINGS . reindexIntervalMs ) ;
188
308
const repos = await db . repo . findMany ( {
0 commit comments