Skip to content

perf: optimize updated resource queue#585

Merged
adityachoudhari26 merged 1 commit intomainfrom
optimize-updated-resource-queue
Jun 11, 2025
Merged

perf: optimize updated resource queue#585
adityachoudhari26 merged 1 commit intomainfrom
optimize-updated-resource-queue

Conversation

@adityachoudhari26
Copy link
Copy Markdown
Member

@adityachoudhari26 adityachoudhari26 commented Jun 11, 2025

Summary by CodeRabbit

  • New Features

    • Improved job dispatching for updated resources to prevent duplicate jobs and streamline processing.
  • Refactor

    • Replaced direct queue additions with a dedicated dispatch function for handling updated resource jobs across multiple components.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jun 11, 2025

Walkthrough

The changes introduce a new function, dispatchUpdatedResourceJob, for dispatching updated resource jobs. Existing code that previously enqueued jobs directly via the queue is refactored to use this function. Supporting exports are added to the events package, and bulk job enqueuing is replaced with iterative dispatching for updated resources.

Changes

Files/Paths Change Summary
apps/pty-proxy/src/controller/agent-socket.ts
apps/webservice/src/app/api/github/webhook/handlers/pull_request.ts
apps/webservice/src/app/api/v1/resources/[resourceId]/route.ts
Replaced direct queue job addition with calls to dispatchUpdatedResourceJob for updated resources.
packages/events/src/updated-resource/dispatch-job.ts Added new function dispatchUpdatedResourceJob to prevent duplicate jobs and handle job dispatching.
packages/events/src/updated-resource/index.ts
packages/events/src/index.ts
Added and re-exported the new dispatch function from the updated-resource module.
packages/events/src/resource-provider-scan/handle-provider-scan.ts Replaced bulk enqueueing of update jobs with iterative calls to dispatchUpdatedResourceJob for each resource.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Any Caller (API, Agent, Handler)
    participant Events as events package
    participant Queue as Job Queue

    Caller->>Events: dispatchUpdatedResourceJob(resource)
    Events->>Queue: get waiting jobs
    alt Job with resource.id exists
        Events-->>Caller: return (do not enqueue)
    else No existing job
        Events->>Queue: add job (resource.id, resource)
        Queue-->>Events: confirmation
        Events-->>Caller: return
    end
Loading

Poem

A hop, a skip, a queue anew,
Now jobs for updates pass right through!
No more doubles in the line,
Each resource waits its turn just fine.
With dispatch magic, work flows true—
The rabbits cheer this code review!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
apps/pty-proxy/src/controller/agent-socket.ts (1)

141-142: Optional: make dispatch fire-and-forget

If the rest of the method doesn’t rely on the queue write succeeding synchronously, drop the await to avoid stalling the agent handshake on network latency:

-    await dispatchUpdatedResourceJob(res);
+    void dispatchUpdatedResourceJob(res);

Purely a latency optimisation—ignore if strong delivery guarantees are required here.

apps/webservice/src/app/api/github/webhook/handlers/pull_request.ts (1)

303-304: You might not need the await here
Queuing a BullMQ job is fire-and-forget for most use-cases, and awaiting it makes the webhook response path a bit slower.
If webhook latency matters, consider returning the promise un-awaited (or void dispatchUpdatedResourceJob(...)) unless callers rely on the resolution.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b33e389 and 758c664.

📒 Files selected for processing (7)
  • apps/pty-proxy/src/controller/agent-socket.ts (2 hunks)
  • apps/webservice/src/app/api/github/webhook/handlers/pull_request.ts (2 hunks)
  • apps/webservice/src/app/api/v1/resources/[resourceId]/route.ts (2 hunks)
  • packages/events/src/index.ts (1 hunks)
  • packages/events/src/resource-provider-scan/handle-provider-scan.ts (2 hunks)
  • packages/events/src/updated-resource/dispatch-job.ts (1 hunks)
  • packages/events/src/updated-resource/index.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{ts,tsx}`: **Note on Error Handling:** Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error...

**/*.{ts,tsx}: Note on Error Handling:
Avoid strict enforcement of try/catch blocks. Code may use early returns, Promise chains (.then().catch()), or other patterns for error handling. These are acceptable as long as they maintain clarity and predictability.

  • packages/events/src/index.ts
  • apps/webservice/src/app/api/v1/resources/[resourceId]/route.ts
  • packages/events/src/updated-resource/index.ts
  • packages/events/src/resource-provider-scan/handle-provider-scan.ts
  • apps/pty-proxy/src/controller/agent-socket.ts
  • apps/webservice/src/app/api/github/webhook/handlers/pull_request.ts
  • packages/events/src/updated-resource/dispatch-job.ts
🧬 Code Graph Analysis (4)
packages/events/src/resource-provider-scan/handle-provider-scan.ts (2)
packages/db/src/schema/resource.ts (1)
  • resource (58-86)
packages/events/src/updated-resource/dispatch-job.ts (1)
  • dispatchUpdatedResourceJob (5-11)
apps/pty-proxy/src/controller/agent-socket.ts (1)
packages/events/src/updated-resource/dispatch-job.ts (1)
  • dispatchUpdatedResourceJob (5-11)
apps/webservice/src/app/api/github/webhook/handlers/pull_request.ts (1)
packages/events/src/updated-resource/dispatch-job.ts (1)
  • dispatchUpdatedResourceJob (5-11)
packages/events/src/updated-resource/dispatch-job.ts (2)
packages/db/src/schema/resource.ts (1)
  • resource (58-86)
packages/events/src/index.ts (1)
  • getQueue (28-34)
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: Typecheck
  • GitHub Check: Lint
  • GitHub Check: build (linux/amd64)
🔇 Additional comments (5)
packages/events/src/index.ts (1)

39-40: Barrel export looks correct

The additional re-export makes dispatchUpdatedResourceJob available at the package root without introducing naming collisions or type-level ambiguities.

packages/events/src/updated-resource/index.ts (1)

1-1: Minimal, purposeful barrel file

Straightforward re-export; no issues spotted.

packages/events/src/resource-provider-scan/handle-provider-scan.ts (1)

9-9: Possible circular-import hotspot

handle-provider-scan.ts imports from "../index.js", while packages/events/src/index.ts in turn re-exports from "./resource-provider-scan/handle-provider-scan.js".
This creates a circular dependency chain:

handle-provider-scan → index → handle-provider-scan

In Node ESM that often works, but the imported bindings can be undefined during module initialisation, leading to intermittent “Cannot read property … of undefined” errors in cold starts or tests.

Consider importing the needed symbols directly to break the cycle, e.g.

import { getQueue } from "../redis.js";
import { dispatchUpdatedResourceJob } from "../updated-resource/dispatch-job.js";

or move shared utilities to a dedicated, cycle-free helper module.

apps/webservice/src/app/api/v1/resources/[resourceId]/route.ts (1)

8-12: Import group is fine

The extended import list is coherent and keeps tree-shaking behaviour unchanged.

apps/webservice/src/app/api/github/webhook/handlers/pull_request.ts (1)

7-7: Nice abstraction – 👍 for switching to the shared dispatch helper
Replacing the direct queue access with dispatchUpdatedResourceJob centralises duplicate-protection logic and keeps this handler thin.

Comment on lines +105 to +106
for (const resource of updatedResources)
await dispatchUpdatedResourceJob(resource);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Sequential await harms scan-time throughput

Each call hits Redis twice (getWaiting + add) and you await inside the for…of, turning N resources into N round-trips.
A safe, non-duplicating parallel dispatch is trivial because each resource ID is unique within updatedResources.

-    for (const resource of updatedResources)
-      await dispatchUpdatedResourceJob(resource);
+    await Promise.all(updatedResources.map(dispatchUpdatedResourceJob));

This preserves de-dup logic, avoids bulk-add removal regressions, and restores near-original performance.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const resource of updatedResources)
await dispatchUpdatedResourceJob(resource);
await Promise.all(updatedResources.map(dispatchUpdatedResourceJob));
🤖 Prompt for AI Agents
In packages/events/src/resource-provider-scan/handle-provider-scan.ts at lines
105-106, the sequential await inside the for...of loop causes multiple Redis
round-trips and slows down throughput. Refactor the code to dispatch all updated
resource jobs in parallel by collecting the promises from
dispatchUpdatedResourceJob calls into an array and then await them all together
using Promise.all. This maintains the de-duplication logic while improving
performance by reducing the number of Redis calls.

Comment on lines +160 to 161
await dispatchUpdatedResourceJob(resource);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incorrect payload: sends pre-update snapshot

dispatchUpdatedResourceJob is invoked with resource (the old record), not res (the freshly upserted one).
Down-stream consumers will miss the latest changes.

-      await dispatchUpdatedResourceJob(resource);
+      await dispatchUpdatedResourceJob(res);

This maintains previous semantics (job fired after update) while ensuring the queue carries the current state.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await dispatchUpdatedResourceJob(resource);
await dispatchUpdatedResourceJob(res);
🤖 Prompt for AI Agents
In apps/webservice/src/app/api/v1/resources/[resourceId]/route.ts around lines
160 to 161, the function dispatchUpdatedResourceJob is called with the old
resource object instead of the updated one. To fix this, replace the argument
from resource to res so that the job receives the latest upserted resource
state, ensuring downstream consumers get the current data after the update.

Comment on lines +5 to +11
export const dispatchUpdatedResourceJob = async (resource: schema.Resource) => {
const q = getQueue(Channel.UpdatedResource);
const waiting = await q.getWaiting();
const isAlreadyQueued = waiting.some((job) => job.data.id === resource.id);
if (isAlreadyQueued) return;
await q.add(resource.id, resource);
};
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Inefficient scan & race-condition – leverage BullMQ’s jobId for dedup
Fetching the entire waiting list on every call is O(n) and still misses duplicates in active, delayed, etc. Two concurrent workers can also race between the getWaiting() and add() calls and enqueue a duplicate.

BullMQ already prevents duplicates when jobId is supplied; we can rely on that and avoid the scan altogether:

-  const q = getQueue(Channel.UpdatedResource);
-  const waiting = await q.getWaiting();
-  const isAlreadyQueued = waiting.some((job) => job.data.id === resource.id);
-  if (isAlreadyQueued) return;
-  await q.add(resource.id, resource);
+  const q = getQueue(Channel.UpdatedResource);
+
+  // `jobId` guarantees idempotency across ALL job states and is O(1).
+  await q.add(resource.id, resource, {
+    jobId: resource.id,            // de-dupe key
+    jobIdDuplicateBehavior: "ignore", // BullMQ ≥3.0 – silently skip if exists
+  });

Benefits
• O(1) lookup instead of O(n) scan
• Covers waiting, delayed, active, etc.
• Eliminates race windows between lookup and add

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const dispatchUpdatedResourceJob = async (resource: schema.Resource) => {
const q = getQueue(Channel.UpdatedResource);
const waiting = await q.getWaiting();
const isAlreadyQueued = waiting.some((job) => job.data.id === resource.id);
if (isAlreadyQueued) return;
await q.add(resource.id, resource);
};
export const dispatchUpdatedResourceJob = async (resource: schema.Resource) => {
const q = getQueue(Channel.UpdatedResource);
// `jobId` guarantees idempotency across ALL job states and is O(1).
await q.add(resource.id, resource, {
jobId: resource.id, // de-dupe key
jobIdDuplicateBehavior: "ignore", // BullMQ ≥3.0 – silently skip if exists
});
};
🤖 Prompt for AI Agents
In packages/events/src/updated-resource/dispatch-job.ts around lines 5 to 11,
the current code fetches all waiting jobs and scans them to avoid duplicates,
which is inefficient and prone to race conditions. To fix this, remove the
getWaiting() call and the duplicate check, and instead pass the resource.id as
the jobId parameter when calling q.add(). This leverages BullMQ's built-in
deduplication by jobId, providing O(1) lookup and preventing duplicates across
all job states without race conditions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BullMQ's deduplication will skip jobs if the job is actively being processed by a worker, which we don't want. We specifically only want to skip if its waiting, if its actively processing we still want to add it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adityachoudhari26 Understood—your requirement is “skip only when the job is still waiting, but enqueue a new one once the previous job is already being processed.”

BullMQ unfortunately ties its jobId de-duplication to all states (waiting, delayed, active, etc.). If a job with the same jobId is active, a second add() call with that jobId is ignored, so the built-in mechanism can’t express your “waiting-only” rule.

A middle-ground that avoids the O(n) full-queue scan but still checks only the waiting list is to ask BullMQ directly for the job by jobId and inspect its state:

export const dispatchUpdatedResourceJob = async (resource: schema.Resource) => {
  const q = getQueue(Channel.UpdatedResource);

  // Give the waiting job a predictable id so we can query it in O(1)
  const jobId = `waiting:${resource.id}`;

  // Look up any existing job with this id.
  const existing = await q.getJob(jobId);

  // If it exists **and** is still waiting, we’re done—otherwise enqueue again.
  if (existing && (await existing.getState()) === "waiting") return;

  await q.add(resource.id, resource, { jobId });
};

Why this helps
queue.getJob(jobId) is a direct Redis hash lookup—O(1), no full list scan
• We only skip when state === “waiting”, satisfying your requirement
• No race window: if two callers run concurrently, only one will win the add() because the other’s getJob() will see the new job in waiting state.

Trade-offs

  1. The jobId needs a prefix (e.g., waiting:) so simultaneous active jobs can still have independent automatically-generated IDs.
  2. If you ever change queue settings (e.g., LIFO, priorities), make sure the logic still reflects the desired behaviour.

If you prefer to stay with the original scan for now, that’s perfectly valid; just wanted to surface this lighter alternative. Let me know which direction you’d like to take!

@adityachoudhari26 adityachoudhari26 merged commit bb0dbfc into main Jun 11, 2025
8 checks passed
@adityachoudhari26 adityachoudhari26 deleted the optimize-updated-resource-queue branch June 11, 2025 16:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant