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,123 @@ 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 ( ) => {
98+ const _syncConfig = async ( dbConfig ?: Prisma . JsonValue | undefined ) => {
8099 if ( isSyncing ) {
81100 abortController . abort ( ) ;
82101 abortController = new AbortController ( ) ;
83102 }
103+
104+ let config : SourcebotConfigurationSchema ;
105+ switch ( SOURCEBOT_TENANT_MODE ) {
106+ case 'single' :
107+ logger . info ( `Syncing configuration file ${ context . configPath } ...` ) ;
108+ config = await fetchConfigFromPath ( context . configPath , abortController . signal ) ;
109+ break ;
110+ case 'multi' :
111+ if ( ! dbConfig ) {
112+ throw new Error ( 'config object is required in multi tenant mode' ) ;
113+ }
114+ config = dbConfig as SourcebotConfigurationSchema
115+ break ;
116+ default :
117+ throw new Error ( `Invalid SOURCEBOT_TENANT_MODE: ${ SOURCEBOT_TENANT_MODE } ` ) ;
118+ }
84119
85- logger . info ( `Syncing configuration file ${ context . configPath } ...` ) ;
86120 isSyncing = true ;
87-
88121 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` ) ;
122+ const { durationMs } = await measure ( ( ) => syncConfig ( config , db , abortController . signal , context ) )
123+ logger . info ( `Synced configuration file in ${ durationMs / 1000 } s` ) ;
91124 isSyncing = false ;
92125 } catch ( err : any ) {
93126 if ( err . name === "AbortError" ) {
94127 // @note : If we're aborting, we don't want to set isSyncing to false
95128 // since it implies another sync is in progress.
96129 } else {
97130 isSyncing = false ;
98- logger . error ( `Failed to sync configuration file ${ context . configPath } with error:` ) ;
131+ logger . error ( `Failed to sync configuration file with error:` ) ;
99132 console . log ( err ) ;
100133 }
101134 }
102135 }
103136
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-
137+ /////////////////////////////
138+ // Init Redis
139+ /////////////////////////////
120140 const redis = new Redis ( {
121141 host : 'localhost' ,
122142 port : 6379 ,
123143 maxRetriesPerRequest : null
124144 } ) ;
125145 redis . ping ( ) . then ( ( ) => {
126146 logger . info ( 'Connected to redis' ) ;
127- } ) . catch ( ( err ) => {
147+ } ) . catch ( ( err : unknown ) => {
128148 logger . error ( 'Failed to connect to redis' ) ;
129149 console . error ( err ) ;
130150 process . exit ( 1 ) ;
131151 } ) ;
132152
153+ /////////////////////////////
154+ // Setup config sync watchers
155+ /////////////////////////////
156+ switch ( SOURCEBOT_TENANT_MODE ) {
157+ case 'single' :
158+ // Re-sync on file changes if the config file is local
159+ if ( ! isRemotePath ( context . configPath ) ) {
160+ watch ( context . configPath , ( ) => {
161+ logger . info ( `Config file ${ context . configPath } changed. Re-syncing...` ) ;
162+ _syncConfig ( ) ;
163+ } ) ;
164+ }
165+
166+ // Re-sync at a fixed interval
167+ setInterval ( ( ) => {
168+ _syncConfig ( ) ;
169+ } , DEFAULT_SETTINGS . resyncIntervalMs ) ;
170+
171+ // Sync immediately on startup
172+ await _syncConfig ( ) ;
173+ break ;
174+ case 'multi' :
175+ const configSyncQueue = new Queue ( 'configSyncQueue' ) ;
176+ const numCores = os . cpus ( ) . length ;
177+ const numWorkers = numCores * DEFAULT_SETTINGS . configSyncConcurrencyMultiple ;
178+ logger . info ( `Detected ${ numCores } cores. Setting config sync max concurrency to ${ numWorkers } ` ) ;
179+ const configSyncWorker = new Worker ( 'configSyncQueue' , async ( job : Job ) => {
180+ const config = job . data as Config ;
181+ await _syncConfig ( config . data ) ;
182+ } , { connection : redis , concurrency : numWorkers } ) ;
183+ configSyncWorker . on ( 'completed' , ( job : Job ) => {
184+ logger . info ( `Config sync job ${ job . id } completed` ) ;
185+ } ) ;
186+ configSyncWorker . on ( 'failed' , ( job : Job | undefined , err : unknown ) => {
187+ logger . info ( `Config sync job failed with error: ${ err } ` ) ;
188+ } ) ;
189+
190+ setInterval ( async ( ) => {
191+ const configs = await db . config . findMany ( {
192+ where : {
193+ syncStatus : ConfigSyncStatus . SYNC_NEEDED ,
194+ }
195+ } ) ;
196+
197+ logger . info ( `Found ${ configs . length } configs to sync...` ) ;
198+ addConfigsToQueue ( db , configSyncQueue , configs ) ;
199+ } , 1000 ) ;
200+ break ;
201+ default :
202+ throw new Error ( `Invalid SOURCEBOT_TENANT_MODE: ${ SOURCEBOT_TENANT_MODE } ` ) ;
203+ }
204+
205+
206+ /////////////////////////
207+ // Setup repo indexing
208+ /////////////////////////
133209 const indexQueue = new Queue ( 'indexQueue' ) ;
134210
135211 const numCores = os . cpus ( ) . length ;
136212 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 ) => {
213+ logger . info ( `Detected ${ numCores } cores. Setting repo index max concurrency to ${ numWorkers } ` ) ;
214+ const worker = new Worker ( 'indexQueue' , async ( job : Job ) => {
139215 const repo = job . data as Repo ;
140216
141217 let indexDuration_s : number | undefined ;
@@ -166,10 +242,10 @@ export const main = async (db: PrismaClient, context: AppContext) => {
166242 } ) ;
167243 } , { connection : redis , concurrency : numWorkers } ) ;
168244
169- worker . on ( 'completed' , ( job ) => {
245+ worker . on ( 'completed' , ( job : Job ) => {
170246 logger . info ( `Job ${ job . id } completed` ) ;
171247 } ) ;
172- worker . on ( 'failed' , async ( job : Job | undefined , err ) => {
248+ worker . on ( 'failed' , async ( job : Job | undefined , err : unknown ) => {
173249 logger . info ( `Job failed with error: ${ err } ` ) ;
174250 if ( job ) {
175251 await db . repo . update ( {
@@ -183,6 +259,7 @@ export const main = async (db: PrismaClient, context: AppContext) => {
183259 }
184260 } ) ;
185261
262+ // Repo indexing loop
186263 while ( true ) {
187264 const thresholdDate = new Date ( Date . now ( ) - DEFAULT_SETTINGS . reindexIntervalMs ) ;
188265 const repos = await db . repo . findMany ( {
0 commit comments