aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2026-02-08 13:07:16 +0000
committerGitHub <noreply@github.com>2026-02-08 13:07:16 +0000
commit960ca9b67915408f26b825886f2b6c6481a658dc (patch)
tree2de224f2b3efaedeba3c979f1bad0ace9e658f15
parent1a01f75d35abb4aa02edee22e1e5640d3f0aa27c (diff)
downloadkarakeep-960ca9b67915408f26b825886f2b6c6481a658dc.tar.zst
fix: treat bookmark not found as a no-op in rule engine instead of a failure (#2464)
When a bookmark is deleted before the rule engine worker processes its event, the worker would throw an error, triggering failure metrics, error logging, and retries. This changes both the worker and RuleEngine.forBookmark to gracefully skip processing with an info log instead. Co-authored-by: Claude <noreply@anthropic.com>
-rw-r--r--apps/workers/workers/ruleEngineWorker.ts11
-rw-r--r--packages/trpc/lib/__tests__/ruleEngine.test.ts21
-rw-r--r--packages/trpc/lib/ruleEngine.ts7
3 files changed, 24 insertions, 15 deletions
diff --git a/apps/workers/workers/ruleEngineWorker.ts b/apps/workers/workers/ruleEngineWorker.ts
index 00e20127..ecf733cd 100644
--- a/apps/workers/workers/ruleEngineWorker.ts
+++ b/apps/workers/workers/ruleEngineWorker.ts
@@ -67,14 +67,21 @@ async function runRuleEngine(job: DequeuedJob<ZRuleEngineRequest>) {
const bookmark = await getBookmarkUserId(bookmarkId);
if (!bookmark) {
- throw new Error(
- `[ruleEngine][${jobId}] bookmark with id ${bookmarkId} was not found`,
+ logger.info(
+ `[ruleEngine][${jobId}] bookmark with id ${bookmarkId} was not found, skipping`,
);
+ return;
}
const userId = bookmark.userId;
const authedCtx = await buildImpersonatingAuthedContext(userId);
const ruleEngine = await RuleEngine.forBookmark(authedCtx, bookmarkId);
+ if (!ruleEngine) {
+ logger.info(
+ `[ruleEngine][${jobId}] bookmark with id ${bookmarkId} was not found during rule evaluation, skipping`,
+ );
+ return;
+ }
const results = (
await Promise.all(events.map((event) => ruleEngine.onEvent(event)))
diff --git a/packages/trpc/lib/__tests__/ruleEngine.test.ts b/packages/trpc/lib/__tests__/ruleEngine.test.ts
index d7f216e5..b737e3a5 100644
--- a/packages/trpc/lib/__tests__/ruleEngine.test.ts
+++ b/packages/trpc/lib/__tests__/ruleEngine.test.ts
@@ -172,10 +172,9 @@ describe("RuleEngine", () => {
expect(engine).toBeInstanceOf(RuleEngine);
});
- it("should throw an error if bookmark is not found", async () => {
- await expect(
- RuleEngine.forBookmark(ctx, "nonexistent-bookmark"),
- ).rejects.toThrow("Bookmark nonexistent-bookmark not found");
+ it("should return null if bookmark is not found", async () => {
+ const engine = await RuleEngine.forBookmark(ctx, "nonexistent-bookmark");
+ expect(engine).toBeNull();
});
it("should load rules associated with the bookmark's user", async () => {
@@ -189,7 +188,7 @@ describe("RuleEngine", () => {
actions: [{ type: "addTag", tagId: tagId2 }],
});
- const engine = await RuleEngine.forBookmark(ctx, bookmarkId);
+ const engine = (await RuleEngine.forBookmark(ctx, bookmarkId))!;
// @ts-expect-error Accessing private property for test verification
expect(engine.rules).toHaveLength(1);
// @ts-expect-error Accessing private property for test verification
@@ -201,7 +200,7 @@ describe("RuleEngine", () => {
let engine: RuleEngine;
beforeEach(async () => {
- engine = await RuleEngine.forBookmark(ctx, bookmarkId);
+ engine = (await RuleEngine.forBookmark(ctx, bookmarkId))!;
});
it("should return true for urlContains condition", () => {
@@ -320,7 +319,7 @@ describe("RuleEngine", () => {
.update(bookmarks)
.set({ favourited: true })
.where(eq(bookmarks.id, bookmarkId));
- const updatedEngine = await RuleEngine.forBookmark(ctx, bookmarkId);
+ const updatedEngine = (await RuleEngine.forBookmark(ctx, bookmarkId))!;
const condition: RuleEngineCondition = { type: "isFavourited" };
expect(updatedEngine.doesBookmarkMatchConditions(condition)).toBe(true);
});
@@ -335,7 +334,7 @@ describe("RuleEngine", () => {
.update(bookmarks)
.set({ archived: true })
.where(eq(bookmarks.id, bookmarkId));
- const updatedEngine = await RuleEngine.forBookmark(ctx, bookmarkId);
+ const updatedEngine = (await RuleEngine.forBookmark(ctx, bookmarkId))!;
const condition: RuleEngineCondition = { type: "isArchived" };
expect(updatedEngine.doesBookmarkMatchConditions(condition)).toBe(true);
});
@@ -403,7 +402,7 @@ describe("RuleEngine", () => {
} as Omit<RuleEngineRule, "id"> & { userId: string };
ruleId = await seedRule(tmp);
testRule = { ...tmp, id: ruleId };
- engine = await RuleEngine.forBookmark(ctx, bookmarkId);
+ engine = (await RuleEngine.forBookmark(ctx, bookmarkId))!;
});
it("should evaluate rule successfully when event and conditions match", async () => {
@@ -492,7 +491,7 @@ describe("RuleEngine", () => {
let engine: RuleEngine;
beforeEach(async () => {
- engine = await RuleEngine.forBookmark(ctx, bookmarkId);
+ engine = (await RuleEngine.forBookmark(ctx, bookmarkId))!;
});
it("should execute addTag action", async () => {
@@ -674,7 +673,7 @@ describe("RuleEngine", () => {
actions: [{ type: "addToList", listId: listId1 }],
});
- engine = await RuleEngine.forBookmark(ctx, bookmarkId);
+ engine = (await RuleEngine.forBookmark(ctx, bookmarkId))!;
});
it("should process event and return only results for matching, enabled rules", async () => {
diff --git a/packages/trpc/lib/ruleEngine.ts b/packages/trpc/lib/ruleEngine.ts
index 233a6acf..acfd747e 100644
--- a/packages/trpc/lib/ruleEngine.ts
+++ b/packages/trpc/lib/ruleEngine.ts
@@ -72,13 +72,16 @@ export class RuleEngine {
);
}
- static async forBookmark(ctx: AuthedContext, bookmarkId: string) {
+ static async forBookmark(
+ ctx: AuthedContext,
+ bookmarkId: string,
+ ): Promise<RuleEngine | null> {
const [bookmark, rules] = await Promise.all([
fetchBookmark(ctx.db, bookmarkId),
RuleEngineRuleModel.getAll(ctx),
]);
if (!bookmark) {
- throw new Error(`Bookmark ${bookmarkId} not found`);
+ return null;
}
return new RuleEngine(
ctx,