1- import { PrismaClient , Repo , RepoIndexingStatus } from '@sourcebot/db' ;
1+ import { ConfigSyncStatus , PrismaClient , Repo , Config , RepoIndexingStatus , Prisma } from '@sourcebot/db' ;
22import { existsSync , watch } from 'fs' ;
3- import { syncConfig } from "./config.js" ;
3+ import { fetchConfigFromPath , syncConfig } from "./config.js" ;
44import { cloneRepository , fetchRepository } from "./git.js" ;
55import { createLogger } from "./logger.js" ;
66import { captureEvent } from "./posthog.js" ;
@@ -11,6 +11,8 @@ import { DEFAULT_SETTINGS } from './constants.js';
1111import { Queue , Worker , Job } from 'bullmq' ;
1212import { Redis } from 'ioredis' ;
1313import * as os from 'os' ;
14+ import { SOURCEBOT_TENANT_MODE } from './environment.js' ;
15+ import { SourcebotConfigurationSchema } from './schemas/v2.js' ;
1416
1517const logger = createLogger ( 'main' ) ;
1618
@@ -56,6 +58,23 @@ const syncGitRepository = async (repo: Repo, ctx: AppContext) => {
5658 }
5759}
5860
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+
5978async function addReposToQueue ( db : PrismaClient , queue : Queue , repos : Repo [ ] ) {
6079 for ( const repo of repos ) {
6180 await db . $transaction ( async ( tx ) => {
@@ -67,7 +86,7 @@ async function addReposToQueue(db: PrismaClient, queue: Queue, repos: Repo[]) {
6786 // Add the job to the queue
6887 await queue . add ( 'indexJob' , repo ) ;
6988 logger . info ( `Added job to queue for repo ${ repo . id } ` ) ;
70- } ) . catch ( ( err ) => {
89+ } ) . catch ( ( err : unknown ) => {
7190 logger . error ( `Failed to add job to queue for repo ${ repo . id } : ${ err } ` ) ;
7291 } ) ;
7392 }
@@ -76,66 +95,166 @@ async function addReposToQueue(db: PrismaClient, queue: Queue, repos: Repo[]) {
7695export const main = async ( db : PrismaClient , context : AppContext ) => {
7796 let abortController = new AbortController ( ) ;
7897 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 ) => {
8499
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 } ...` ) ;
87105
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
88132 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` ) ;
91135 isSyncing = false ;
92136 } 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 } ` ) ;
100165 }
101166 }
102167 }
103168
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+ /////////////////////////////
120172 const redis = new Redis ( {
121173 host : 'localhost' ,
122174 port : 6379 ,
123175 maxRetriesPerRequest : null
124176 } ) ;
125177 redis . ping ( ) . then ( ( ) => {
126178 logger . info ( 'Connected to redis' ) ;
127- } ) . catch ( ( err ) => {
179+ } ) . catch ( ( err : unknown ) => {
128180 logger . error ( 'Failed to connect to redis' ) ;
129181 console . error ( err ) ;
130182 process . exit ( 1 ) ;
131183 } ) ;
132184
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+ /////////////////////////
133252 const indexQueue = new Queue ( 'indexQueue' ) ;
134253
135254 const numCores = os . cpus ( ) . length ;
136255 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 ) => {
139258 const repo = job . data as Repo ;
140259
141260 let indexDuration_s : number | undefined ;
@@ -166,10 +285,10 @@ export const main = async (db: PrismaClient, context: AppContext) => {
166285 } ) ;
167286 } , { connection : redis , concurrency : numWorkers } ) ;
168287
169- worker . on ( 'completed' , ( job ) => {
288+ worker . on ( 'completed' , ( job : Job ) => {
170289 logger . info ( `Job ${ job . id } completed` ) ;
171290 } ) ;
172- worker . on ( 'failed' , async ( job : Job | undefined , err ) => {
291+ worker . on ( 'failed' , async ( job : Job | undefined , err : unknown ) => {
173292 logger . info ( `Job failed with error: ${ err } ` ) ;
174293 if ( job ) {
175294 await db . repo . update ( {
@@ -183,6 +302,7 @@ export const main = async (db: PrismaClient, context: AppContext) => {
183302 }
184303 } ) ;
185304
305+ // Repo indexing loop
186306 while ( true ) {
187307 const thresholdDate = new Date ( Date . now ( ) - DEFAULT_SETTINGS . reindexIntervalMs ) ;
188308 const repos = await db . repo . findMany ( {
0 commit comments