mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-13 21:05:22 +00:00
further wip
This commit is contained in:
parent
963f6fd69e
commit
775b87a06c
11 changed files with 420 additions and 156 deletions
|
|
@ -6,7 +6,7 @@
|
|||
"scripts": {
|
||||
"build": "cross-env SKIP_ENV_VALIDATION=1 yarn workspaces foreach -A run build",
|
||||
"test": "yarn workspaces foreach -A run test",
|
||||
"dev": "yarn dev:prisma:migrate:dev && npm-run-all --print-label --parallel dev:zoekt dev:backend dev:web watch:mcp watch:schemas",
|
||||
"dev": "concurrently --kill-others --names \"zoekt,worker,web,mcp,schemas\" 'yarn dev:zoekt' 'yarn dev:backend' 'yarn dev:web' 'yarn watch:mcp' 'yarn watch:schemas'",
|
||||
"with-env": "cross-env PATH=\"$PWD/bin:$PATH\" dotenv -e .env.development -c --",
|
||||
"dev:zoekt": "yarn with-env zoekt-webserver -index .sourcebot/index -rpc",
|
||||
"dev:backend": "yarn with-env yarn workspace @sourcebot/backend dev:watch",
|
||||
|
|
@ -21,9 +21,9 @@
|
|||
"build:deps": "yarn workspaces foreach -R --from '{@sourcebot/schemas,@sourcebot/error,@sourcebot/crypto,@sourcebot/db,@sourcebot/shared}' run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.2.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"npm-run-all": "^4.1.5"
|
||||
"dotenv-cli": "^8.0.0"
|
||||
},
|
||||
"packageManager": "yarn@4.7.0",
|
||||
"resolutions": {
|
||||
|
|
|
|||
|
|
@ -364,12 +364,12 @@ export class ConnectionManager {
|
|||
}
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
public async dispose() {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
this.worker.close();
|
||||
this.queue.close();
|
||||
await this.worker.close();
|
||||
await this.queue.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,12 +101,12 @@ export class RepoPermissionSyncer {
|
|||
}, 1000 * 5);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
public async dispose() {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
this.worker.close();
|
||||
this.queue.close();
|
||||
await this.worker.close();
|
||||
await this.queue.close();
|
||||
}
|
||||
|
||||
private async schedulePermissionSync(repos: Repo[]) {
|
||||
|
|
|
|||
|
|
@ -101,12 +101,12 @@ export class UserPermissionSyncer {
|
|||
}, 1000 * 5);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
public async dispose() {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
this.worker.close();
|
||||
this.queue.close();
|
||||
await this.worker.close();
|
||||
await this.queue.close();
|
||||
}
|
||||
|
||||
private async schedulePermissionSync(users: User[]) {
|
||||
|
|
|
|||
|
|
@ -90,13 +90,28 @@ else if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement(
|
|||
}
|
||||
|
||||
const cleanup = async (signal: string) => {
|
||||
logger.info(`Recieved ${signal}, cleaning up...`);
|
||||
logger.info(`Received ${signal}, cleaning up...`);
|
||||
|
||||
connectionManager.dispose();
|
||||
repoManager.dispose();
|
||||
repoPermissionSyncer.dispose();
|
||||
userPermissionSyncer.dispose();
|
||||
indexSyncer.dispose();
|
||||
const shutdownTimeout = 30000; // 30 seconds
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
Promise.all([
|
||||
indexSyncer.dispose(),
|
||||
repoManager.dispose(),
|
||||
connectionManager.dispose(),
|
||||
repoPermissionSyncer.dispose(),
|
||||
userPermissionSyncer.dispose(),
|
||||
promClient.dispose(),
|
||||
]),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Shutdown timeout')), shutdownTimeout)
|
||||
)
|
||||
]);
|
||||
logger.info('All workers shut down gracefully');
|
||||
} catch (error) {
|
||||
logger.warn('Shutdown timeout or error, forcing exit:', error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
|
||||
await prisma.$disconnect();
|
||||
await redis.quit();
|
||||
|
|
|
|||
|
|
@ -1,29 +1,46 @@
|
|||
import { createBullBoard } from '@bull-board/api';
|
||||
import { ExpressAdapter } from '@bull-board/express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { PrismaClient, Repo, RepoIndexingJobStatus } from "@sourcebot/db";
|
||||
import { createLogger } from "@sourcebot/logger";
|
||||
import { PrismaClient, Repo, RepoJobStatus, RepoJobType } from "@sourcebot/db";
|
||||
import { createLogger, Logger } from "@sourcebot/logger";
|
||||
import express from 'express';
|
||||
import { BullBoardGroupMQAdapter, Job, Queue, ReservedJob, Worker } from "groupmq";
|
||||
import { Redis } from 'ioredis';
|
||||
import { AppContext, repoMetadataSchema, RepoWithConnections, Settings } from "./types.js";
|
||||
import { getAuthCredentialsForRepo, getRepoPath, measure } from './utils.js';
|
||||
import { getAuthCredentialsForRepo, getRepoPath, getShardPrefix, measure } from './utils.js';
|
||||
import { existsSync } from 'fs';
|
||||
import { cloneRepository, fetchRepository, unsetGitConfig, upsertGitConfig } from './git.js';
|
||||
import { cloneRepository, fetchRepository, isPathAValidGitRepoRoot, unsetGitConfig, upsertGitConfig } from './git.js';
|
||||
import { indexGitRepository } from './zoekt.js';
|
||||
import { rm, readdir } from 'fs/promises';
|
||||
|
||||
const logger = createLogger('index-syncer');
|
||||
const LOG_TAG = 'index-syncer';
|
||||
const logger = createLogger(LOG_TAG);
|
||||
const createJobLogger = (jobId: string) => createLogger(`${LOG_TAG}:job:${jobId}`);
|
||||
|
||||
type IndexSyncJob = {
|
||||
type JobPayload = {
|
||||
type: 'INDEX' | 'CLEANUP';
|
||||
jobId: string;
|
||||
}
|
||||
repoId: number;
|
||||
repoName: string;
|
||||
};
|
||||
|
||||
const JOB_TIMEOUT_MS = 1000 * 60 * 60 * 6; // 6 hour indexing timeout
|
||||
|
||||
|
||||
const groupmqLifecycleExceptionWrapper = async (name: string, fn: () => Promise<void>) => {
|
||||
try {
|
||||
await fn();
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
logger.error(`Exception thrown while executing lifecycle function \`${name}\`.`, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class IndexSyncer {
|
||||
private interval?: NodeJS.Timeout;
|
||||
private queue: Queue<IndexSyncJob>;
|
||||
private worker: Worker<IndexSyncJob>;
|
||||
private queue: Queue<JobPayload>;
|
||||
private worker: Worker<JobPayload>;
|
||||
|
||||
constructor(
|
||||
private db: PrismaClient,
|
||||
|
|
@ -31,28 +48,26 @@ export class IndexSyncer {
|
|||
redis: Redis,
|
||||
private ctx: AppContext,
|
||||
) {
|
||||
this.queue = new Queue<IndexSyncJob>({
|
||||
this.queue = new Queue<JobPayload>({
|
||||
redis,
|
||||
namespace: 'index-sync-queue',
|
||||
jobTimeoutMs: JOB_TIMEOUT_MS,
|
||||
// logger: true,
|
||||
logger,
|
||||
maxAttempts: 1,
|
||||
});
|
||||
|
||||
this.worker = new Worker<IndexSyncJob>({
|
||||
this.worker = new Worker<JobPayload>({
|
||||
queue: this.queue,
|
||||
maxStalledCount: 1,
|
||||
stalledInterval: 1000,
|
||||
handler: this.runJob.bind(this),
|
||||
concurrency: this.settings.maxRepoIndexingJobConcurrency,
|
||||
logger,
|
||||
});
|
||||
|
||||
this.worker.on('completed', this.onJobCompleted.bind(this));
|
||||
this.worker.on('failed', this.onJobFailed.bind(this));
|
||||
this.worker.on('stalled', this.onJobStalled.bind(this));
|
||||
this.worker.on('error', async (error) => {
|
||||
Sentry.captureException(error);
|
||||
logger.error(`Index syncer worker error.`, error);
|
||||
});
|
||||
this.worker.on('error', this.onWorkerError.bind(this));
|
||||
|
||||
// @nocheckin
|
||||
const app = express();
|
||||
|
|
@ -69,75 +84,133 @@ export class IndexSyncer {
|
|||
|
||||
public async startScheduler() {
|
||||
this.interval = setInterval(async () => {
|
||||
const thresholdDate = new Date(Date.now() - this.settings.reindexIntervalMs);
|
||||
|
||||
const repos = await this.db.repo.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{ indexedAt: null },
|
||||
{ indexedAt: { lt: thresholdDate } },
|
||||
]
|
||||
},
|
||||
{
|
||||
NOT: {
|
||||
indexingJobs: {
|
||||
some: {
|
||||
OR: [
|
||||
// Don't schedule if there are active jobs that were created within the threshold date.
|
||||
// This handles the case where a job is stuck in a pending state and will never be scheduled.
|
||||
{
|
||||
AND: [
|
||||
{
|
||||
status: {
|
||||
in: [
|
||||
RepoIndexingJobStatus.PENDING,
|
||||
RepoIndexingJobStatus.IN_PROGRESS,
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
createdAt: {
|
||||
gt: thresholdDate,
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// Don't schedule if there are recent failed jobs (within the threshold date).
|
||||
{
|
||||
AND: [
|
||||
{ status: RepoIndexingJobStatus.FAILED },
|
||||
{ completedAt: { gt: thresholdDate } },
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
});
|
||||
|
||||
if (repos.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.scheduleIndexSync(repos);
|
||||
await this.scheduleIndexJobs();
|
||||
await this.scheduleCleanupJobs();
|
||||
}, 1000 * 5);
|
||||
|
||||
this.worker.run();
|
||||
}
|
||||
|
||||
private async scheduleIndexSync(repos: Repo[]) {
|
||||
private async scheduleIndexJobs() {
|
||||
const thresholdDate = new Date(Date.now() - this.settings.reindexIntervalMs);
|
||||
const reposToIndex = await this.db.repo.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{ indexedAt: null },
|
||||
{ indexedAt: { lt: thresholdDate } },
|
||||
]
|
||||
},
|
||||
{
|
||||
NOT: {
|
||||
jobs: {
|
||||
some: {
|
||||
AND: [
|
||||
{
|
||||
type: RepoJobType.INDEX,
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
// Don't schedule if there are active jobs that were created within the threshold date.
|
||||
// This handles the case where a job is stuck in a pending state and will never be scheduled.
|
||||
{
|
||||
AND: [
|
||||
{
|
||||
status: {
|
||||
in: [
|
||||
RepoJobStatus.PENDING,
|
||||
RepoJobStatus.IN_PROGRESS,
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
createdAt: {
|
||||
gt: thresholdDate,
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// Don't schedule if there are recent failed jobs (within the threshold date).
|
||||
{
|
||||
AND: [
|
||||
{ status: RepoJobStatus.FAILED },
|
||||
{ completedAt: { gt: thresholdDate } },
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
});
|
||||
|
||||
if (reposToIndex.length > 0) {
|
||||
await this.createJobs(reposToIndex, RepoJobType.INDEX);
|
||||
}
|
||||
}
|
||||
|
||||
private async scheduleCleanupJobs() {
|
||||
const thresholdDate = new Date(Date.now() - this.settings.repoGarbageCollectionGracePeriodMs);
|
||||
|
||||
const reposToCleanup = await this.db.repo.findMany({
|
||||
where: {
|
||||
connections: {
|
||||
none: {}
|
||||
},
|
||||
OR: [
|
||||
{ indexedAt: null },
|
||||
{ indexedAt: { lt: thresholdDate } },
|
||||
],
|
||||
// Don't schedule if there are active jobs that were created within the threshold date.
|
||||
NOT: {
|
||||
jobs: {
|
||||
some: {
|
||||
AND: [
|
||||
{
|
||||
type: RepoJobType.CLEANUP,
|
||||
},
|
||||
{
|
||||
status: {
|
||||
in: [
|
||||
RepoJobStatus.PENDING,
|
||||
RepoJobStatus.IN_PROGRESS,
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
createdAt: {
|
||||
gt: thresholdDate,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (reposToCleanup.length > 0) {
|
||||
await this.createJobs(reposToCleanup, RepoJobType.CLEANUP);
|
||||
}
|
||||
}
|
||||
|
||||
private async createJobs(repos: Repo[], type: RepoJobType) {
|
||||
// @note: we don't perform this in a transaction because
|
||||
// we want to avoid the situation where a job is created and run
|
||||
// prior to the transaction being committed.
|
||||
const jobs = await this.db.repoIndexingJob.createManyAndReturn({
|
||||
const jobs = await this.db.repoJob.createManyAndReturn({
|
||||
data: repos.map(repo => ({
|
||||
type,
|
||||
repoId: repo.id,
|
||||
}))
|
||||
})),
|
||||
include: {
|
||||
repo: true,
|
||||
}
|
||||
});
|
||||
|
||||
for (const job of jobs) {
|
||||
|
|
@ -145,22 +218,29 @@ export class IndexSyncer {
|
|||
groupId: `repo:${job.repoId}`,
|
||||
data: {
|
||||
jobId: job.id,
|
||||
type,
|
||||
repoName: job.repo.name,
|
||||
repoId: job.repo.id,
|
||||
},
|
||||
jobId: job.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async runJob(job: ReservedJob<IndexSyncJob>) {
|
||||
private async runJob(job: ReservedJob<JobPayload>) {
|
||||
const id = job.data.jobId;
|
||||
const { repo } = await this.db.repoIndexingJob.update({
|
||||
const logger = createJobLogger(id);
|
||||
logger.info(`Running job ${id} for repo ${job.data.repoName}`);
|
||||
|
||||
const { repo, type: jobType } = await this.db.repoJob.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: RepoIndexingJobStatus.IN_PROGRESS,
|
||||
status: RepoJobStatus.IN_PROGRESS,
|
||||
},
|
||||
select: {
|
||||
type: true,
|
||||
repo: {
|
||||
include: {
|
||||
connections: {
|
||||
|
|
@ -173,10 +253,14 @@ export class IndexSyncer {
|
|||
}
|
||||
});
|
||||
|
||||
await this.syncGitRepository(repo);
|
||||
if (jobType === RepoJobType.INDEX) {
|
||||
await this.indexRepository(repo, logger);
|
||||
} else if (jobType === RepoJobType.CLEANUP) {
|
||||
await this.cleanupRepository(repo, logger);
|
||||
}
|
||||
}
|
||||
|
||||
private async syncGitRepository(repo: RepoWithConnections) {
|
||||
private async indexRepository(repo: RepoWithConnections, logger: Logger) {
|
||||
const { path: repoPath, isReadOnly } = getRepoPath(repo, this.ctx);
|
||||
|
||||
const metadata = repoMetadataSchema.parse(repo.metadata);
|
||||
|
|
@ -185,6 +269,14 @@ export class IndexSyncer {
|
|||
const cloneUrlMaybeWithToken = credentials?.cloneUrlWithToken ?? repo.cloneUrl;
|
||||
const authHeader = credentials?.authHeader ?? undefined;
|
||||
|
||||
// If the repo path exists but it is not a valid git repository root, this indicates
|
||||
// that the repository is in a bad state. To fix, we remove the directory and perform
|
||||
// a fresh clone.
|
||||
if (existsSync(repoPath) && !(await isPathAValidGitRepoRoot(repoPath)) && !isReadOnly) {
|
||||
logger.warn(`${repoPath} is not a valid git repository root. Deleting directory and performing fresh clone.`);
|
||||
await rm(repoPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
if (existsSync(repoPath) && !isReadOnly) {
|
||||
// @NOTE: in #483, we changed the cloning method s.t., we _no longer_
|
||||
// write the clone URL (which could contain a auth token) to the
|
||||
|
|
@ -238,57 +330,94 @@ export class IndexSyncer {
|
|||
logger.info(`Indexed ${repo.displayName} in ${indexDuration_s}s`);
|
||||
}
|
||||
|
||||
private async onJobCompleted(job: Job<IndexSyncJob>) {
|
||||
const { repo } = await this.db.repoIndexingJob.update({
|
||||
where: { id: job.data.jobId },
|
||||
data: {
|
||||
status: RepoIndexingJobStatus.COMPLETED,
|
||||
repo: {
|
||||
update: {
|
||||
private async cleanupRepository(repo: Repo, logger: Logger) {
|
||||
const { path: repoPath, isReadOnly } = getRepoPath(repo, this.ctx);
|
||||
if (existsSync(repoPath) && !isReadOnly) {
|
||||
logger.info(`Deleting repo directory ${repoPath}`);
|
||||
await rm(repoPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const shardPrefix = getShardPrefix(repo.orgId, repo.id);
|
||||
const files = (await readdir(this.ctx.indexPath)).filter(file => file.startsWith(shardPrefix));
|
||||
for (const file of files) {
|
||||
const filePath = `${this.ctx.indexPath}/${file}`;
|
||||
logger.info(`Deleting shard file ${filePath}`);
|
||||
await rm(filePath, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
private onJobCompleted = async (job: Job<JobPayload>) =>
|
||||
groupmqLifecycleExceptionWrapper('onJobCompleted', async () => {
|
||||
const logger = createJobLogger(job.data.jobId);
|
||||
const jobData = await this.db.repoJob.update({
|
||||
where: { id: job.data.jobId },
|
||||
data: {
|
||||
status: RepoJobStatus.COMPLETED,
|
||||
completedAt: new Date(),
|
||||
}
|
||||
});
|
||||
|
||||
if (jobData.type === RepoJobType.INDEX) {
|
||||
const repo = await this.db.repo.update({
|
||||
where: { id: jobData.repoId },
|
||||
data: {
|
||||
indexedAt: new Date(),
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`Completed index job ${job.data.jobId} for repo ${repo.name}`);
|
||||
}
|
||||
else if (jobData.type === RepoJobType.CLEANUP) {
|
||||
const repo = await this.db.repo.delete({
|
||||
where: { id: jobData.repoId },
|
||||
});
|
||||
|
||||
logger.info(`Completed cleanup job ${job.data.jobId} for repo ${repo.name}`);
|
||||
}
|
||||
});
|
||||
|
||||
private onJobFailed = async (job: Job<JobPayload>) =>
|
||||
groupmqLifecycleExceptionWrapper('onJobFailed', async () => {
|
||||
const logger = createJobLogger(job.data.jobId);
|
||||
|
||||
const { repo } = await this.db.repoJob.update({
|
||||
where: { id: job.data.jobId },
|
||||
data: {
|
||||
completedAt: new Date(),
|
||||
errorMessage: job.failedReason,
|
||||
},
|
||||
completedAt: new Date(),
|
||||
},
|
||||
select: { repo: true }
|
||||
select: { repo: true }
|
||||
});
|
||||
|
||||
logger.error(`Failed job ${job.data.jobId} for repo ${repo.name}`);
|
||||
});
|
||||
|
||||
logger.info(`Completed index job ${job.data.jobId} for repo ${repo.name}`);
|
||||
}
|
||||
private onJobStalled = async (jobId: string) =>
|
||||
groupmqLifecycleExceptionWrapper('onJobStalled', async () => {
|
||||
const logger = createJobLogger(jobId);
|
||||
const { repo } = await this.db.repoJob.update({
|
||||
where: { id: jobId },
|
||||
data: {
|
||||
status: RepoJobStatus.FAILED,
|
||||
completedAt: new Date(),
|
||||
errorMessage: 'Job stalled',
|
||||
},
|
||||
select: { repo: true }
|
||||
});
|
||||
|
||||
private async onJobFailed(job: Job<IndexSyncJob>) {
|
||||
const { repo } = await this.db.repoIndexingJob.update({
|
||||
where: { id: job.data.jobId },
|
||||
data: {
|
||||
status: RepoIndexingJobStatus.FAILED,
|
||||
completedAt: new Date(),
|
||||
errorMessage: job.failedReason,
|
||||
},
|
||||
select: { repo: true}
|
||||
logger.error(`Job ${jobId} stalled for repo ${repo.name}`);
|
||||
});
|
||||
|
||||
logger.error(`Failed index job ${job.data.jobId} for repo ${repo.name}`);
|
||||
private async onWorkerError(error: Error) {
|
||||
Sentry.captureException(error);
|
||||
logger.error(`Index syncer worker error.`, error);
|
||||
}
|
||||
|
||||
private async onJobStalled(jobId: string) {
|
||||
const { repo } = await this.db.repoIndexingJob.update({
|
||||
where: { id: jobId },
|
||||
data: {
|
||||
status: RepoIndexingJobStatus.FAILED,
|
||||
completedAt: new Date(),
|
||||
errorMessage: 'Job stalled',
|
||||
},
|
||||
select: { repo: true }
|
||||
});
|
||||
|
||||
logger.error(`Job ${jobId} stalled for repo ${repo.name}`);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
public async dispose() {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
this.worker.close();
|
||||
this.queue.close();
|
||||
await this.worker.close();
|
||||
await this.queue.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import express, { Request, Response } from 'express';
|
||||
import { Server } from 'http';
|
||||
import client, { Registry, Counter, Gauge } from 'prom-client';
|
||||
import { createLogger } from "@sourcebot/logger";
|
||||
|
||||
|
|
@ -7,6 +8,8 @@ const logger = createLogger('prometheus-client');
|
|||
export class PromClient {
|
||||
private registry: Registry;
|
||||
private app: express.Application;
|
||||
private server: Server;
|
||||
|
||||
public activeRepoIndexingJobs: Gauge<string>;
|
||||
public pendingRepoIndexingJobs: Gauge<string>;
|
||||
public repoIndexingReattemptsTotal: Counter<string>;
|
||||
|
|
@ -98,12 +101,12 @@ export class PromClient {
|
|||
res.end(metrics);
|
||||
});
|
||||
|
||||
this.app.listen(this.PORT, () => {
|
||||
this.server = this.app.listen(this.PORT, () => {
|
||||
logger.info(`Prometheus metrics server is running on port ${this.PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
getRegistry(): Registry {
|
||||
return this.registry;
|
||||
dispose() {
|
||||
this.server.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -558,9 +558,9 @@ export class RepoManager {
|
|||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
this.indexWorker.close();
|
||||
this.indexQueue.close();
|
||||
this.gcQueue.close();
|
||||
this.gcWorker.close();
|
||||
await this.indexWorker.close();
|
||||
await this.indexQueue.close();
|
||||
await this.gcQueue.close();
|
||||
await this.gcWorker.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -54,13 +54,15 @@ model Repo {
|
|||
webUrl String?
|
||||
connections RepoToConnection[]
|
||||
imageUrl String?
|
||||
|
||||
/// @deprecated status tracking is now done via the `jobs` table.
|
||||
repoIndexingStatus RepoIndexingStatus @default(NEW)
|
||||
|
||||
permittedUsers UserToRepoPermission[]
|
||||
permissionSyncJobs RepoPermissionSyncJob[]
|
||||
permissionSyncedAt DateTime? /// When the permissions were last synced successfully.
|
||||
|
||||
indexingJobs RepoIndexingJob[]
|
||||
jobs RepoJob[]
|
||||
indexedAt DateTime? /// When the repo was last indexed successfully.
|
||||
|
||||
external_id String /// The id of the repo in the external service
|
||||
|
|
@ -76,16 +78,22 @@ model Repo {
|
|||
@@index([orgId])
|
||||
}
|
||||
|
||||
enum RepoIndexingJobStatus {
|
||||
enum RepoJobStatus {
|
||||
PENDING
|
||||
IN_PROGRESS
|
||||
COMPLETED
|
||||
FAILED
|
||||
}
|
||||
|
||||
model RepoIndexingJob {
|
||||
enum RepoJobType {
|
||||
INDEX
|
||||
CLEANUP
|
||||
}
|
||||
|
||||
model RepoJob {
|
||||
id String @id @default(cuid())
|
||||
status RepoIndexingJobStatus @default(PENDING)
|
||||
type RepoJobType
|
||||
status RepoJobStatus @default(PENDING)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
completedAt DateTime?
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import winston, { format } from 'winston';
|
||||
import winston, { format, Logger } from 'winston';
|
||||
import { Logtail } from '@logtail/node';
|
||||
import { LogtailTransport } from '@logtail/winston';
|
||||
import { MESSAGE } from 'triple-beam';
|
||||
|
|
@ -48,7 +48,7 @@ const createLogger = (label: string) => {
|
|||
format: combine(
|
||||
errors({ stack: true }),
|
||||
timestamp(),
|
||||
labelFn({ label: label })
|
||||
labelFn({ label: label }),
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
|
|
@ -84,4 +84,8 @@ const createLogger = (label: string) => {
|
|||
|
||||
export {
|
||||
createLogger
|
||||
};
|
||||
};
|
||||
|
||||
export type {
|
||||
Logger,
|
||||
}
|
||||
113
yarn.lock
113
yarn.lock
|
|
@ -10061,6 +10061,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cliui@npm:^8.0.1":
|
||||
version: 8.0.1
|
||||
resolution: "cliui@npm:8.0.1"
|
||||
dependencies:
|
||||
string-width: "npm:^4.2.0"
|
||||
strip-ansi: "npm:^6.0.1"
|
||||
wrap-ansi: "npm:^7.0.0"
|
||||
checksum: 10c0/4bda0f09c340cbb6dfdc1ed508b3ca080f12992c18d68c6be4d9cf51756033d5266e61ec57529e610dacbf4da1c634423b0c1b11037709cc6b09045cbd815df5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"clone@npm:^1.0.2":
|
||||
version: 1.0.4
|
||||
resolution: "clone@npm:1.0.4"
|
||||
|
|
@ -10459,6 +10470,23 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"concurrently@npm:^9.2.1":
|
||||
version: 9.2.1
|
||||
resolution: "concurrently@npm:9.2.1"
|
||||
dependencies:
|
||||
chalk: "npm:4.1.2"
|
||||
rxjs: "npm:7.8.2"
|
||||
shell-quote: "npm:1.8.3"
|
||||
supports-color: "npm:8.1.1"
|
||||
tree-kill: "npm:1.2.2"
|
||||
yargs: "npm:17.7.2"
|
||||
bin:
|
||||
conc: dist/bin/concurrently.js
|
||||
concurrently: dist/bin/concurrently.js
|
||||
checksum: 10c0/da37f239f82eb7ac24f5ddb56259861e5f1d6da2ade7602b6ea7ad3101b13b5ccec02a77b7001402d1028ff2fdc38eed55644b32853ad5abf30e057002a963aa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"content-disposition@npm:0.5.4":
|
||||
version: 0.5.4
|
||||
resolution: "content-disposition@npm:0.5.4"
|
||||
|
|
@ -11810,7 +11838,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"escalade@npm:^3.2.0":
|
||||
"escalade@npm:^3.1.1, escalade@npm:^3.2.0":
|
||||
version: 3.2.0
|
||||
resolution: "escalade@npm:3.2.0"
|
||||
checksum: 10c0/ced4dd3a78e15897ed3be74e635110bbf3b08877b0a41be50dcb325ee0e0b5f65fc2d50e9845194d7c4633f327e2e1c6cce00a71b617c5673df0374201d67f65
|
||||
|
|
@ -12781,6 +12809,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-caller-file@npm:^2.0.5":
|
||||
version: 2.0.5
|
||||
resolution: "get-caller-file@npm:2.0.5"
|
||||
checksum: 10c0/c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7, get-intrinsic@npm:^1.3.0":
|
||||
version: 1.3.0
|
||||
resolution: "get-intrinsic@npm:1.3.0"
|
||||
|
|
@ -17617,6 +17652,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"require-directory@npm:^2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "require-directory@npm:2.1.1"
|
||||
checksum: 10c0/83aa76a7bc1531f68d92c75a2ca2f54f1b01463cb566cf3fbc787d0de8be30c9dbc211d1d46be3497dac5785fe296f2dd11d531945ac29730643357978966e99
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"require-from-string@npm:^2.0.2":
|
||||
version: 2.0.2
|
||||
resolution: "require-from-string@npm:2.0.2"
|
||||
|
|
@ -17931,9 +17973,9 @@ __metadata:
|
|||
version: 0.0.0-use.local
|
||||
resolution: "root-workspace-0b6124@workspace:."
|
||||
dependencies:
|
||||
concurrently: "npm:^9.2.1"
|
||||
cross-env: "npm:^7.0.3"
|
||||
dotenv-cli: "npm:^8.0.0"
|
||||
npm-run-all: "npm:^4.1.5"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
|
|
@ -18015,6 +18057,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rxjs@npm:7.8.2":
|
||||
version: 7.8.2
|
||||
resolution: "rxjs@npm:7.8.2"
|
||||
dependencies:
|
||||
tslib: "npm:^2.1.0"
|
||||
checksum: 10c0/1fcd33d2066ada98ba8f21fcbbcaee9f0b271de1d38dc7f4e256bfbc6ffcdde68c8bfb69093de7eeb46f24b1fb820620bf0223706cff26b4ab99a7ff7b2e2c45
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"safe-array-concat@npm:^1.1.3":
|
||||
version: 1.1.3
|
||||
resolution: "safe-array-concat@npm:1.1.3"
|
||||
|
|
@ -18443,6 +18494,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"shell-quote@npm:1.8.3":
|
||||
version: 1.8.3
|
||||
resolution: "shell-quote@npm:1.8.3"
|
||||
checksum: 10c0/bee87c34e1e986cfb4c30846b8e6327d18874f10b535699866f368ade11ea4ee45433d97bf5eada22c4320c27df79c3a6a7eb1bf3ecfc47f2c997d9e5e2672fd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"shell-quote@npm:^1.6.1":
|
||||
version: 1.8.2
|
||||
resolution: "shell-quote@npm:1.8.2"
|
||||
|
|
@ -18864,7 +18922,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0":
|
||||
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3":
|
||||
version: 4.2.3
|
||||
resolution: "string-width@npm:4.2.3"
|
||||
dependencies:
|
||||
|
|
@ -19128,6 +19186,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"supports-color@npm:8.1.1":
|
||||
version: 8.1.1
|
||||
resolution: "supports-color@npm:8.1.1"
|
||||
dependencies:
|
||||
has-flag: "npm:^4.0.0"
|
||||
checksum: 10c0/ea1d3c275dd604c974670f63943ed9bd83623edc102430c05adb8efc56ba492746b6e95386e7831b872ec3807fd89dd8eb43f735195f37b5ec343e4234cc7e89
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"supports-color@npm:^5.3.0, supports-color@npm:^5.5.0":
|
||||
version: 5.5.0
|
||||
resolution: "supports-color@npm:5.5.0"
|
||||
|
|
@ -19438,6 +19505,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tree-kill@npm:1.2.2":
|
||||
version: 1.2.2
|
||||
resolution: "tree-kill@npm:1.2.2"
|
||||
bin:
|
||||
tree-kill: cli.js
|
||||
checksum: 10c0/7b1b7c7f17608a8f8d20a162e7957ac1ef6cd1636db1aba92f4e072dc31818c2ff0efac1e3d91064ede67ed5dc57c565420531a8134090a12ac10cf792ab14d2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"trim-lines@npm:^3.0.0":
|
||||
version: 3.0.1
|
||||
resolution: "trim-lines@npm:3.0.1"
|
||||
|
|
@ -20487,7 +20563,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "wrap-ansi@npm:7.0.0"
|
||||
dependencies:
|
||||
|
|
@ -20574,6 +20650,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"y18n@npm:^5.0.5":
|
||||
version: 5.0.8
|
||||
resolution: "y18n@npm:5.0.8"
|
||||
checksum: 10c0/4df2842c36e468590c3691c894bc9cdbac41f520566e76e24f59401ba7d8b4811eb1e34524d57e54bc6d864bcb66baab7ffd9ca42bf1eda596618f9162b91249
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yallist@npm:^3.0.2":
|
||||
version: 3.1.1
|
||||
resolution: "yallist@npm:3.1.1"
|
||||
|
|
@ -20604,6 +20687,28 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yargs-parser@npm:^21.1.1":
|
||||
version: 21.1.1
|
||||
resolution: "yargs-parser@npm:21.1.1"
|
||||
checksum: 10c0/f84b5e48169479d2f402239c59f084cfd1c3acc197a05c59b98bab067452e6b3ea46d4dd8ba2985ba7b3d32a343d77df0debd6b343e5dae3da2aab2cdf5886b2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yargs@npm:17.7.2":
|
||||
version: 17.7.2
|
||||
resolution: "yargs@npm:17.7.2"
|
||||
dependencies:
|
||||
cliui: "npm:^8.0.1"
|
||||
escalade: "npm:^3.1.1"
|
||||
get-caller-file: "npm:^2.0.5"
|
||||
require-directory: "npm:^2.1.1"
|
||||
string-width: "npm:^4.2.3"
|
||||
y18n: "npm:^5.0.5"
|
||||
yargs-parser: "npm:^21.1.1"
|
||||
checksum: 10c0/ccd7e723e61ad5965fffbb791366db689572b80cca80e0f96aad968dfff4156cd7cd1ad18607afe1046d8241e6fb2d6c08bf7fa7bfb5eaec818735d8feac8f05
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yocto-queue@npm:^0.1.0":
|
||||
version: 0.1.0
|
||||
resolution: "yocto-queue@npm:0.1.0"
|
||||
|
|
|
|||
Loading…
Reference in a new issue