Skip to content

Conversation

@adityachoudhari26
Copy link
Contributor

@adityachoudhari26 adityachoudhari26 commented Apr 30, 2025

Summary by CodeRabbit

  • New Features

    • Introduced new background workers to efficiently compute and update policy target associations for environments and deployments.
    • Added new database tables to track computed relationships between policy targets, environments, and deployments.
  • Improvements

    • Enhanced policy retrieval logic with a more flexible and reusable query system for determining applicable policies.
    • Streamlined the process for creating and managing approval records, including detailed user-specific approvals.
  • Refactor

    • Updated internal job processing to use new worker channels for better concurrency and reliability.
    • Refactored policy query utilities to a class-based structure for improved maintainability.
  • Bug Fixes

    • Improved error handling and job re-queuing to ensure consistent and reliable policy computation.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 30, 2025

Walkthrough

This set of changes introduces new event worker modules and supporting infrastructure for computing policy target selectors in a concurrent and modular fashion. Three new workers are added to handle environment, deployment, and all-selector computations, along with corresponding event channels and queue logic. The database schema is extended with tables for computed policy target environments and deployments. The policy applicability logic is refactored into a class-based API, enabling more flexible and granular queries. Various parts of the API and worker logic are updated to use the new event channels and policy query interfaces, improving modularity and maintainability.

Changes

File(s) Change Summary
apps/event-worker/src/workers/compute-policy-taret-all-selectors.ts, apps/event-worker/src/workers/compute-policy-target-deployment-selector.ts, apps/event-worker/src/workers/compute-policy-target-environment-selector.ts Added new worker modules for computing policy target selectors, each listening to its own channel and processing jobs accordingly. The "all selectors" worker enqueues jobs for the environment, deployment, and release target selector workers.
apps/event-worker/src/workers/index.ts Registered the new workers and their channels in the exported workers object, integrating them into the event-worker system.
apps/event-worker/src/workers/compute-systems-release-targets.ts, apps/event-worker/src/workers/new-policy.ts, apps/event-worker/src/workers/update-policy.ts Updated job dispatching logic to enqueue jobs to the new ComputePolicyTargetAllSelectors channel instead of the previous release target selector channel.
packages/events/src/types.ts Added new enum members and channel map entries for the three new policy target selector channels.
packages/db/src/schema/policy.ts Introduced new tables: computed_policy_target_environment and computed_policy_target_deployment, establishing many-to-many relationships between policy targets and environments/deployments.
packages/db/src/schema/rules/approval-base.ts Added a new type alias BaseApprovalRecordInsert for inserting base approval records.
packages/rule-engine/src/db/get-applicable-policies.ts Refactored the policy applicability logic into two classes (ApplicablePolicies, EnvironmentAndDeploymentApplicablePolicies) and a new API for querying applicable policies using method chaining.
packages/rule-engine/src/manager/version-manager.ts Updated to use the new class-based policy applicability API for retrieving applicable policies.
packages/api/src/router/deployment-version-checks/approvals.ts Refactored to use the new policy query builder, added helper for creating user approval records, and improved data consistency in approval record creation.
packages/api/src/router/deployment-version-checks/version-selector.ts Replaced multi-step policy retrieval with direct use of the new policy query builder, simplifying logic and imports.
packages/api/src/router/deployment-version-checks/utils.ts Removed the now-obsolete getApplicablePoliciesWithoutResourceScope function and related imports.

Sequence Diagram(s)

sequenceDiagram
    participant Producer as Job Producer (e.g., new policy)
    participant WorkerAll as computePolicyTargetAllSelectorsWorker
    participant WorkerEnv as computePolicyTargetEnvironmentSelectorWorker
    participant WorkerDep as computePolicyTargetDeploymentSelectorWorker
    participant WorkerRT as computePolicyTargetReleaseTargetSelectorWorker

    Producer->>WorkerAll: Enqueue job (id)
    WorkerAll->>WorkerEnv: Enqueue job (id)
    WorkerAll->>WorkerDep: Enqueue job (id)
    WorkerAll->>WorkerRT: Enqueue job (id)
    Note over WorkerAll: Waits for all three jobs to be enqueued
Loading

Possibly related PRs

  • ctrlplanedev/ctrlplane#444: Refactors worker creation and registration logic to use the same createWorker function and infrastructure as in this PR.
  • ctrlplanedev/ctrlplane#440: Sets up the foundational event library and worker infrastructure that the new workers and channels in this PR build upon.
  • ctrlplanedev/ctrlplane#490: Implements and refactors policy target computation logic, including release target selectors, which directly relates to the new worker logic introduced here.

Suggested reviewers

  • jsbroks

Poem

In the warren of code, selectors abound,
New workers hop in, their channels now found.
Policies mapped, environments and more,
Deployments and targets—computed galore!
With tables and types, the groundwork is set,
This rabbit’s quite pleased with the progress we’ve met.
🐇✨

✨ 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.
    • Generate unit testing code for this file.
    • 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 generate unit testing code for this file.
    • @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 generate unit testing code.
    • @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
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: 6

🔭 Outside diff range comments (1)
apps/event-worker/src/workers/compute-policy-taret-all-selectors.ts (1)

1-21: ⚠️ Potential issue

Typo in filename needs correction

The filename contains a typo: compute-policy-taret-all-selectors.ts should be compute-policy-target-all-selectors.ts to maintain consistency with the rest of the codebase.

The filename needs to be corrected throughout the project, including import statements that reference it.

Effective fan-out pattern for selector computation

This worker efficiently uses Promise.all to concurrently dispatch jobs to specific selector workers. The implementation is clean and focuses on its dispatching responsibility, which follows good separation of concerns.

🧹 Nitpick comments (11)
packages/events/src/types.ts (1)

37-40: Enum order / discoverability

The three new channels work fine semantically, but they break the informal alphabetical / semantic grouping that the previous block followed (all Compute…Selector channels were grouped together and sorted). Re-ordering keeps cognitive load low when scanning the file.

   ComputeEnvironmentResourceSelector = "compute-environment-resource-selector",
   ComputeDeploymentResourceSelector = "compute-deployment-resource-selector",
   ComputePolicyTargetReleaseTargetSelector = "compute-policy-target-release-target-selector",
-  ComputePolicyTargetEnvironmentSelector = "compute-policy-target-environment-selector",
-  ComputePolicyTargetDeploymentSelector = "compute-policy-target-deployment-selector",
-  ComputePolicyTargetAllSelectors = "compute-policy-target-all-selectors",
+  ComputePolicyTargetAllSelectors         = "compute-policy-target-all-selectors",
+  ComputePolicyTargetDeploymentSelector   = "compute-policy-target-deployment-selector",
+  ComputePolicyTargetEnvironmentSelector  = "compute-policy-target-environment-selector",

(Only a move – no functional change.)

packages/db/src/schema/policy.ts (2)

97-110: Missing secondary indexes may hurt look-ups

computed_policy_target_environment is queried both by

  • policyTargetId (already covered by the PK) and
  • environmentId (reverse look-ups: “which policy targets apply to this env?”).

Adding a simple index on environment_id will keep those queries from doing seq-scans when the table grows.

 export const computedPolicyTargetEnvironment = pgTable(
   "computed_policy_target_environment",
   {
     …
   },
   (t) => ({
     pk: primaryKey({ columns: [t.policyTargetId, t.environmentId] }),
+    byEnvironment: index("cpt_env_by_environment").on(t.environmentId),
   }),
 );

Same applies to the deployment table below.


112-125: Consider exporting the generated types

Down-stream code (rule-engine, API) will probably want to refer to
InferSelectModel<typeof computedPolicyTargetDeployment> / Environment>.
Exporting those right here reduces repetition and keeps types single-sourced.

export type ComputedPolicyTargetEnvironment = InferSelectModel<
  typeof computedPolicyTargetEnvironment
>;
export type ComputedPolicyTargetDeployment = InferSelectModel<
  typeof computedPolicyTargetDeployment
>;
packages/api/src/router/deployment-version-checks/version-selector.ts (1)

6-11: Unused import after refactor

and, eq, selector are still imported but only and/eq are used after
the refactor; consider dropping selector to silence eslint/tsc warnings.

apps/event-worker/src/workers/compute-policy-target-environment-selector.ts (1)

8-32: Query only needed columns & use implicit select type

The helper currently selects all environment + system columns and then
projects only environment.id. This moves unnecessary bytes over the wire.

-  const environments = await tx
-    .select()
+  const environments = await tx
+    .select({ id: schema.environment.id })

Performance boost is marginal now but scales better.

packages/api/src/router/deployment-version-checks/approvals.ts (1)

148-153: Add retry/back-off when re-queuing evaluation jobs

addBulk fires jobs immediately. If the downstream worker is still processing locks for the same release target, we may create a tight retry loop.
Consider supplying opts: { delay: 5_000, attempts: 5, backoff: { type: "exponential", delay: 2_000 } } (BullMQ signature) or similar.

apps/event-worker/src/workers/compute-policy-target-deployment-selector.ts (2)

8-32: Avoid fetching full deployment rows – select only the IDs

Only deployment.id is needed to build matchingDeployments. Pulling entire rows increases network and memory use.

-  const deployments = await tx
-    .select()
+  const deployments = await tx
+    .select({ id: schema.deployment.id })
     .from(schema.deployment)
     ...
-  return deployments.map((d) => ({
+  return deployments.map((d) => ({
     policyTargetId: policyTarget.id,
-    deploymentId: d.deployment.id,
+    deploymentId: d.id,
   }));

80-88: Re-queue loop can be infinite – add delay or maxAttempts

If the row stays locked for an extended period (e.g., long-running migration) the worker will enqueue itself continuously. Provide exponential back-off or pass opts: { attempts: 5, backoff: { type: "exponential", delay: 1000 } }.

await getQueue(Channel.ComputePolicyTargetDeploymentSelector).add(
   job.name,
   job.data,
+  { backoff: { type: "exponential", delay: 1_000 }, attempts: 5 },
 );
packages/rule-engine/src/db/get-applicable-policies.ts (3)

120-133: Stale JSDoc – parameters no longer match implementation

The docstring references workspaceId and repo, but releaseTarget() now only accepts releaseTargetId. This can confuse consumers and automated documentation.

Please update the comment block accordingly.


105-109: Deduplication in JS could be pushed to SQL

_.uniqBy runs in memory after fetching potentially large result sets.
Using select distinct on (policy.id) (Postgres) or grouping in SQL would cut down transferred rows and CPU.

Not blocking, but worth considering for large workspaces.


111-114: Fluent API leak – confusing method name

Inside EnvironmentAndDeploymentApplicablePolicies, environmentAndDeployment() simply returns an object wrapping withoutResourceScope().
Call-sites already are in the environment+deployment context, so this extra hop reads oddly:

getApplicablePolicies().environmentAndDeployment(target).withoutResourceScope();

Consider exposing withoutResourceScope() directly from the class and letting ApplicablePolicies.environmentAndDeployment() return the instance itself:

// in ApplicablePolicies
-  environmentAndDeployment(target) {
-    return new EnvironmentAndDeploymentApplicablePolicies(this.tx, target);
-  }
+  environmentAndDeployment(target) {
+    return new EnvironmentAndDeploymentApplicablePolicies(this.tx, target);
+  }

// in EnvironmentAndDeploymentApplicablePolicies
-  environmentAndDeployment() {
-    return { withoutResourceScope: () => this.withoutResourceScope() };
-  }
+  // consumers now call .withoutResourceScope() directly
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 06db728 and 60be06b.

📒 Files selected for processing (15)
  • apps/event-worker/src/workers/compute-policy-taret-all-selectors.ts (1 hunks)
  • apps/event-worker/src/workers/compute-policy-target-deployment-selector.ts (1 hunks)
  • apps/event-worker/src/workers/compute-policy-target-environment-selector.ts (1 hunks)
  • apps/event-worker/src/workers/compute-systems-release-targets.ts (1 hunks)
  • apps/event-worker/src/workers/index.ts (2 hunks)
  • apps/event-worker/src/workers/new-policy.ts (1 hunks)
  • apps/event-worker/src/workers/update-policy.ts (1 hunks)
  • packages/api/src/router/deployment-version-checks/approvals.ts (4 hunks)
  • packages/api/src/router/deployment-version-checks/utils.ts (1 hunks)
  • packages/api/src/router/deployment-version-checks/version-selector.ts (2 hunks)
  • packages/db/src/schema/policy.ts (2 hunks)
  • packages/db/src/schema/rules/approval-base.ts (1 hunks)
  • packages/events/src/types.ts (2 hunks)
  • packages/rule-engine/src/db/get-applicable-policies.ts (1 hunks)
  • packages/rule-engine/src/manager/version-manager.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.

  • apps/event-worker/src/workers/update-policy.ts
  • apps/event-worker/src/workers/compute-systems-release-targets.ts
  • apps/event-worker/src/workers/new-policy.ts
  • packages/api/src/router/deployment-version-checks/utils.ts
  • packages/events/src/types.ts
  • apps/event-worker/src/workers/compute-policy-taret-all-selectors.ts
  • apps/event-worker/src/workers/index.ts
  • packages/db/src/schema/rules/approval-base.ts
  • packages/rule-engine/src/manager/version-manager.ts
  • packages/db/src/schema/policy.ts
  • apps/event-worker/src/workers/compute-policy-target-environment-selector.ts
  • packages/api/src/router/deployment-version-checks/approvals.ts
  • packages/api/src/router/deployment-version-checks/version-selector.ts
  • apps/event-worker/src/workers/compute-policy-target-deployment-selector.ts
  • packages/rule-engine/src/db/get-applicable-policies.ts
🧬 Code Graph Analysis (5)
apps/event-worker/src/workers/update-policy.ts (1)
packages/events/src/index.ts (1)
  • getQueue (28-34)
apps/event-worker/src/workers/compute-systems-release-targets.ts (1)
packages/events/src/index.ts (1)
  • getQueue (28-34)
apps/event-worker/src/workers/new-policy.ts (1)
packages/events/src/index.ts (1)
  • getQueue (28-34)
packages/rule-engine/src/manager/version-manager.ts (1)
packages/rule-engine/src/db/get-applicable-policies.ts (1)
  • getApplicablePolicies (23-52)
packages/rule-engine/src/db/get-applicable-policies.ts (1)
packages/db/src/common.ts (1)
  • Tx (22-22)
⏰ 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: build (linux/amd64)
  • GitHub Check: Lint
🔇 Additional comments (9)
packages/db/src/schema/rules/approval-base.ts (1)

58-64: Type definition aligns well with existing schema structure

The newly added BaseApprovalRecordInsert type properly reflects the core fields from baseApprovalRecordFields while appropriately excluding auto-generated fields like id, createdAt, and updatedAt. The optional fields are correctly marked with ?. This type will provide better type safety when inserting approval records.

apps/event-worker/src/workers/new-policy.ts (1)

12-12:

❓ Verification inconclusive

Channel migration to centralized selector computation

The change from the previous channel to ComputePolicyTargetAllSelectors aligns with the broader refactoring of policy target selector computations to a more centralized approach.


🏁 Script executed:

#!/bin/bash
# Verify that the corresponding worker exists to handle the ComputePolicyTargetAllSelectors channel
rg -A 5 "ComputePolicyTargetAllSelectors" --type ts

Length of output: 51


#!/bin/bash
# Search across all file types for any references to ComputePolicyTargetAllSelectors
rg -n "ComputePolicyTargetAllSelectors"

Confirm presence of ComputePolicyTargetAllSelectors worker
I ran a search for any handler or subscriber for the ComputePolicyTargetAllSelectors channel and found no matching references. Please manually verify that:

  • The Channel enum defines ComputePolicyTargetAllSelectors.
  • A corresponding worker (e.g. in apps/event-worker/src/workers/…) is implemented and registered to process jobs from this queue.

Without a handler, enqueued tasks will never be processed.

apps/event-worker/src/workers/compute-systems-release-targets.ts (1)

178-178: Queue channel update for centralized selector processing

The migration to Channel.ComputePolicyTargetAllSelectors aligns with the centralized approach for computing policy targets that's being implemented across the codebase. This change ensures consistency with the updates in other worker files.

apps/event-worker/src/workers/update-policy.ts (1)

14-14: Consistent channel update for policy computations

Updated to use ComputePolicyTargetAllSelectors for job processing, maintaining consistency with the channel changes in other worker files. This ensures all policy target computation jobs are routed through the new centralized worker.

packages/api/src/router/deployment-version-checks/utils.ts (1)

4-4: Imports updated to reflect removed function

This import change is part of removing the getApplicablePoliciesWithoutResourceScope function, which aligns with moving to the new class-based policy applicability API. The updated import now only includes what's needed for the remaining functions.

packages/rule-engine/src/manager/version-manager.ts (1)

163-165: Updated to use new class-based policy applicability API

This change follows the broader refactoring pattern, moving from a direct function call with parameters to a more flexible method chaining style. The logic remains functionally equivalent while providing better reusability and more granular querying options.

apps/event-worker/src/workers/index.ts (1)

59-64: New policy target selector workers properly registered

The new workers for environment, deployment, and all selectors computations are correctly registered with their corresponding channels in the workers object.

packages/events/src/types.ts (1)

77-80: Keep ChannelMap in sync with the enum

ChannelMap is correctly updated, 👍🏼.
Double-check that each worker enqueues/dequeues with { id: string } – if any worker needs extra fields later you’ll have to widen the type here.

packages/api/src/router/deployment-version-checks/version-selector.ts (1)

31-34:

❓ Verification inconclusive

Verify builder chain actually executes the query

getApplicablePolicies().environmentAndDeployment(...).withoutResourceScope()
returns an object in the latest rule-engine refactor; you still need to
call .run() (or .execute()) to resolve the promise with the array of
policies. Otherwise mergePolicies will receive a builder and type-checking
will fail.

Please make sure the final call returns Promise<Policy[]>.


🏁 Script executed:

#!/bin/bash
# Grep the builder to see what the terminal method is called
rg -A3 -n "class .*ApplicablePolicies" packages/rule-engine | head

Length of output: 68


I’ll dig into the rules-engine code to find how getApplicablePolicies and .withoutResourceScope() are implemented. Running:


🏁 Script executed:

#!/bin/bash
# Locate the definition of getApplicablePolicies
echo "=== getApplicablePolicies ==="
rg -n "getApplicablePolicies" -n packages/rule-engine | sed -n '1,20p'

# Locate the definition of withoutResourceScope
echo -e "\n=== withoutResourceScope ==="
rg -n "withoutResourceScope" -n packages/rule-engine | sed -n '1,20p'

Length of output: 703


Digging into implementations and usages to see how the builder chain resolves:


🏁 Script executed:

#!/bin/bash
# Show the implementation of getApplicablePolicies
echo "=== getApplicablePolicies implementation ==="
sed -n '1,200p' packages/rule-engine/src/db/get-applicable-policies.ts

# Show how it's used in version-manager.ts
echo -e "\n=== Usage in version-manager.ts ==="
sed -n '140,200p' packages/rule-engine/src/manager/version-manager.ts

# Check where environmentAndDeployment and withoutResourceScope are defined
echo -e "\n=== Search for builder methods ==="
rg -n "environmentAndDeployment\|withoutResourceScope" -n packages/rule-engine

Length of output: 3990


I need to inspect the API-side builder to see what getApplicablePolicies() returns and where environmentAndDeployment/withoutResourceScope are defined. Gathering:


🏁 Script executed:

#!/bin/bash
# 1. Locate version-selector.ts
echo "=== File location ==="
fd version-selector.ts

# 2. Show imports and usage in version-selector.ts
echo -e "\n=== version-selector.ts (first 200 lines) ==="
sed -n '1,200p' packages/api/src/router/deployment-version-checks/version-selector.ts

# 3. Find builder method definitions in API
echo -e "\n=== environmentAndDeployment definitions ==="
rg -n "environmentAndDeployment" -n packages/api

echo -e "\n=== withoutResourceScope definitions ==="
rg -n "withoutResourceScope" -n packages/api

Length of output: 742


Verify that the query builder is executed before merging
It looks like

const policies = await getApplicablePolicies()
  .environmentAndDeployment({ environmentId, deploymentId })
  .withoutResourceScope();
const mergedPolicy = mergePolicies(policies);

is still dealing with a builder object, not the resolved array. You need to append the terminal call (e.g. .run() or .execute()) so that policies is a Policy[] and mergePolicies type-checks correctly.

• Check packages/api/src/router/deployment-version-checks/version-selector.ts (around lines 31–34)
• Ensure the final expression is a Promise<Policy[]>, for example:

const policies = await getApplicablePolicies()
  .environmentAndDeployment({ environmentId, deploymentId })
  .withoutResourceScope()
  .run();

Comment on lines +8 to +10
import { computePolicyTargetAllSelectorsWorker } from "./compute-policy-taret-all-selectors.js";
import { computePolicyTargetDeploymentSelectorWorker } from "./compute-policy-target-deployment-selector.js";
import { computePolicyTargetEnvironmentSelectorWorker } from "./compute-policy-target-environment-selector.js";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Typo in imported module filename

There's a typo in the module filename: compute-policy-taret-all-selectors.js should be compute-policy-target-all-selectors.js.

-import { computePolicyTargetAllSelectorsWorker } from "./compute-policy-taret-all-selectors.js";
+import { computePolicyTargetAllSelectorsWorker } from "./compute-policy-target-all-selectors.js";
📝 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
import { computePolicyTargetAllSelectorsWorker } from "./compute-policy-taret-all-selectors.js";
import { computePolicyTargetDeploymentSelectorWorker } from "./compute-policy-target-deployment-selector.js";
import { computePolicyTargetEnvironmentSelectorWorker } from "./compute-policy-target-environment-selector.js";
import { computePolicyTargetAllSelectorsWorker } from "./compute-policy-target-all-selectors.js";
import { computePolicyTargetDeploymentSelectorWorker } from "./compute-policy-target-deployment-selector.js";
import { computePolicyTargetEnvironmentSelectorWorker } from "./compute-policy-target-environment-selector.js";

Comment on lines +48 to +79
try {
await db.transaction(async (tx) => {
await tx.execute(
sql`
SELECT * from ${schema.computedPolicyTargetEnvironment}
WHERE ${eq(schema.computedPolicyTargetEnvironment.policyTargetId, policyTarget.id)}
FOR UPDATE NOWAIT
`,
);

await tx
.delete(schema.computedPolicyTargetEnvironment)
.where(
eq(
schema.computedPolicyTargetEnvironment.policyTargetId,
policyTarget.id,
),
);

const matchingEnvironments = await findMatchingEnvironments(
tx,
policyTarget,
workspaceId,
);

if (matchingEnvironments.length === 0) return;
await tx
.insert(schema.computedPolicyTargetEnvironment)
.values(matchingEnvironments)
.onConflictDoNothing();
});
} catch (e: any) {
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Row-locking strategy can still race

FOR UPDATE NOWAIT on computed_policy_target_environment only locks existing
rows. When the first worker for a brand-new policyTarget arrives, the
table is empty → no row is locked → two workers can both proceed, resulting
in duplicate inserts (ignored thanks to ON CONFLICT DO NOTHING) but still
wasting work and opening a small window for inconsistent reads.

Locking the policy_target row instead guarantees single-writer semantics:

-        SELECT * FROM ${schema.computedPolicyTargetEnvironment}
-        WHERE ${eq(schema.computedPolicyTargetEnvironment.policyTargetId, policyTarget.id)}
+        SELECT id FROM ${schema.policyTarget}
+        WHERE ${eq(schema.policyTarget.id, policyTarget.id)}
         FOR UPDATE NOWAIT

Optionally add a small retry back-off when re-queuing to avoid hot-looping.

📝 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
try {
await db.transaction(async (tx) => {
await tx.execute(
sql`
SELECT * from ${schema.computedPolicyTargetEnvironment}
WHERE ${eq(schema.computedPolicyTargetEnvironment.policyTargetId, policyTarget.id)}
FOR UPDATE NOWAIT
`,
);
await tx
.delete(schema.computedPolicyTargetEnvironment)
.where(
eq(
schema.computedPolicyTargetEnvironment.policyTargetId,
policyTarget.id,
),
);
const matchingEnvironments = await findMatchingEnvironments(
tx,
policyTarget,
workspaceId,
);
if (matchingEnvironments.length === 0) return;
await tx
.insert(schema.computedPolicyTargetEnvironment)
.values(matchingEnvironments)
.onConflictDoNothing();
});
} catch (e: any) {
try {
await db.transaction(async (tx) => {
await tx.execute(
sql`
SELECT id FROM ${schema.policyTarget}
WHERE ${eq(schema.policyTarget.id, policyTarget.id)}
FOR UPDATE NOWAIT
`,
);
await tx
.delete(schema.computedPolicyTargetEnvironment)
.where(
eq(
schema.computedPolicyTargetEnvironment.policyTargetId,
policyTarget.id,
),
);
const matchingEnvironments = await findMatchingEnvironments(
tx,
policyTarget,
workspaceId,
);
if (matchingEnvironments.length === 0) return;
await tx
.insert(schema.computedPolicyTargetEnvironment)
.values(matchingEnvironments)
.onConflictDoNothing();
});
} catch (e: any) {

Comment on lines +70 to 73
const policies = await getApplicablePolicies()
.environmentAndDeployment({ environmentId, deploymentId })
.withoutResourceScope();
const mergedPolicy = mergePolicies(policies);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Duplicate policy look-up – consider a single query inside a transaction

getApplicablePolicies() is executed twice, once for the status query (lines 70-73) and again during mutation (lines 123-126). This can be expensive and, more importantly, risks inconsistent reads if policies change between the two calls.

Wrap both the approval record insertion and policy fetch inside a single ctx.db.transaction, returning the merged result at the end, e.g.

-await ctx.db
-  .insert(SCHEMA.policyRuleAnyApprovalRecord)
-  .values(baseApprovalRecord);
-
-const policies = await getApplicablePolicies()
-  .environmentAndDeployment({ environmentId, deploymentId })
-  .withoutResourceScope();
-
-await createUserApprovalRecords(ctx.db, policies, baseApprovalRecord);
+await ctx.db.transaction(async (tx) => {
+  await tx.insert(SCHEMA.policyRuleAnyApprovalRecord).values(baseApprovalRecord);
+
+  const policies = await getApplicablePolicies(tx)
+    .environmentAndDeployment({ environmentId, deploymentId })
+    .withoutResourceScope();
+
+  await createUserApprovalRecords(tx, policies, baseApprovalRecord);
+});

This ensures atomicity and halves the number of DB round-trips.

Also applies to: 123-126

Comment on lines +21 to +40
type PolicyWithUserApprovals = SCHEMA.Policy & {
versionUserApprovals: SCHEMA.PolicyRuleUserApproval[];
};
const createUserApprovalRecords = async (
tx: Tx,
policies: PolicyWithUserApprovals[],
baseApprovalRecord: SCHEMA.BaseApprovalRecordInsert,
) => {
const userApprovalRules = policies
.flatMap((p) => p.versionUserApprovals)
.filter((a) => a.userId === baseApprovalRecord.userId);

const records = userApprovalRules.map((rule) => ({
...baseApprovalRecord,
ruleId: rule.id,
}));

await tx.insert(SCHEMA.policyRuleUserApprovalRecord).values(records);
};

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard against inserting zero-length arrays into policyRuleUserApprovalRecord

When the current user has no matching PolicyRuleUserApproval entries, records becomes an empty array.
Several SQL drivers (including Drizzle’s Postgres adapter) throw when .values([]) is invoked.
Add a fast-return to avoid an unnecessary round-trip / potential error.

   const records = userApprovalRules.map((rule) => ({
     ...baseApprovalRecord,
     ruleId: rule.id,
   }));

-  await tx.insert(SCHEMA.policyRuleUserApprovalRecord).values(records);
+  if (records.length > 0) {
+    await tx.insert(SCHEMA.policyRuleUserApprovalRecord).values(records);
+  }
📝 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
type PolicyWithUserApprovals = SCHEMA.Policy & {
versionUserApprovals: SCHEMA.PolicyRuleUserApproval[];
};
const createUserApprovalRecords = async (
tx: Tx,
policies: PolicyWithUserApprovals[],
baseApprovalRecord: SCHEMA.BaseApprovalRecordInsert,
) => {
const userApprovalRules = policies
.flatMap((p) => p.versionUserApprovals)
.filter((a) => a.userId === baseApprovalRecord.userId);
const records = userApprovalRules.map((rule) => ({
...baseApprovalRecord,
ruleId: rule.id,
}));
await tx.insert(SCHEMA.policyRuleUserApprovalRecord).values(records);
};
type PolicyWithUserApprovals = SCHEMA.Policy & {
versionUserApprovals: SCHEMA.PolicyRuleUserApproval[];
};
const createUserApprovalRecords = async (
tx: Tx,
policies: PolicyWithUserApprovals[],
baseApprovalRecord: SCHEMA.BaseApprovalRecordInsert,
) => {
const userApprovalRules = policies
.flatMap((p) => p.versionUserApprovals)
.filter((a) => a.userId === baseApprovalRecord.userId);
const records = userApprovalRules.map((rule) => ({
...baseApprovalRecord,
ruleId: rule.id,
}));
if (records.length > 0) {
await tx.insert(SCHEMA.policyRuleUserApprovalRecord).values(records);
}
};

Comment on lines +107 to +118
const version = await getVersionWithMetadata(ctx.db, deploymentVersionId);
const { deploymentId } = version;

const baseApprovalRecord = {
deploymentVersionId,
userId: ctx.session.user.id,
status,
reason,
approvedAt:
status === SCHEMA.ApprovalStatus.Approved ? new Date() : undefined,
};

Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Potential race condition – missing unique index / conflict handling on policyRuleAnyApprovalRecord

If two tabs submit an approval simultaneously, duplicate any-approval rows may be inserted.
Consider enforcing a (deploymentVersionId, userId) unique constraint and using onConflictDoUpdate or letting the mutation surface a friendly error to the caller.

Without this, users can accidentally spam approval history.


🏁 Script executed:

#!/bin/bash
set -e

echo "1. Searching for policyRuleAnyApprovalRecord usage in code..."
rg --context 3 "policyRuleAnyApprovalRecord"

echo
echo "2. Locating Prisma schema file..."
SCHEMA_FILE=$(find . -type f -name "schema.prisma" | head -n1)
if [[ -z "$SCHEMA_FILE" ]]; then
  echo "⚠️ schema.prisma not found."
  exit 1
else
  echo "Found schema at: $SCHEMA_FILE"
fi

echo
echo "3. Inspecting PolicyRuleAnyApprovalRecord model definition for unique constraints..."
rg -n "model PolicyRuleAnyApprovalRecord" -A 20 "$SCHEMA_FILE"

Length of output: 6530


🏁 Script executed:

#!/bin/bash
set -e

echo "1. Displaying the policyRuleAnyApprovalRecord Drizzle schema:"
sed -n '1,200p' packages/db/src/schema/rules/approval-any.ts || echo "⚠️ File not found: approval-any.ts"

echo
echo "2. Searching for uniqueIndex across db schemas:"
rg --context 2 "uniqueIndex" -n packages/db || echo "No uniqueIndex usages found."

echo
echo "3. Searching for generic index definitions:"
rg --context 2 "index(" -n packages/db || echo "No index definitions found."

echo
echo "4. Locating any migrations directories:"
find . -type d -name migrations || echo "No migrations directories found."

echo
echo "5. Listing TypeScript migration files:"
find . -type f -path "*/migrations/*.ts" || echo "No migration files found."

Length of output: 23962


Handle insert conflicts on policyRuleAnyApprovalRecord
We’ve confirmed there is already a unique index on (deploymentVersionId, userId) in packages/db/src/schema/rules/approval-any.ts. However, the insert in packages/api/src/router/deployment-version-checks/approvals.ts (lines 107–118) doesn’t handle conflicts. A concurrent approval request will trigger a unique‐constraint violation and surface as a 500 error.

Action items:

  • In the insert call, add .onConflictDoUpdate() (or .onConflictDoNothing()) to merge or dedupe duplicate submissions, or
  • Wrap the insert in a try/catch to intercept unique‐constraint errors and return a clear, friendly error to the client.

This ensures race conditions can’t spam your approval history or crash the endpoint.

Comment on lines +49 to +79
try {
await db.transaction(async (tx) => {
await tx.execute(
sql`
SELECT * from ${schema.computedPolicyTargetDeployment}
WHERE ${eq(schema.computedPolicyTargetDeployment.policyTargetId, policyTarget.id)}
FOR UPDATE NOWAIT
`,
);

await tx
.delete(schema.computedPolicyTargetDeployment)
.where(
eq(
schema.computedPolicyTargetDeployment.policyTargetId,
policyTarget.id,
),
);

const matchingDeployments = await findMatchingDeployments(
tx,
policyTarget,
workspaceId,
);

if (matchingDeployments.length === 0) return;
await tx
.insert(schema.computedPolicyTargetDeployment)
.values(matchingDeployments)
.onConflictDoNothing();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Transaction could still deadlock – lock the policyTarget row first

Multiple workers may lock computedPolicyTargetDeployment rows for the same policy target, then delete/insert.
Locking the parent policyTarget row first gives a single canonical lock order and eliminates cyclical waits.

await db.transaction(async (tx) => {
+  // Acquire lock on the parent – ensures global ordering.
+  await tx
+    .select()
+    .from(schema.policyTarget)
+    .where(eq(schema.policyTarget.id, policyTarget.id))
+    .forUpdate();

   await tx.execute(
     sql`
       SELECT * from ${schema.computedPolicyTargetDeployment}

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