Skip to content

Commit fdd71cf

Browse files
authored
add better visualization for connection/repo errors and warnings (#201)
* replace upsert with seperate create many and raw update many calls * add bulk repo status update and queue addition with priority * add support for managed redis * add note for changing raw sql on schema change * add error package and use BackendException in connection manager * handle connection failure display on web app * add warning banner for not found orgs/repos/users * add failure handling for gerrit * add gitea notfound warning support * add warning icon in connections list * style nits * add failed repo vis in connections list * added retry failed repo index buttons * move nav indicators to client with polling * fix indicator flash issue and truncate large list results * display error nav better * truncate failed repo list in connection list item * fix merge error * fix merge bug * add connection util file [wip] * refactor notfound fetch logic and add missing error package to dockerfile * move repeated logic to function and add zod schema for syncStatusMetadata
1 parent b99a648 commit fdd71cf

39 files changed

+1735
-374
lines changed

Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ COPY package.json yarn.lock* ./
1818
COPY ./packages/db ./packages/db
1919
COPY ./packages/schemas ./packages/schemas
2020
COPY ./packages/crypto ./packages/crypto
21+
COPY ./packages/error ./packages/error
2122
RUN yarn workspace @sourcebot/db install --frozen-lockfile
2223
RUN yarn workspace @sourcebot/schemas install --frozen-lockfile
2324
RUN yarn workspace @sourcebot/crypto install --frozen-lockfile
25+
RUN yarn workspace @sourcebot/error install --frozen-lockfile
2426

2527
# ------ Build Web ------
2628
FROM node-alpine AS web-builder
@@ -33,6 +35,7 @@ COPY --from=shared-libs-builder /app/node_modules ./node_modules
3335
COPY --from=shared-libs-builder /app/packages/db ./packages/db
3436
COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas
3537
COPY --from=shared-libs-builder /app/packages/crypto ./packages/crypto
38+
COPY --from=shared-libs-builder /app/packages/error ./packages/error
3639

3740
# Fixes arm64 timeouts
3841
RUN yarn config set registry https://registry.npmjs.org/
@@ -66,6 +69,7 @@ COPY --from=shared-libs-builder /app/node_modules ./node_modules
6669
COPY --from=shared-libs-builder /app/packages/db ./packages/db
6770
COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas
6871
COPY --from=shared-libs-builder /app/packages/crypto ./packages/crypto
72+
COPY --from=shared-libs-builder /app/packages/error ./packages/error
6973
RUN yarn workspace @sourcebot/backend install --frozen-lockfile
7074
RUN yarn workspace @sourcebot/backend build
7175

@@ -138,6 +142,7 @@ COPY --from=shared-libs-builder /app/node_modules ./node_modules
138142
COPY --from=shared-libs-builder /app/packages/db ./packages/db
139143
COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas
140144
COPY --from=shared-libs-builder /app/packages/crypto ./packages/crypto
145+
COPY --from=shared-libs-builder /app/packages/error ./packages/error
141146

142147
# Configure the database
143148
RUN mkdir -p /run/postgresql && \

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ clean:
2626
packages/schemas/dist \
2727
packages/crypto/node_modules \
2828
packages/crypto/dist \
29+
packages/error/node_modules \
30+
packages/error/dist \
2931
.sourcebot
3032

3133
soft-reset:

packages/backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@sourcebot/crypto": "^0.1.0",
3535
"@sourcebot/db": "^0.1.0",
3636
"@sourcebot/schemas": "^0.1.0",
37+
"@sourcebot/error": "^0.1.0",
3738
"simple-git": "^3.27.0",
3839
"strip-json-comments": "^5.0.1",
3940
"winston": "^3.15.0",

packages/backend/src/connectionManager.ts

Lines changed: 108 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createLogger } from "./logger.js";
66
import os from 'os';
77
import { Redis } from 'ioredis';
88
import { RepoData, compileGithubConfig, compileGitlabConfig, compileGiteaConfig, compileGerritConfig } from "./repoCompileUtils.js";
9+
import { BackendError, BackendException } from "@sourcebot/error";
910

1011
interface IConnectionManager {
1112
scheduleConnectionSync: (connection: Connection) => Promise<void>;
@@ -81,26 +82,93 @@ export class ConnectionManager implements IConnectionManager {
8182
// @note: We aren't actually doing anything with this atm.
8283
const abortController = new AbortController();
8384

84-
const repoData: RepoData[] = await (async () => {
85-
switch (config.type) {
86-
case 'github': {
87-
return await compileGithubConfig(config, job.data.connectionId, orgId, this.db, abortController);
88-
}
89-
case 'gitlab': {
90-
return await compileGitlabConfig(config, job.data.connectionId, orgId, this.db);
91-
}
92-
case 'gitea': {
93-
return await compileGiteaConfig(config, job.data.connectionId, orgId, this.db);
94-
}
95-
case 'gerrit': {
96-
return await compileGerritConfig(config, job.data.connectionId, orgId);
97-
}
98-
default: {
99-
return [];
85+
const connection = await this.db.connection.findUnique({
86+
where: {
87+
id: job.data.connectionId,
88+
},
89+
});
90+
91+
if (!connection) {
92+
throw new BackendException(BackendError.CONNECTION_SYNC_CONNECTION_NOT_FOUND, {
93+
message: `Connection ${job.data.connectionId} not found`,
94+
});
95+
}
96+
97+
// Reset the syncStatusMetadata to an empty object at the start of the sync job
98+
await this.db.connection.update({
99+
where: {
100+
id: job.data.connectionId,
101+
},
102+
data: {
103+
syncStatusMetadata: {}
104+
}
105+
})
106+
107+
108+
let result: {
109+
repoData: RepoData[],
110+
notFound: {
111+
users: string[],
112+
orgs: string[],
113+
repos: string[],
114+
}
115+
} = {
116+
repoData: [],
117+
notFound: {
118+
users: [],
119+
orgs: [],
120+
repos: [],
121+
}
122+
};
123+
124+
try {
125+
result = await (async () => {
126+
switch (config.type) {
127+
case 'github': {
128+
return await compileGithubConfig(config, job.data.connectionId, orgId, this.db, abortController);
129+
}
130+
case 'gitlab': {
131+
return await compileGitlabConfig(config, job.data.connectionId, orgId, this.db);
132+
}
133+
case 'gitea': {
134+
return await compileGiteaConfig(config, job.data.connectionId, orgId, this.db);
135+
}
136+
case 'gerrit': {
137+
return await compileGerritConfig(config, job.data.connectionId, orgId);
138+
}
139+
default: {
140+
return {repoData: [], notFound: {
141+
users: [],
142+
orgs: [],
143+
repos: [],
144+
}};
145+
}
100146
}
147+
})();
148+
} catch (err) {
149+
this.logger.error(`Failed to compile repo data for connection ${job.data.connectionId}: ${err}`);
150+
if (err instanceof BackendException) {
151+
throw err;
152+
} else {
153+
throw new BackendException(BackendError.CONNECTION_SYNC_SYSTEM_ERROR, {
154+
message: `Failed to compile repo data for connection ${job.data.connectionId}`,
155+
});
101156
}
102-
})();
157+
}
158+
159+
const { repoData, notFound } = result;
103160

161+
// Push the information regarding not found users, orgs, and repos to the connection's syncStatusMetadata. Note that
162+
// this won't be overwritten even if the connection job fails
163+
await this.db.connection.update({
164+
where: {
165+
id: job.data.connectionId,
166+
},
167+
data: {
168+
syncStatusMetadata: { notFound }
169+
}
170+
});
171+
104172
// Filter out any duplicates by external_id and external_codeHostUrl.
105173
repoData.filter((repo, index, self) => {
106174
return index === self.findIndex(r =>
@@ -265,16 +333,37 @@ export class ConnectionManager implements IConnectionManager {
265333
private async onSyncJobFailed(job: Job | undefined, err: unknown) {
266334
this.logger.info(`Connection sync job failed with error: ${err}`);
267335
if (job) {
336+
337+
// We may have pushed some metadata during the execution of the job, so we make sure to not overwrite the metadata here
268338
const { connectionId } = job.data;
339+
let syncStatusMetadata: Record<string, unknown> = (await this.db.connection.findUnique({
340+
where: { id: connectionId },
341+
select: { syncStatusMetadata: true }
342+
}))?.syncStatusMetadata as Record<string, unknown> ?? {};
343+
344+
if (err instanceof BackendException) {
345+
syncStatusMetadata = {
346+
...syncStatusMetadata,
347+
error: err.code,
348+
...err.metadata,
349+
}
350+
} else {
351+
syncStatusMetadata = {
352+
...syncStatusMetadata,
353+
error: 'UNKNOWN',
354+
}
355+
}
356+
269357
await this.db.connection.update({
270358
where: {
271359
id: connectionId,
272360
},
273361
data: {
274362
syncStatus: ConnectionSyncStatus.FAILED,
275-
syncedAt: new Date()
363+
syncedAt: new Date(),
364+
syncStatusMetadata: syncStatusMetadata as Prisma.InputJsonValue,
276365
}
277-
})
366+
});
278367
}
279368
}
280369

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
type ValidResult<T> = {
2+
type: 'valid';
3+
data: T[];
4+
};
5+
6+
type NotFoundResult = {
7+
type: 'notFound';
8+
value: string;
9+
};
10+
11+
type CustomResult<T> = ValidResult<T> | NotFoundResult;
12+
13+
export function processPromiseResults<T>(
14+
results: PromiseSettledResult<CustomResult<T>>[],
15+
): {
16+
validItems: T[];
17+
notFoundItems: string[];
18+
} {
19+
const validItems: T[] = [];
20+
const notFoundItems: string[] = [];
21+
22+
results.forEach(result => {
23+
if (result.status === 'fulfilled') {
24+
const value = result.value;
25+
if (value.type === 'valid') {
26+
validItems.push(...value.data);
27+
} else {
28+
notFoundItems.push(value.value);
29+
}
30+
}
31+
});
32+
33+
return {
34+
validItems,
35+
notFoundItems,
36+
};
37+
}
38+
39+
export function throwIfAnyFailed<T>(results: PromiseSettledResult<T>[]) {
40+
const failedResult = results.find(result => result.status === 'rejected');
41+
if (failedResult) {
42+
throw failedResult.reason;
43+
}
44+
}

packages/backend/src/gerrit.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { GerritConfig } from "@sourcebot/schemas/v2/index.type"
33
import { createLogger } from './logger.js';
44
import micromatch from "micromatch";
55
import { measure, marshalBool, excludeReposByName, includeReposByName, fetchWithRetry } from './utils.js';
6+
import { BackendError } from '@sourcebot/error';
7+
import { BackendException } from '@sourcebot/error';
68

79
// https://gerrit-review.googlesource.com/Documentation/rest-api.html
810
interface GerritProjects {
@@ -38,6 +40,10 @@ export const getGerritReposFromConfig = async (config: GerritConfig): Promise<Ge
3840
const fetchFn = () => fetchAllProjects(url);
3941
return fetchWithRetry(fetchFn, `projects from ${url}`, logger);
4042
} catch (err) {
43+
if (err instanceof BackendException) {
44+
throw err;
45+
}
46+
4147
logger.error(`Failed to fetch projects from ${url}`, err);
4248
return null;
4349
}
@@ -78,9 +84,25 @@ const fetchAllProjects = async (url: string): Promise<GerritProject[]> => {
7884
const endpointWithParams = `${projectsEndpoint}?S=${start}`;
7985
logger.debug(`Fetching projects from Gerrit at ${endpointWithParams}`);
8086

81-
const response = await fetch(endpointWithParams);
82-
if (!response.ok) {
83-
throw new Error(`Failed to fetch projects from Gerrit: ${response.statusText}`);
87+
let response: Response;
88+
try {
89+
response = await fetch(endpointWithParams);
90+
if (!response.ok) {
91+
console.log(`Failed to fetch projects from Gerrit at ${endpointWithParams} with status ${response.status}`);
92+
throw new BackendException(BackendError.CONNECTION_SYNC_FAILED_TO_FETCH_GERRIT_PROJECTS, {
93+
status: response.status,
94+
});
95+
}
96+
} catch (err) {
97+
if (err instanceof BackendException) {
98+
throw err;
99+
}
100+
101+
const status = (err as any).code;
102+
console.log(`Failed to fetch projects from Gerrit at ${endpointWithParams} with status ${status}`);
103+
throw new BackendException(BackendError.CONNECTION_SYNC_FAILED_TO_FETCH_GERRIT_PROJECTS, {
104+
status: status,
105+
});
84106
}
85107

86108
const text = await response.text();

0 commit comments

Comments
 (0)