Skip to content

Commit 1f365eb

Browse files
cursoragentmsukkari
andcommitted
feat: allow repo sync interval settings to be configured via env vars
Add environment variable support for repo sync interval settings: - REINDEX_INTERVAL_MS - RESYNC_CONNECTION_INTERVAL_MS - REINDEX_REPO_POLLING_INTERVAL_MS - RESYNC_CONNECTION_POLLING_INTERVAL_MS Priority order: env var > config file settings > hardcoded default. Invalid values (non-numeric or <= 0) will produce a clear error at startup via Zod validation. Fixes SOU-818 Co-authored-by: Michael Sukkarieh <msukkari@users.noreply.github.com>
1 parent 2fa86ff commit 1f365eb

5 files changed

Lines changed: 148 additions & 15 deletions

File tree

docs/docs/configuration/config-file.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ The following are settings that can be provided in your config file to modify So
4040
|-------------------------------------------------|---------|------------|---------|----------------------------------------------------------------------------------------|
4141
| `maxFileSize` | number | 2 MB | 1 | Maximum size (bytes) of a file to index. Files exceeding this are skipped. |
4242
| `maxTrigramCount` | number | 20 000 | 1 | Maximum trigrams per document. Larger files are skipped. |
43-
| `reindexIntervalMs` | number | 1 hour | 1 | Interval at which all repositories are re‑indexed. |
44-
| `resyncConnectionIntervalMs` | number | 24 hours | 1 | Interval for checking connections that need re‑syncing. |
45-
| `resyncConnectionPollingIntervalMs` | number | 1 second | 1 | DB polling rate for connections that need re‑syncing. |
46-
| `reindexRepoPollingIntervalMs` | number | 1 second | 1 | DB polling rate for repos that should be re‑indexed. |
43+
| `reindexIntervalMs` | number | 1 hour | 1 | Interval at which all repositories are re‑indexed. Can be overridden with `REINDEX_INTERVAL_MS` env var. |
44+
| `resyncConnectionIntervalMs` | number | 24 hours | 1 | Interval for checking connections that need re‑syncing. Can be overridden with `RESYNC_CONNECTION_INTERVAL_MS` env var. |
45+
| `resyncConnectionPollingIntervalMs` | number | 1 second | 1 | DB polling rate for connections that need re‑syncing. Can be overridden with `RESYNC_CONNECTION_POLLING_INTERVAL_MS` env var. |
46+
| `reindexRepoPollingIntervalMs` | number | 1 second | 1 | DB polling rate for repos that should be re‑indexed. Can be overridden with `REINDEX_REPO_POLLING_INTERVAL_MS` env var. |
4747
| `maxConnectionSyncJobConcurrency` | number | 8 | 1 | Concurrent connection‑sync jobs. |
4848
| `maxRepoIndexingJobConcurrency` | number | 8 | 1 | Concurrent repo‑indexing jobs. |
4949
| `maxRepoGarbageCollectionJobConcurrency` | number | 8 | 1 | Concurrent repo‑garbage‑collection jobs. |

docs/docs/configuration/environment-variables.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ The following environment variables allow you to configure your Sourcebot deploy
2828
| `REDIS_REMOVE_ON_FAIL` | `100` | <p>Controls how many failed jobs are allowed to remain in Redis queues</p> |
2929
| `REPO_SYNC_RETRY_BASE_SLEEP_SECONDS` | `60` | <p>The base sleep duration (in seconds) for exponential backoff when retrying repository sync operations that fail</p> |
3030
| `GITLAB_CLIENT_QUERY_TIMEOUT_SECONDS` | `600` | <p>The timeout duration (in seconds) for GitLab client queries</p> |
31+
| `REINDEX_INTERVAL_MS` | `3600000` (1 hour) | <p>The interval (in milliseconds) at which all repositories are re-indexed. Overrides `settings.reindexIntervalMs` from the config file.</p> |
32+
| `RESYNC_CONNECTION_INTERVAL_MS` | `86400000` (24 hours) | <p>The interval (in milliseconds) for checking connections that need re-syncing. Overrides `settings.resyncConnectionIntervalMs` from the config file.</p> |
33+
| `REINDEX_REPO_POLLING_INTERVAL_MS` | `1000` (1 second) | <p>The polling rate (in milliseconds) at which the database should be checked for repos that need re-indexing. Overrides `settings.reindexRepoPollingIntervalMs` from the config file.</p> |
34+
| `RESYNC_CONNECTION_POLLING_INTERVAL_MS` | `1000` (1 second) | <p>The polling rate (in milliseconds) at which the database should be checked for connections that need re-syncing. Overrides `settings.resyncConnectionPollingIntervalMs` from the config file.</p> |
3135
| `SMTP_CONNECTION_URL` | `-` | <p>The url to the SMTP service used for sending transactional emails. See [this doc](/docs/configuration/transactional-emails) for more info.</p><p>You can also use `SMTP_HOST`, `SMTP_PORT`, `SMTP_USERNAME`, and `SMTP_PASSWORD` to construct the SMTP connection url.</p> |
3236
| `SMTP_HOST` | `-` | <p>The hostname of the SMTP server. Used to construct `SMTP_CONNECTION_URL` when individual SMTP variables are provided.</p> |
3337
| `SMTP_PORT` | `-` | <p>The port of the SMTP server.</p> |

packages/shared/src/env.server.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,12 @@ const options = {
296296
CONNECTION_MANAGER_UPSERT_TIMEOUT_MS: numberSchema.default(300000),
297297
REPO_SYNC_RETRY_BASE_SLEEP_SECONDS: numberSchema.default(60),
298298

299+
// Repo sync interval settings (override config file settings)
300+
REINDEX_INTERVAL_MS: z.coerce.number().int().positive().optional(),
301+
RESYNC_CONNECTION_INTERVAL_MS: z.coerce.number().int().positive().optional(),
302+
REINDEX_REPO_POLLING_INTERVAL_MS: z.coerce.number().int().positive().optional(),
303+
RESYNC_CONNECTION_POLLING_INTERVAL_MS: z.coerce.number().int().positive().optional(),
304+
299305
GITLAB_CLIENT_QUERY_TIMEOUT_SECONDS: numberSchema.default(60 * 10),
300306

301307
SOURCEBOT_LOG_LEVEL: z.enum(["info", "debug", "warn", "error"]).default("info"),

packages/shared/src/utils.test.ts

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,42 @@ vi.mock('fs/promises', () => ({
99
readFile: vi.fn().mockResolvedValue('{}'),
1010
}));
1111

12+
// Mock env module to allow overriding env vars in tests
13+
vi.mock('./env.server.js', async (importOriginal) => {
14+
const original = await importOriginal() as Record<string, unknown>;
15+
return {
16+
...original,
17+
env: {
18+
...(original.env as Record<string, unknown>),
19+
REINDEX_INTERVAL_MS: undefined,
20+
RESYNC_CONNECTION_INTERVAL_MS: undefined,
21+
REINDEX_REPO_POLLING_INTERVAL_MS: undefined,
22+
RESYNC_CONNECTION_POLLING_INTERVAL_MS: undefined,
23+
},
24+
};
25+
});
26+
1227
const mockConfigFile = (settings?: object) => {
1328
vi.mocked(readFile).mockResolvedValueOnce(
1429
JSON.stringify(settings !== undefined ? { settings } : {}) as any
1530
);
1631
};
1732

33+
const mockEnvVars = async (envOverrides: Record<string, number | undefined>) => {
34+
const envModule = await import('./env.server.js');
35+
Object.assign(envModule.env, envOverrides);
36+
};
37+
1838
describe('getConfigSettings', () => {
19-
beforeEach(() => {
39+
beforeEach(async () => {
2040
vi.mocked(readFile).mockResolvedValue('{}' as any);
41+
// Reset env vars to undefined before each test
42+
await mockEnvVars({
43+
REINDEX_INTERVAL_MS: undefined,
44+
RESYNC_CONNECTION_INTERVAL_MS: undefined,
45+
REINDEX_REPO_POLLING_INTERVAL_MS: undefined,
46+
RESYNC_CONNECTION_POLLING_INTERVAL_MS: undefined,
47+
});
2148
});
2249

2350
test('returns DEFAULT_CONFIG_SETTINGS when no config path is provided', async () => {
@@ -94,4 +121,97 @@ describe('getConfigSettings', () => {
94121
);
95122
});
96123
});
124+
125+
describe('env var overrides for sync intervals', () => {
126+
test('REINDEX_INTERVAL_MS env var overrides config file setting', async () => {
127+
await mockEnvVars({ REINDEX_INTERVAL_MS: 7200000 });
128+
mockConfigFile({ reindexIntervalMs: 1800000 });
129+
const result = await getConfigSettings('/config.json');
130+
expect(result.reindexIntervalMs).toBe(7200000);
131+
});
132+
133+
test('REINDEX_INTERVAL_MS env var overrides default when no config', async () => {
134+
await mockEnvVars({ REINDEX_INTERVAL_MS: 7200000 });
135+
const result = await getConfigSettings(undefined);
136+
expect(result.reindexIntervalMs).toBe(7200000);
137+
});
138+
139+
test('RESYNC_CONNECTION_INTERVAL_MS env var overrides config file setting', async () => {
140+
await mockEnvVars({ RESYNC_CONNECTION_INTERVAL_MS: 43200000 });
141+
mockConfigFile({ resyncConnectionIntervalMs: 86400000 });
142+
const result = await getConfigSettings('/config.json');
143+
expect(result.resyncConnectionIntervalMs).toBe(43200000);
144+
});
145+
146+
test('RESYNC_CONNECTION_INTERVAL_MS env var overrides default when no config', async () => {
147+
await mockEnvVars({ RESYNC_CONNECTION_INTERVAL_MS: 43200000 });
148+
const result = await getConfigSettings(undefined);
149+
expect(result.resyncConnectionIntervalMs).toBe(43200000);
150+
});
151+
152+
test('REINDEX_REPO_POLLING_INTERVAL_MS env var overrides config file setting', async () => {
153+
await mockEnvVars({ REINDEX_REPO_POLLING_INTERVAL_MS: 5000 });
154+
mockConfigFile({ reindexRepoPollingIntervalMs: 2000 });
155+
const result = await getConfigSettings('/config.json');
156+
expect(result.reindexRepoPollingIntervalMs).toBe(5000);
157+
});
158+
159+
test('REINDEX_REPO_POLLING_INTERVAL_MS env var overrides default when no config', async () => {
160+
await mockEnvVars({ REINDEX_REPO_POLLING_INTERVAL_MS: 5000 });
161+
const result = await getConfigSettings(undefined);
162+
expect(result.reindexRepoPollingIntervalMs).toBe(5000);
163+
});
164+
165+
test('RESYNC_CONNECTION_POLLING_INTERVAL_MS env var overrides config file setting', async () => {
166+
await mockEnvVars({ RESYNC_CONNECTION_POLLING_INTERVAL_MS: 3000 });
167+
mockConfigFile({ resyncConnectionPollingIntervalMs: 1000 });
168+
const result = await getConfigSettings('/config.json');
169+
expect(result.resyncConnectionPollingIntervalMs).toBe(3000);
170+
});
171+
172+
test('RESYNC_CONNECTION_POLLING_INTERVAL_MS env var overrides default when no config', async () => {
173+
await mockEnvVars({ RESYNC_CONNECTION_POLLING_INTERVAL_MS: 3000 });
174+
const result = await getConfigSettings(undefined);
175+
expect(result.resyncConnectionPollingIntervalMs).toBe(3000);
176+
});
177+
178+
test('multiple env vars can be set simultaneously', async () => {
179+
await mockEnvVars({
180+
REINDEX_INTERVAL_MS: 7200000,
181+
RESYNC_CONNECTION_INTERVAL_MS: 43200000,
182+
REINDEX_REPO_POLLING_INTERVAL_MS: 5000,
183+
RESYNC_CONNECTION_POLLING_INTERVAL_MS: 3000,
184+
});
185+
mockConfigFile({
186+
reindexIntervalMs: 1800000,
187+
resyncConnectionIntervalMs: 86400000,
188+
reindexRepoPollingIntervalMs: 2000,
189+
resyncConnectionPollingIntervalMs: 1000,
190+
});
191+
const result = await getConfigSettings('/config.json');
192+
expect(result.reindexIntervalMs).toBe(7200000);
193+
expect(result.resyncConnectionIntervalMs).toBe(43200000);
194+
expect(result.reindexRepoPollingIntervalMs).toBe(5000);
195+
expect(result.resyncConnectionPollingIntervalMs).toBe(3000);
196+
});
197+
198+
test('config file values are used when env vars are not set', async () => {
199+
mockConfigFile({
200+
reindexIntervalMs: 1800000,
201+
resyncConnectionIntervalMs: 43200000,
202+
});
203+
const result = await getConfigSettings('/config.json');
204+
expect(result.reindexIntervalMs).toBe(1800000);
205+
expect(result.resyncConnectionIntervalMs).toBe(43200000);
206+
});
207+
208+
test('defaults are used when neither env vars nor config file are set', async () => {
209+
mockConfigFile({});
210+
const result = await getConfigSettings('/config.json');
211+
expect(result.reindexIntervalMs).toBe(DEFAULT_CONFIG_SETTINGS.reindexIntervalMs);
212+
expect(result.resyncConnectionIntervalMs).toBe(DEFAULT_CONFIG_SETTINGS.resyncConnectionIntervalMs);
213+
expect(result.reindexRepoPollingIntervalMs).toBe(DEFAULT_CONFIG_SETTINGS.reindexRepoPollingIntervalMs);
214+
expect(result.resyncConnectionPollingIntervalMs).toBe(DEFAULT_CONFIG_SETTINGS.resyncConnectionPollingIntervalMs);
215+
});
216+
});
97217
});

packages/shared/src/utils.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,24 +75,27 @@ export const loadJsonFile = async <T>(
7575

7676

7777
export const getConfigSettings = async (configPath?: string): Promise<ConfigSettings> => {
78-
if (!configPath) {
79-
return DEFAULT_CONFIG_SETTINGS;
80-
}
81-
82-
const config = await loadConfig(configPath);
78+
const config = configPath ? await loadConfig(configPath) : undefined;
8379

80+
// Merge settings: env vars > config file > defaults
81+
// Priority order: env var > config file `settings` block > hardcoded default
8482
return {
8583
...DEFAULT_CONFIG_SETTINGS,
86-
...config.settings,
84+
...config?.settings,
8785
// Fall back to deprecated experiment_ variants if new keys are not set.
8886
repoDrivenPermissionSyncIntervalMs:
89-
config.settings?.repoDrivenPermissionSyncIntervalMs
90-
?? config.settings?.experiment_repoDrivenPermissionSyncIntervalMs
87+
config?.settings?.repoDrivenPermissionSyncIntervalMs
88+
?? config?.settings?.experiment_repoDrivenPermissionSyncIntervalMs
9189
?? DEFAULT_CONFIG_SETTINGS.repoDrivenPermissionSyncIntervalMs,
9290
userDrivenPermissionSyncIntervalMs:
93-
config.settings?.userDrivenPermissionSyncIntervalMs
94-
?? config.settings?.experiment_userDrivenPermissionSyncIntervalMs
91+
config?.settings?.userDrivenPermissionSyncIntervalMs
92+
?? config?.settings?.experiment_userDrivenPermissionSyncIntervalMs
9593
?? DEFAULT_CONFIG_SETTINGS.userDrivenPermissionSyncIntervalMs,
94+
// Env var overrides (highest priority)
95+
...(env.REINDEX_INTERVAL_MS !== undefined && { reindexIntervalMs: env.REINDEX_INTERVAL_MS }),
96+
...(env.RESYNC_CONNECTION_INTERVAL_MS !== undefined && { resyncConnectionIntervalMs: env.RESYNC_CONNECTION_INTERVAL_MS }),
97+
...(env.REINDEX_REPO_POLLING_INTERVAL_MS !== undefined && { reindexRepoPollingIntervalMs: env.REINDEX_REPO_POLLING_INTERVAL_MS }),
98+
...(env.RESYNC_CONNECTION_POLLING_INTERVAL_MS !== undefined && { resyncConnectionPollingIntervalMs: env.RESYNC_CONNECTION_POLLING_INTERVAL_MS }),
9699
}
97100
}
98101

0 commit comments

Comments
 (0)