Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Added 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`). [#1057](https://github.com/sourcebot-dev/sourcebot/pull/1057)

## [4.16.3] - 2026-03-27

### Added
Expand Down
8 changes: 4 additions & 4 deletions docs/docs/configuration/config-file.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ The following are settings that can be provided in your config file to modify So
|-------------------------------------------------|---------|------------|---------|----------------------------------------------------------------------------------------|
| `maxFileSize` | number | 2 MB | 1 | Maximum size (bytes) of a file to index. Files exceeding this are skipped. |
| `maxTrigramCount` | number | 20 000 | 1 | Maximum trigrams per document. Larger files are skipped. |
| `reindexIntervalMs` | number | 1 hour | 1 | Interval at which all repositories are re‑indexed. |
| `resyncConnectionIntervalMs` | number | 24 hours | 1 | Interval for checking connections that need re‑syncing. |
| `resyncConnectionPollingIntervalMs` | number | 1 second | 1 | DB polling rate for connections that need re‑syncing. |
| `reindexRepoPollingIntervalMs` | number | 1 second | 1 | DB polling rate for repos that should be re‑indexed. |
| `reindexIntervalMs` | number | 1 hour | 1 | Interval at which all repositories are re‑indexed. Can be overridden with `REINDEX_INTERVAL_MS` env var. |
| `resyncConnectionIntervalMs` | number | 24 hours | 1 | Interval for checking connections that need re‑syncing. Can be overridden with `RESYNC_CONNECTION_INTERVAL_MS` env var. |
| `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. |
| `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. |
| `maxConnectionSyncJobConcurrency` | number | 8 | 1 | Concurrent connection‑sync jobs. |
| `maxRepoIndexingJobConcurrency` | number | 8 | 1 | Concurrent repo‑indexing jobs. |
| `maxRepoGarbageCollectionJobConcurrency` | number | 8 | 1 | Concurrent repo‑garbage‑collection jobs. |
Expand Down
4 changes: 4 additions & 0 deletions docs/docs/configuration/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ The following environment variables allow you to configure your Sourcebot deploy
| `REDIS_REMOVE_ON_FAIL` | `100` | <p>Controls how many failed jobs are allowed to remain in Redis queues</p> |
| `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> |
| `GITLAB_CLIENT_QUERY_TIMEOUT_SECONDS` | `600` | <p>The timeout duration (in seconds) for GitLab client queries</p> |
| `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> |
| `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> |
| `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> |
| `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> |
| `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> |
| `SMTP_HOST` | `-` | <p>The hostname of the SMTP server. Used to construct `SMTP_CONNECTION_URL` when individual SMTP variables are provided.</p> |
| `SMTP_PORT` | `-` | <p>The port of the SMTP server.</p> |
Expand Down
6 changes: 6 additions & 0 deletions packages/shared/src/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,12 @@ const options = {
CONNECTION_MANAGER_UPSERT_TIMEOUT_MS: numberSchema.default(300000),
REPO_SYNC_RETRY_BASE_SLEEP_SECONDS: numberSchema.default(60),

// Repo sync interval settings (override config file settings)
REINDEX_INTERVAL_MS: z.coerce.number().int().positive().optional(),
RESYNC_CONNECTION_INTERVAL_MS: z.coerce.number().int().positive().optional(),
REINDEX_REPO_POLLING_INTERVAL_MS: z.coerce.number().int().positive().optional(),
RESYNC_CONNECTION_POLLING_INTERVAL_MS: z.coerce.number().int().positive().optional(),

GITLAB_CLIENT_QUERY_TIMEOUT_SECONDS: numberSchema.default(60 * 10),

SOURCEBOT_LOG_LEVEL: z.enum(["info", "debug", "warn", "error"]).default("info"),
Expand Down
122 changes: 121 additions & 1 deletion packages/shared/src/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,42 @@ vi.mock('fs/promises', () => ({
readFile: vi.fn().mockResolvedValue('{}'),
}));

// Mock env module to allow overriding env vars in tests
vi.mock('./env.server.js', async (importOriginal) => {
const original = await importOriginal() as Record<string, unknown>;
return {
...original,
env: {
...(original.env as Record<string, unknown>),
REINDEX_INTERVAL_MS: undefined,
RESYNC_CONNECTION_INTERVAL_MS: undefined,
REINDEX_REPO_POLLING_INTERVAL_MS: undefined,
RESYNC_CONNECTION_POLLING_INTERVAL_MS: undefined,
},
};
});

const mockConfigFile = (settings?: object) => {
vi.mocked(readFile).mockResolvedValueOnce(
JSON.stringify(settings !== undefined ? { settings } : {}) as any
);
};

const mockEnvVars = async (envOverrides: Record<string, number | undefined>) => {
const envModule = await import('./env.server.js');
Object.assign(envModule.env, envOverrides);
};

describe('getConfigSettings', () => {
beforeEach(() => {
beforeEach(async () => {
vi.mocked(readFile).mockResolvedValue('{}' as any);
// Reset env vars to undefined before each test
await mockEnvVars({
REINDEX_INTERVAL_MS: undefined,
RESYNC_CONNECTION_INTERVAL_MS: undefined,
REINDEX_REPO_POLLING_INTERVAL_MS: undefined,
RESYNC_CONNECTION_POLLING_INTERVAL_MS: undefined,
});
});

test('returns DEFAULT_CONFIG_SETTINGS when no config path is provided', async () => {
Expand Down Expand Up @@ -94,4 +121,97 @@ describe('getConfigSettings', () => {
);
});
});

describe('env var overrides for sync intervals', () => {
test('REINDEX_INTERVAL_MS env var overrides config file setting', async () => {
await mockEnvVars({ REINDEX_INTERVAL_MS: 7200000 });
mockConfigFile({ reindexIntervalMs: 1800000 });
const result = await getConfigSettings('/config.json');
expect(result.reindexIntervalMs).toBe(7200000);
});

test('REINDEX_INTERVAL_MS env var overrides default when no config', async () => {
await mockEnvVars({ REINDEX_INTERVAL_MS: 7200000 });
const result = await getConfigSettings(undefined);
expect(result.reindexIntervalMs).toBe(7200000);
});

test('RESYNC_CONNECTION_INTERVAL_MS env var overrides config file setting', async () => {
await mockEnvVars({ RESYNC_CONNECTION_INTERVAL_MS: 43200000 });
mockConfigFile({ resyncConnectionIntervalMs: 86400000 });
const result = await getConfigSettings('/config.json');
expect(result.resyncConnectionIntervalMs).toBe(43200000);
});

test('RESYNC_CONNECTION_INTERVAL_MS env var overrides default when no config', async () => {
await mockEnvVars({ RESYNC_CONNECTION_INTERVAL_MS: 43200000 });
const result = await getConfigSettings(undefined);
expect(result.resyncConnectionIntervalMs).toBe(43200000);
});

test('REINDEX_REPO_POLLING_INTERVAL_MS env var overrides config file setting', async () => {
await mockEnvVars({ REINDEX_REPO_POLLING_INTERVAL_MS: 5000 });
mockConfigFile({ reindexRepoPollingIntervalMs: 2000 });
const result = await getConfigSettings('/config.json');
expect(result.reindexRepoPollingIntervalMs).toBe(5000);
});

test('REINDEX_REPO_POLLING_INTERVAL_MS env var overrides default when no config', async () => {
await mockEnvVars({ REINDEX_REPO_POLLING_INTERVAL_MS: 5000 });
const result = await getConfigSettings(undefined);
expect(result.reindexRepoPollingIntervalMs).toBe(5000);
});

test('RESYNC_CONNECTION_POLLING_INTERVAL_MS env var overrides config file setting', async () => {
await mockEnvVars({ RESYNC_CONNECTION_POLLING_INTERVAL_MS: 3000 });
mockConfigFile({ resyncConnectionPollingIntervalMs: 1000 });
const result = await getConfigSettings('/config.json');
expect(result.resyncConnectionPollingIntervalMs).toBe(3000);
});

test('RESYNC_CONNECTION_POLLING_INTERVAL_MS env var overrides default when no config', async () => {
await mockEnvVars({ RESYNC_CONNECTION_POLLING_INTERVAL_MS: 3000 });
const result = await getConfigSettings(undefined);
expect(result.resyncConnectionPollingIntervalMs).toBe(3000);
});

test('multiple env vars can be set simultaneously', async () => {
await mockEnvVars({
REINDEX_INTERVAL_MS: 7200000,
RESYNC_CONNECTION_INTERVAL_MS: 43200000,
REINDEX_REPO_POLLING_INTERVAL_MS: 5000,
RESYNC_CONNECTION_POLLING_INTERVAL_MS: 3000,
});
mockConfigFile({
reindexIntervalMs: 1800000,
resyncConnectionIntervalMs: 86400000,
reindexRepoPollingIntervalMs: 2000,
resyncConnectionPollingIntervalMs: 1000,
});
const result = await getConfigSettings('/config.json');
expect(result.reindexIntervalMs).toBe(7200000);
expect(result.resyncConnectionIntervalMs).toBe(43200000);
expect(result.reindexRepoPollingIntervalMs).toBe(5000);
expect(result.resyncConnectionPollingIntervalMs).toBe(3000);
});

test('config file values are used when env vars are not set', async () => {
mockConfigFile({
reindexIntervalMs: 1800000,
resyncConnectionIntervalMs: 43200000,
});
const result = await getConfigSettings('/config.json');
expect(result.reindexIntervalMs).toBe(1800000);
expect(result.resyncConnectionIntervalMs).toBe(43200000);
});

test('defaults are used when neither env vars nor config file are set', async () => {
mockConfigFile({});
const result = await getConfigSettings('/config.json');
expect(result.reindexIntervalMs).toBe(DEFAULT_CONFIG_SETTINGS.reindexIntervalMs);
expect(result.resyncConnectionIntervalMs).toBe(DEFAULT_CONFIG_SETTINGS.resyncConnectionIntervalMs);
expect(result.reindexRepoPollingIntervalMs).toBe(DEFAULT_CONFIG_SETTINGS.reindexRepoPollingIntervalMs);
expect(result.resyncConnectionPollingIntervalMs).toBe(DEFAULT_CONFIG_SETTINGS.resyncConnectionPollingIntervalMs);
});
});
});
23 changes: 13 additions & 10 deletions packages/shared/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,24 +75,27 @@ export const loadJsonFile = async <T>(


export const getConfigSettings = async (configPath?: string): Promise<ConfigSettings> => {
if (!configPath) {
return DEFAULT_CONFIG_SETTINGS;
}

const config = await loadConfig(configPath);
const config = configPath ? await loadConfig(configPath) : undefined;

// Merge settings: env vars > config file > defaults
// Priority order: env var > config file `settings` block > hardcoded default
return {
...DEFAULT_CONFIG_SETTINGS,
...config.settings,
...config?.settings,
// Fall back to deprecated experiment_ variants if new keys are not set.
repoDrivenPermissionSyncIntervalMs:
config.settings?.repoDrivenPermissionSyncIntervalMs
?? config.settings?.experiment_repoDrivenPermissionSyncIntervalMs
config?.settings?.repoDrivenPermissionSyncIntervalMs
?? config?.settings?.experiment_repoDrivenPermissionSyncIntervalMs
?? DEFAULT_CONFIG_SETTINGS.repoDrivenPermissionSyncIntervalMs,
userDrivenPermissionSyncIntervalMs:
config.settings?.userDrivenPermissionSyncIntervalMs
?? config.settings?.experiment_userDrivenPermissionSyncIntervalMs
config?.settings?.userDrivenPermissionSyncIntervalMs
?? config?.settings?.experiment_userDrivenPermissionSyncIntervalMs
?? DEFAULT_CONFIG_SETTINGS.userDrivenPermissionSyncIntervalMs,
// Env var overrides (highest priority)
...(env.REINDEX_INTERVAL_MS !== undefined && { reindexIntervalMs: env.REINDEX_INTERVAL_MS }),
...(env.RESYNC_CONNECTION_INTERVAL_MS !== undefined && { resyncConnectionIntervalMs: env.RESYNC_CONNECTION_INTERVAL_MS }),
...(env.REINDEX_REPO_POLLING_INTERVAL_MS !== undefined && { reindexRepoPollingIntervalMs: env.REINDEX_REPO_POLLING_INTERVAL_MS }),
...(env.RESYNC_CONNECTION_POLLING_INTERVAL_MS !== undefined && { resyncConnectionPollingIntervalMs: env.RESYNC_CONNECTION_POLLING_INTERVAL_MS }),
}
}

Expand Down
Loading