[SILO-717] fix: add ttl and different key for issue sync race condition #4887

This commit is contained in:
Saurabh Kumar
2025-12-03 14:24:40 +05:30
committed by GitHub
parent 0e8f171b9c
commit bfb430e5f5
4 changed files with 40 additions and 27 deletions
@@ -22,15 +22,16 @@ export type GithubCommentWebhookPayload = GithubWebhookPayload[
};
export const handleIssueComment = async (store: Store, action: GithubCommentAction, data: unknown) => {
// Check if this webhook was triggered by our own Plane->GitHub sync (loop prevention)
// @ts-expect-error
if (data && data.comment && data.comment.id) {
// @ts-expect-error
const exist = await store.get(`silo:comment:${data.comment.id}`);
const exist = await store.get(`silo:comment:gh:${data.comment.id}`);
if (exist) {
logger.info(`[ISSUE-COMMENT] Event Processed Successfully, confirmed by target`);
// Remove the webhook from the store
logger.info(`[ISSUE-COMMENT] Event triggered by Plane->GitHub sync, skipping to prevent loop`);
// Remove the key so future legitimate webhooks are not blocked
// @ts-expect-error
await store.del(`silo:comment:${data.comment.id}`);
await store.del(`silo:comment:gh:${data.comment.id}`);
return true;
}
}
@@ -169,7 +170,9 @@ export const syncCommentWithPlane = async (
comment.id,
planeComment
);
await store.set(`silo:comment:${comment.id}`, "true");
// Set key with Plane comment ID so Plane->GitHub handler can detect and skip
// Use 5 second TTL to allow the webhook loop back but expire quickly
await store.set(`silo:comment:plane:${comment.id}`, "true", 5);
} else {
const createdComment = await planeClient.issueComment.create(
workspaceConnection.workspace_slug,
@@ -177,7 +180,9 @@ export const syncCommentWithPlane = async (
issue.id,
planeComment
);
await store.set(`silo:comment:${createdComment.id}`, "true");
// Set key with Plane comment ID so Plane->GitHub handler can detect and skip
// Use 5 second TTL to allow the webhook loop back but expire quickly
await store.set(`silo:comment:plane:${createdComment.id}`, "true", 5);
}
};
@@ -18,16 +18,17 @@ import type { Store } from "@/worker/base";
const SYNC_LABEL = "plane";
export const handleIssueEvents = async (store: Store, action: string, data: unknown) => {
// If the issue number exist inside the store, skip it
// Check if this webhook was triggered by our own Plane->GitHub sync (loop prevention)
// The Plane->GitHub handler sets a temporary key right after syncing to GitHub
// @ts-expect-error
if (data && data.issueNumber) {
// @ts-expect-error
const exist = await store.get(`silo:issue:${data.issueNumber}`);
const exist = await store.get(`silo:issue:gh:${data.issueNumber}`);
if (exist) {
logger.info(`[GITHUB][ISSUE] Event Processed Successfully, confirmed by target`);
// Remove the webhook from the store
logger.info(`[GITHUB][ISSUE] Event triggered by Plane->GitHub sync, skipping to prevent loop`);
// Remove the key so future legitimate webhooks are not blocked
// @ts-expect-error
await store.del(`silo:issue:${data.issueNumber}`);
await store.del(`silo:issue:gh:${data.issueNumber}`);
return true;
}
}
@@ -205,7 +206,9 @@ export const syncIssueWithPlane = async (store: Store, action: IssueWebhookActio
issue.id,
planeIssue
);
await store.set(`silo:issue:${issue.id}`, "true");
// Set key with Plane issue ID so Plane->GitHub handler can detect and skip
// Use 5 second TTL to allow the webhook loop back but expire quickly
await store.set(`silo:issue:plane:${issue.id}`, "true", 5);
} else {
const createdIssue = await planeClient.issue.create(
entityConnection.workspace_slug,
@@ -241,7 +244,9 @@ export const syncIssueWithPlane = async (store: Store, action: IssueWebhookActio
await ghService.createIssueComment(data.owner, data.repositoryName, Number(data.issueNumber), comment);
};
await Promise.all([createLink(), createLinkBack(), store.set(`silo:issue:${createdIssue.id}`, "true")]);
// Set key with Plane issue ID so Plane->GitHub handler can detect and skip
// Use 5 second TTL to allow the webhook loop back but expire quickly
await Promise.all([createLink(), createLinkBack(), store.set(`silo:issue:plane:${createdIssue.id}`, "true", 5)]);
}
} catch (error) {
logger.error("Error syncing issue with Plane", error);
@@ -23,14 +23,13 @@ export const handleIssueCommentWebhook = async (
) => {
const payloadId = payload?.id ?? "";
// Store a key associated with the data in the store
// If the key is present, then we need to requeue all that task and process them again
// Check if this webhook was triggered by our own GitHub->Plane sync (loop prevention)
if (payloadId) {
const exist = await store.get(`silo:comment:${payload.id}`);
const exist = await store.get(`silo:comment:plane:${payload.id}`);
if (exist) {
logger.info("[PLANE][COMMENT] Event Processed Successfully, confirmed by target");
// Remove the webhook from the store
await store.del(`silo:comment:${payload.id}`);
logger.info("[PLANE][COMMENT] Event triggered by GitHub->Plane sync, skipping to prevent loop");
// Remove the key so future legitimate webhooks are not blocked
await store.del(`silo:comment:plane:${payload.id}`);
return true;
}
}
@@ -130,7 +129,9 @@ const handleCommentSync = async (store: Store, payload: PlaneWebhookPayload) =>
credentials,
ghIntegrationKey
);
await store.set(`silo:comment:${githubComment.data.id}`, "true");
// Set key with GitHub comment ID so GitHub->Plane handler can detect and skip
// Use 5 second TTL to allow the webhook loop back but expire quickly
await store.set(`silo:comment:gh:${githubComment.data.id}`, "true", 5);
if (
!comment.external_id ||
@@ -20,13 +20,14 @@ const apiClient = getAPIClient();
export const imagePrefix = encodeURI(env.SILO_API_BASE_URL + env.SILO_BASE_PATH + "/api/assets/github/");
export const handleIssueWebhook = async (headers: TaskHeaders, mq: MQ, store: Store, payload: PlaneWebhookPayload) => {
// Check for the key in the store, if the key is present, then the issue is already synced
// Check if this webhook was triggered by our own GitHub->Plane sync (loop prevention)
// The GitHub->Plane handler sets a temporary key with the Plane issue ID
if (payload && payload.id) {
const exist = await store.get(`silo:issue:${payload.id}`);
const exist = await store.get(`silo:issue:plane:${payload.id}`);
if (exist) {
logger.info("[PLANE][ISSUE] Event Processed Successfully, confirmed by target");
// Remove the webhook from the store
await store.del(`silo:issue:${payload.id}`);
logger.info("[PLANE][ISSUE] Event triggered by GitHub->Plane sync, skipping to prevent loop");
// Remove the key so future legitimate webhooks are not blocked
await store.del(`silo:issue:plane:${payload.id}`);
return true;
}
}
@@ -127,8 +128,9 @@ const handleIssueSync = async (store: Store, payload: PlaneWebhookPayload) => {
await Promise.all([addExternalId(), createLink()]);
}
// Add the issue number to the store
await store.set(`silo:issue:${githubIssue?.data.number}`, "true");
// Set key with GitHub issue number so GitHub->Plane handler can detect and skip
// Use 5 second TTL to allow the webhook loop back but expire quickly
await store.set(`silo:issue:gh:${githubIssue?.data.number}`, "true", 5);
} catch (error) {
logger.error("[Plane][Github] Error handling issue create/update event", {
error,