Skip to content

Commit 1029b6c

Browse files
user syncing + represent sync job status in a seperate table
1 parent 6af031c commit 1029b6c

File tree

11 files changed

+488
-107
lines changed

11 files changed

+488
-107
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"watch:mcp": "yarn workspace @sourcebot/mcp build:watch",
1515
"watch:schemas": "yarn workspace @sourcebot/schemas watch",
1616
"dev:prisma:migrate:dev": "yarn with-env yarn workspace @sourcebot/db prisma:migrate:dev",
17+
"dev:prisma:generate": "yarn with-env yarn workspace @sourcebot/db prisma:generate",
1718
"dev:prisma:studio": "yarn with-env yarn workspace @sourcebot/db prisma:studio",
1819
"dev:prisma:migrate:reset": "yarn with-env yarn workspace @sourcebot/db prisma:migrate:reset",
1920
"dev:prisma:db:push": "yarn with-env yarn workspace @sourcebot/db prisma:db:push",

packages/backend/src/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@ export const DEFAULT_SETTINGS: Settings = {
1717
repoIndexTimeoutMs: 1000 * 60 * 60 * 2, // 2 hours
1818
enablePublicAccess: false // deprected, use FORCE_ENABLE_ANONYMOUS_ACCESS instead
1919
}
20+
21+
export const PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES = [
22+
'github',
23+
];

packages/backend/src/github.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,17 @@ export const getUserIdsWithReadAccessToRepo = async (owner: string, repo: string
129129
return collaborators.map(collaborator => collaborator.id.toString());
130130
}
131131

132+
export const getReposThatAuthenticatedUserHasReadAccessTo = async (octokit: Octokit) => {
133+
const fetchFn = () => octokit.paginate(octokit.repos.listForAuthenticatedUser, {
134+
per_page: 100,
135+
// @todo: do we need to set a visibility to private only?
136+
// visibility: 'private'
137+
});
138+
139+
const repos = await fetchWithRetry(fetchFn, `authenticated user`, logger);
140+
return repos.map(repo => repo.id.toString());
141+
}
142+
132143
export const createOctokitFromConfig = async (config: GithubConnectionConfig, orgId: number, db: PrismaClient): Promise<{ octokit: Octokit, isAuthenticated: boolean }> => {
133144
const hostname = config.url ?
134145
new URL(config.url).hostname :

packages/backend/src/index.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import path from 'path';
1010
import { ConnectionManager } from './connectionManager.js';
1111
import { DEFAULT_SETTINGS } from './constants.js';
1212
import { env } from "./env.js";
13-
import { RepoPermissionSyncer } from './permissionSyncer.js';
13+
import { RepoPermissionSyncer } from './repoPermissionSyncer.js';
1414
import { PromClient } from './promClient.js';
1515
import { RepoManager } from './repoManager.js';
1616
import { AppContext } from "./types.js";
17+
import { UserPermissionSyncer } from "./userPermissionSyncer.js";
1718

1819

1920
const logger = createLogger('backend-entrypoint');
@@ -68,28 +69,34 @@ const settings = await getSettings(env.CONFIG_PATH);
6869

6970
const connectionManager = new ConnectionManager(prisma, settings, redis);
7071
const repoManager = new RepoManager(prisma, settings, redis, promClient, context);
71-
const permissionSyncer = new RepoPermissionSyncer(prisma, redis);
72+
const repoPermissionSyncer = new RepoPermissionSyncer(prisma, redis);
73+
const userPermissionSyncer = new UserPermissionSyncer(prisma, redis);
7274

7375
await repoManager.validateIndexedReposHaveShards();
7476

7577
const connectionManagerInterval = connectionManager.startScheduler();
7678
const repoManagerInterval = repoManager.startScheduler();
77-
const permissionSyncerInterval = env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? permissionSyncer.startScheduler() : null;
79+
const repoPermissionSyncerInterval = env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? repoPermissionSyncer.startScheduler() : null;
80+
const userPermissionSyncerInterval = env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? userPermissionSyncer.startScheduler() : null;
7881

7982

8083
const cleanup = async (signal: string) => {
8184
logger.info(`Recieved ${signal}, cleaning up...`);
8285

83-
if (permissionSyncerInterval) {
84-
clearInterval(permissionSyncerInterval);
86+
if (userPermissionSyncerInterval) {
87+
clearInterval(userPermissionSyncerInterval);
88+
}
89+
if (repoPermissionSyncerInterval) {
90+
clearInterval(repoPermissionSyncerInterval);
8591
}
8692

8793
clearInterval(connectionManagerInterval);
8894
clearInterval(repoManagerInterval);
8995

9096
connectionManager.dispose();
9197
repoManager.dispose();
92-
permissionSyncer.dispose();
98+
repoPermissionSyncer.dispose();
99+
userPermissionSyncer.dispose();
93100

94101
await prisma.$disconnect();
95102
await redis.quit();

packages/backend/src/permissionSyncer.ts renamed to packages/backend/src/repoPermissionSyncer.ts

Lines changed: 94 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as Sentry from "@sentry/node";
2-
import { PrismaClient, Repo, RepoPermissionSyncStatus } from "@sourcebot/db";
2+
import { PrismaClient, Repo, RepoPermissionSyncJobStatus } from "@sourcebot/db";
33
import { createLogger } from "@sourcebot/logger";
44
import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/bitbucket.type";
55
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";
@@ -10,16 +10,16 @@ import { Redis } from 'ioredis';
1010
import { env } from "./env.js";
1111
import { createOctokitFromConfig, getUserIdsWithReadAccessToRepo } from "./github.js";
1212
import { RepoWithConnections } from "./types.js";
13+
import { PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES } from "./constants.js";
1314

1415
type RepoPermissionSyncJob = {
15-
repoId: number;
16+
jobId: string;
1617
}
1718

1819
const QUEUE_NAME = 'repoPermissionSyncQueue';
1920

20-
const logger = createLogger('permission-syncer');
21+
const logger = createLogger('repo-permission-syncer');
2122

22-
const SUPPORTED_CODE_HOST_TYPES = ['github'];
2323

2424
export class RepoPermissionSyncer {
2525
private queue: Queue<RepoPermissionSyncJob>;
@@ -46,47 +46,55 @@ export class RepoPermissionSyncer {
4646
return setInterval(async () => {
4747
// @todo: make this configurable
4848
const thresholdDate = new Date(Date.now() - 1000 * 60 * 60 * 24);
49+
4950
const repos = await this.db.repo.findMany({
5051
// Repos need their permissions to be synced against the code host when...
5152
where: {
5253
// They belong to a code host that supports permissions syncing
5354
AND: [
5455
{
5556
external_codeHostType: {
56-
in: SUPPORTED_CODE_HOST_TYPES,
57+
in: PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES,
5758
}
5859
},
59-
// and, they either require a sync (SYNC_NEEDED) or have been in a completed state (SYNCED or FAILED)
60-
// for > some duration (default 24 hours)
6160
{
6261
OR: [
63-
{
64-
permissionSyncStatus: RepoPermissionSyncStatus.SYNC_NEEDED
65-
},
66-
{
67-
AND: [
68-
{
69-
OR: [
70-
{ permissionSyncStatus: RepoPermissionSyncStatus.SYNCED },
71-
{ permissionSyncStatus: RepoPermissionSyncStatus.FAILED },
72-
]
73-
},
74-
{
75-
OR: [
76-
{ permissionSyncJobLastCompletedAt: null },
77-
{ permissionSyncJobLastCompletedAt: { lt: thresholdDate } }
78-
]
79-
}
80-
]
62+
{ permissionSyncedAt: null },
63+
{ permissionSyncedAt: { lt: thresholdDate } },
64+
],
65+
},
66+
{
67+
NOT: {
68+
permissionSyncJobs: {
69+
some: {
70+
OR: [
71+
// Don't schedule if there are active jobs
72+
{
73+
status: {
74+
in: [
75+
RepoPermissionSyncJobStatus.PENDING,
76+
RepoPermissionSyncJobStatus.IN_PROGRESS,
77+
],
78+
}
79+
},
80+
// Don't schedule if there are recent failed jobs (within the threshold date). Note `gt` is used here since this is a inverse condition.
81+
{
82+
AND: [
83+
{ status: RepoPermissionSyncJobStatus.FAILED },
84+
{ completedAt: { gt: thresholdDate } },
85+
]
86+
}
87+
]
88+
}
8189
}
82-
]
90+
}
8391
},
8492
]
8593
}
8694
});
8795

8896
await this.schedulePermissionSync(repos);
89-
}, 1000 * 30);
97+
}, 1000 * 5);
9098
}
9199

92100
public dispose() {
@@ -96,15 +104,16 @@ export class RepoPermissionSyncer {
96104

97105
private async schedulePermissionSync(repos: Repo[]) {
98106
await this.db.$transaction(async (tx) => {
99-
await tx.repo.updateMany({
100-
where: { id: { in: repos.map(repo => repo.id) } },
101-
data: { permissionSyncStatus: RepoPermissionSyncStatus.IN_SYNC_QUEUE },
107+
const jobs = await tx.repoPermissionSyncJob.createManyAndReturn({
108+
data: repos.map(repo => ({
109+
repoId: repo.id,
110+
})),
102111
});
103112

104-
await this.queue.addBulk(repos.map(repo => ({
113+
await this.queue.addBulk(jobs.map((job) => ({
105114
name: 'repoPermissionSyncJob',
106115
data: {
107-
repoId: repo.id,
116+
jobId: job.id,
108117
},
109118
opts: {
110119
removeOnComplete: env.REDIS_REMOVE_ON_COMPLETE,
@@ -115,21 +124,25 @@ export class RepoPermissionSyncer {
115124
}
116125

117126
private async runJob(job: Job<RepoPermissionSyncJob>) {
118-
const id = job.data.repoId;
119-
const repo = await this.db.repo.update({
127+
const id = job.data.jobId;
128+
const { repo } = await this.db.repoPermissionSyncJob.update({
120129
where: {
121-
id
130+
id,
122131
},
123132
data: {
124-
permissionSyncStatus: RepoPermissionSyncStatus.SYNCING,
133+
status: RepoPermissionSyncJobStatus.IN_PROGRESS,
125134
},
126-
include: {
127-
connections: {
135+
select: {
136+
repo: {
128137
include: {
129-
connection: true,
130-
},
131-
},
132-
},
138+
connections: {
139+
include: {
140+
connection: true,
141+
}
142+
}
143+
}
144+
}
145+
}
133146
});
134147

135148
if (!repo) {
@@ -171,34 +184,43 @@ export class RepoPermissionSyncer {
171184
return [];
172185
})();
173186

174-
await this.db.repo.update({
175-
where: {
176-
id: repo.id,
177-
},
178-
data: {
179-
permittedUsers: {
180-
deleteMany: {},
187+
await this.db.$transaction([
188+
this.db.repo.update({
189+
where: {
190+
id: repo.id,
191+
},
192+
data: {
193+
permittedUsers: {
194+
deleteMany: {},
195+
}
181196
}
182-
}
183-
});
184-
185-
await this.db.userToRepoPermission.createMany({
186-
data: userIds.map(userId => ({
187-
userId,
188-
repoId: repo.id,
189-
})),
190-
});
197+
}),
198+
this.db.userToRepoPermission.createMany({
199+
data: userIds.map(userId => ({
200+
userId,
201+
repoId: repo.id,
202+
})),
203+
})
204+
]);
191205
}
192206

193207
private async onJobCompleted(job: Job<RepoPermissionSyncJob>) {
194-
const repo = await this.db.repo.update({
208+
const { repo } = await this.db.repoPermissionSyncJob.update({
195209
where: {
196-
id: job.data.repoId,
210+
id: job.data.jobId,
197211
},
198212
data: {
199-
permissionSyncStatus: RepoPermissionSyncStatus.SYNCED,
200-
permissionSyncJobLastCompletedAt: new Date(),
213+
status: RepoPermissionSyncJobStatus.COMPLETED,
214+
repo: {
215+
update: {
216+
permissionSyncedAt: new Date(),
217+
}
218+
},
219+
completedAt: new Date(),
201220
},
221+
select: {
222+
repo: true
223+
}
202224
});
203225

204226
logger.info(`Permissions synced for repo ${repo.displayName ?? repo.name}`);
@@ -207,21 +229,25 @@ export class RepoPermissionSyncer {
207229
private async onJobFailed(job: Job<RepoPermissionSyncJob> | undefined, err: Error) {
208230
Sentry.captureException(err, {
209231
tags: {
210-
repoId: job?.data.repoId,
232+
jobId: job?.data.jobId,
211233
queue: QUEUE_NAME,
212234
}
213235
});
214236

215-
const errorMessage = (repoName: string) => `Repo permission sync job failed for repo ${repoName}: ${err}`;
237+
const errorMessage = (repoName: string) => `Repo permission sync job failed for repo ${repoName}: ${err.message}`;
216238

217239
if (job) {
218-
const repo = await this.db.repo.update({
240+
const { repo } = await this.db.repoPermissionSyncJob.update({
219241
where: {
220-
id: job?.data.repoId,
242+
id: job.data.jobId,
221243
},
222244
data: {
223-
permissionSyncStatus: RepoPermissionSyncStatus.FAILED,
224-
permissionSyncJobLastCompletedAt: new Date(),
245+
status: RepoPermissionSyncJobStatus.FAILED,
246+
completedAt: new Date(),
247+
errorMessage: err.message,
248+
},
249+
select: {
250+
repo: true
225251
},
226252
});
227253
logger.error(errorMessage(repo.displayName ?? repo.name));

0 commit comments

Comments
 (0)