Skip to content

add better visualization for connection/repo errors and warnings #201

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b8a9513
replace upsert with seperate create many and raw update many calls
msukkari Feb 17, 2025
176d02e
add bulk repo status update and queue addition with priority
msukkari Feb 18, 2025
f9c0849
add support for managed redis
msukkari Feb 18, 2025
372e3a4
add note for changing raw sql on schema change
msukkari Feb 18, 2025
ec231c3
add error package and use BackendException in connection manager
msukkari Feb 18, 2025
653b76c
handle connection failure display on web app
msukkari Feb 18, 2025
50c8107
add warning banner for not found orgs/repos/users
msukkari Feb 18, 2025
94f4edc
add failure handling for gerrit
msukkari Feb 18, 2025
a68b41f
add gitea notfound warning support
msukkari Feb 19, 2025
d0213d3
add warning icon in connections list
msukkari Feb 19, 2025
8772bcc
style nits
msukkari Feb 19, 2025
eaa01ba
add failed repo vis in connections list
msukkari Feb 19, 2025
0a95d4e
added retry failed repo index buttons
msukkari Feb 19, 2025
40d4ee5
move nav indicators to client with polling
msukkari Feb 19, 2025
771bda6
fix indicator flash issue and truncate large list results
msukkari Feb 19, 2025
b83e018
display error nav better
msukkari Feb 19, 2025
6c19695
truncate failed repo list in connection list item
msukkari Feb 19, 2025
c682af4
merge v3
msukkari Feb 19, 2025
48fe1fb
merge v3
msukkari Feb 19, 2025
729f853
fix merge error
msukkari Feb 19, 2025
710f289
fix merge bug
msukkari Feb 20, 2025
da5af4f
add connection util file [wip]
msukkari Feb 20, 2025
9898f4b
refactor notfound fetch logic and add missing error package to docker…
msukkari Feb 20, 2025
426e78d
move repeated logic to function and add zod schema for syncStatusMeta…
msukkari Feb 20, 2025
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
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ COPY package.json yarn.lock* ./
COPY ./packages/db ./packages/db
COPY ./packages/schemas ./packages/schemas
COPY ./packages/crypto ./packages/crypto
COPY ./packages/error ./packages/error
RUN yarn workspace @sourcebot/db install --frozen-lockfile
RUN yarn workspace @sourcebot/schemas install --frozen-lockfile
RUN yarn workspace @sourcebot/crypto install --frozen-lockfile
RUN yarn workspace @sourcebot/error install --frozen-lockfile

# ------ Build Web ------
FROM node-alpine AS web-builder
Expand All @@ -33,6 +35,7 @@ COPY --from=shared-libs-builder /app/node_modules ./node_modules
COPY --from=shared-libs-builder /app/packages/db ./packages/db
COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas
COPY --from=shared-libs-builder /app/packages/crypto ./packages/crypto
COPY --from=shared-libs-builder /app/packages/error ./packages/error

# Fixes arm64 timeouts
RUN yarn config set registry https://registry.npmjs.org/
Expand Down Expand Up @@ -66,6 +69,7 @@ COPY --from=shared-libs-builder /app/node_modules ./node_modules
COPY --from=shared-libs-builder /app/packages/db ./packages/db
COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas
COPY --from=shared-libs-builder /app/packages/crypto ./packages/crypto
COPY --from=shared-libs-builder /app/packages/error ./packages/error
RUN yarn workspace @sourcebot/backend install --frozen-lockfile
RUN yarn workspace @sourcebot/backend build

Expand Down Expand Up @@ -138,6 +142,7 @@ COPY --from=shared-libs-builder /app/node_modules ./node_modules
COPY --from=shared-libs-builder /app/packages/db ./packages/db
COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas
COPY --from=shared-libs-builder /app/packages/crypto ./packages/crypto
COPY --from=shared-libs-builder /app/packages/error ./packages/error

# Configure the database
RUN mkdir -p /run/postgresql && \
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ clean:
packages/schemas/dist \
packages/crypto/node_modules \
packages/crypto/dist \
packages/error/node_modules \
packages/error/dist \
.sourcebot

soft-reset:
Expand Down
1 change: 1 addition & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@sourcebot/crypto": "^0.1.0",
"@sourcebot/db": "^0.1.0",
"@sourcebot/schemas": "^0.1.0",
"@sourcebot/error": "^0.1.0",
"simple-git": "^3.27.0",
"strip-json-comments": "^5.0.1",
"winston": "^3.15.0",
Expand Down
127 changes: 108 additions & 19 deletions packages/backend/src/connectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createLogger } from "./logger.js";
import os from 'os';
import { Redis } from 'ioredis';
import { RepoData, compileGithubConfig, compileGitlabConfig, compileGiteaConfig, compileGerritConfig } from "./repoCompileUtils.js";
import { BackendError, BackendException } from "@sourcebot/error";

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

const repoData: RepoData[] = await (async () => {
switch (config.type) {
case 'github': {
return await compileGithubConfig(config, job.data.connectionId, orgId, this.db, abortController);
}
case 'gitlab': {
return await compileGitlabConfig(config, job.data.connectionId, orgId, this.db);
}
case 'gitea': {
return await compileGiteaConfig(config, job.data.connectionId, orgId, this.db);
}
case 'gerrit': {
return await compileGerritConfig(config, job.data.connectionId, orgId);
}
default: {
return [];
const connection = await this.db.connection.findUnique({
where: {
id: job.data.connectionId,
},
});

if (!connection) {
throw new BackendException(BackendError.CONNECTION_SYNC_CONNECTION_NOT_FOUND, {
message: `Connection ${job.data.connectionId} not found`,
});
}

// Reset the syncStatusMetadata to an empty object at the start of the sync job
await this.db.connection.update({
where: {
id: job.data.connectionId,
},
data: {
syncStatusMetadata: {}
}
})


let result: {
repoData: RepoData[],
notFound: {
users: string[],
orgs: string[],
repos: string[],
}
} = {
repoData: [],
notFound: {
users: [],
orgs: [],
repos: [],
}
};

try {
result = await (async () => {
switch (config.type) {
case 'github': {
return await compileGithubConfig(config, job.data.connectionId, orgId, this.db, abortController);
}
case 'gitlab': {
return await compileGitlabConfig(config, job.data.connectionId, orgId, this.db);
}
case 'gitea': {
return await compileGiteaConfig(config, job.data.connectionId, orgId, this.db);
}
case 'gerrit': {
return await compileGerritConfig(config, job.data.connectionId, orgId);
}
default: {
return {repoData: [], notFound: {
users: [],
orgs: [],
repos: [],
}};
}
}
})();
} catch (err) {
this.logger.error(`Failed to compile repo data for connection ${job.data.connectionId}: ${err}`);
if (err instanceof BackendException) {
throw err;
} else {
throw new BackendException(BackendError.CONNECTION_SYNC_SYSTEM_ERROR, {
message: `Failed to compile repo data for connection ${job.data.connectionId}`,
});
}
})();
}

const { repoData, notFound } = result;

// Push the information regarding not found users, orgs, and repos to the connection's syncStatusMetadata. Note that
// this won't be overwritten even if the connection job fails
await this.db.connection.update({
where: {
id: job.data.connectionId,
},
data: {
syncStatusMetadata: { notFound }
}
});

// Filter out any duplicates by external_id and external_codeHostUrl.
repoData.filter((repo, index, self) => {
return index === self.findIndex(r =>
Expand Down Expand Up @@ -265,16 +333,37 @@ export class ConnectionManager implements IConnectionManager {
private async onSyncJobFailed(job: Job | undefined, err: unknown) {
this.logger.info(`Connection sync job failed with error: ${err}`);
if (job) {

// We may have pushed some metadata during the execution of the job, so we make sure to not overwrite the metadata here
const { connectionId } = job.data;
let syncStatusMetadata: Record<string, unknown> = (await this.db.connection.findUnique({
where: { id: connectionId },
select: { syncStatusMetadata: true }
}))?.syncStatusMetadata as Record<string, unknown> ?? {};

if (err instanceof BackendException) {
syncStatusMetadata = {
...syncStatusMetadata,
error: err.code,
...err.metadata,
}
} else {
syncStatusMetadata = {
...syncStatusMetadata,
error: 'UNKNOWN',
}
}

await this.db.connection.update({
where: {
id: connectionId,
},
data: {
syncStatus: ConnectionSyncStatus.FAILED,
syncedAt: new Date()
syncedAt: new Date(),
syncStatusMetadata: syncStatusMetadata as Prisma.InputJsonValue,
}
})
});
}
}

Expand Down
44 changes: 44 additions & 0 deletions packages/backend/src/connectionUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
type ValidResult<T> = {
type: 'valid';
data: T[];
};

type NotFoundResult = {
type: 'notFound';
value: string;
};

type CustomResult<T> = ValidResult<T> | NotFoundResult;

export function processPromiseResults<T>(
results: PromiseSettledResult<CustomResult<T>>[],
): {
validItems: T[];
notFoundItems: string[];
} {
const validItems: T[] = [];
const notFoundItems: string[] = [];

results.forEach(result => {
if (result.status === 'fulfilled') {
const value = result.value;
if (value.type === 'valid') {
validItems.push(...value.data);
} else {
notFoundItems.push(value.value);
}
}
});

return {
validItems,
notFoundItems,
};
}

export function throwIfAnyFailed<T>(results: PromiseSettledResult<T>[]) {
const failedResult = results.find(result => result.status === 'rejected');
if (failedResult) {
throw failedResult.reason;
}
}
28 changes: 25 additions & 3 deletions packages/backend/src/gerrit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { GerritConfig } from "@sourcebot/schemas/v2/index.type"
import { createLogger } from './logger.js';
import micromatch from "micromatch";
import { measure, marshalBool, excludeReposByName, includeReposByName, fetchWithRetry } from './utils.js';
import { BackendError } from '@sourcebot/error';
import { BackendException } from '@sourcebot/error';

// https://gerrit-review.googlesource.com/Documentation/rest-api.html
interface GerritProjects {
Expand Down Expand Up @@ -38,6 +40,10 @@ export const getGerritReposFromConfig = async (config: GerritConfig): Promise<Ge
const fetchFn = () => fetchAllProjects(url);
return fetchWithRetry(fetchFn, `projects from ${url}`, logger);
} catch (err) {
if (err instanceof BackendException) {
throw err;
}

logger.error(`Failed to fetch projects from ${url}`, err);
return null;
}
Expand Down Expand Up @@ -78,9 +84,25 @@ const fetchAllProjects = async (url: string): Promise<GerritProject[]> => {
const endpointWithParams = `${projectsEndpoint}?S=${start}`;
logger.debug(`Fetching projects from Gerrit at ${endpointWithParams}`);

const response = await fetch(endpointWithParams);
if (!response.ok) {
throw new Error(`Failed to fetch projects from Gerrit: ${response.statusText}`);
let response: Response;
try {
response = await fetch(endpointWithParams);
if (!response.ok) {
console.log(`Failed to fetch projects from Gerrit at ${endpointWithParams} with status ${response.status}`);
throw new BackendException(BackendError.CONNECTION_SYNC_FAILED_TO_FETCH_GERRIT_PROJECTS, {
status: response.status,
});
}
} catch (err) {
if (err instanceof BackendException) {
throw err;
}

const status = (err as any).code;
console.log(`Failed to fetch projects from Gerrit at ${endpointWithParams} with status ${status}`);
throw new BackendException(BackendError.CONNECTION_SYNC_FAILED_TO_FETCH_GERRIT_PROJECTS, {
status: status,
});
}

const text = await response.text();
Expand Down
Loading