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,123 @@ 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 ( ) => {
98
+ const _syncConfig = async ( dbConfig ?: Prisma . JsonValue | undefined ) => {
80
99
if ( isSyncing ) {
81
100
abortController . abort ( ) ;
82
101
abortController = new AbortController ( ) ;
83
102
}
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
+ }
84
119
85
- logger . info ( `Syncing configuration file ${ context . configPath } ...` ) ;
86
120
isSyncing = true ;
87
-
88
121
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` ) ;
91
124
isSyncing = false ;
92
125
} catch ( err : any ) {
93
126
if ( err . name === "AbortError" ) {
94
127
// @note : If we're aborting, we don't want to set isSyncing to false
95
128
// since it implies another sync is in progress.
96
129
} else {
97
130
isSyncing = false ;
98
- logger . error ( `Failed to sync configuration file ${ context . configPath } with error:` ) ;
131
+ logger . error ( `Failed to sync configuration file with error:` ) ;
99
132
console . log ( err ) ;
100
133
}
101
134
}
102
135
}
103
136
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
+ /////////////////////////////
120
140
const redis = new Redis ( {
121
141
host : 'localhost' ,
122
142
port : 6379 ,
123
143
maxRetriesPerRequest : null
124
144
} ) ;
125
145
redis . ping ( ) . then ( ( ) => {
126
146
logger . info ( 'Connected to redis' ) ;
127
- } ) . catch ( ( err ) => {
147
+ } ) . catch ( ( err : unknown ) => {
128
148
logger . error ( 'Failed to connect to redis' ) ;
129
149
console . error ( err ) ;
130
150
process . exit ( 1 ) ;
131
151
} ) ;
132
152
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
+ /////////////////////////
133
209
const indexQueue = new Queue ( 'indexQueue' ) ;
134
210
135
211
const numCores = os . cpus ( ) . length ;
136
212
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 ) => {
139
215
const repo = job . data as Repo ;
140
216
141
217
let indexDuration_s : number | undefined ;
@@ -166,10 +242,10 @@ export const main = async (db: PrismaClient, context: AppContext) => {
166
242
} ) ;
167
243
} , { connection : redis , concurrency : numWorkers } ) ;
168
244
169
- worker . on ( 'completed' , ( job ) => {
245
+ worker . on ( 'completed' , ( job : Job ) => {
170
246
logger . info ( `Job ${ job . id } completed` ) ;
171
247
} ) ;
172
- worker . on ( 'failed' , async ( job : Job | undefined , err ) => {
248
+ worker . on ( 'failed' , async ( job : Job | undefined , err : unknown ) => {
173
249
logger . info ( `Job failed with error: ${ err } ` ) ;
174
250
if ( job ) {
175
251
await db . repo . update ( {
@@ -183,6 +259,7 @@ export const main = async (db: PrismaClient, context: AppContext) => {
183
259
}
184
260
} ) ;
185
261
262
+ // Repo indexing loop
186
263
while ( true ) {
187
264
const thresholdDate = new Date ( Date . now ( ) - DEFAULT_SETTINGS . reindexIntervalMs ) ;
188
265
const repos = await db . repo . findMany ( {
0 commit comments