Skip to content

perf(desiredrelease): paginate vs iterator and versionselector to SQL#1105

Merged
mleonidas merged 2 commits intomainfrom
1101-bug-updating-version-selector-is-not-triggering-reconcile-of-targets
May 5, 2026
Merged

perf(desiredrelease): paginate vs iterator and versionselector to SQL#1105
mleonidas merged 2 commits intomainfrom
1101-bug-updating-version-selector-is-not-triggering-reconcile-of-targets

Conversation

@mleonidas
Copy link
Copy Markdown
Collaborator

@mleonidas mleonidas commented May 5, 2026

  • The desired-release reconciler previously buffered up to 500 candidate deployment versions per release target, silently skipping any version beyond that window. Replace the upfront fetch with a keyset-paginated iter.Seq2 that streams versions newest-first and short-circuits as soon as FindDeployableVersion picks one, removing the hard cap.

    Burst patterns (many RTs of the same deployment reconciling concurrently after a fanout event) now collapse the first-batch fetch through singleflight keyed by (deploymentID, hash(pushdown clauses)), so worker overlap costs one round trip instead of N.

    For policies whose versionselector CEL can be statically translated to SQL, push the predicate into the candidate query via cel2sql. Translatable shapes (literal eq, in-list, startsWith, boolean composition over the version variable) AND-join into the WHERE clause so Postgres returns only plausible candidates instead of the full LIMIT 500. Untranslatable selectors (referencing environment/resource/deployment, complex CEL, JSONB metadata access) fall back to the row-by-row evaluator unchanged — pushdown is a candidate-set narrowing optimization, not a replacement for runtime evaluation.

    Span instrumentation:

    • FindDeployableVersion: versions.scanned, version.found, deployment.id
    • queryCandidateVersionsBatch: pushdown.applied, pushdown.clauses_count, cursor.paginated, rows.returned, rows.below_limit

    SQL injection safety on the inlined pushdown fragments is verified by
    TestTryPushDown_StringEscaping; cel2sql emits Postgres-standard doubled
    single-quote escapes for string literals.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added cursor-based pagination for deployment version retrieval
    • Implemented SQL-level filtering optimization for version candidate selection
  • Performance

    • Improved memory efficiency through lazy evaluation instead of buffering all candidate versions
    • Streamlined version searches with optimized database queries
  • Tests

    • Comprehensive test coverage for version selection, pagination behavior, and input validation

* The desired-release reconciler previously buffered up to 500 candidate
  deployment versions per release target, silently skipping any version
  beyond that window. Replace the upfront fetch with a keyset-paginated
  iter.Seq2 that streams versions newest-first and short-circuits as soon
  as FindDeployableVersion picks one, removing the hard cap.

  Burst patterns (many RTs of the same deployment reconciling concurrently
  after a fanout event) now collapse the first-batch fetch through
  singleflight keyed by (deploymentID, hash(pushdown clauses)), so worker
  overlap costs one round trip instead of N.

  For policies whose versionselector CEL can be statically translated to
  SQL, push the predicate into the candidate query via cel2sql. Translatable
  shapes (literal eq, in-list, startsWith, boolean composition over the
  version variable) AND-join into the WHERE clause so Postgres returns only
  plausible candidates instead of the full LIMIT 500. Untranslatable
  selectors (referencing environment/resource/deployment, complex CEL,
  JSONB metadata access) fall back to the row-by-row evaluator unchanged —
  pushdown is a candidate-set narrowing optimization, not a replacement
  for runtime evaluation.

  Span instrumentation:
  - FindDeployableVersion: versions.scanned, version.found, deployment.id
  - queryCandidateVersionsBatch: pushdown.applied, pushdown.clauses_count,
    cursor.paginated, rows.returned, rows.below_limit

  SQL injection safety on the inlined pushdown fragments is verified by
  TestTryPushDown_StringEscaping; cel2sql emits Postgres-standard doubled
  single-quote escapes for string literals.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Warning

Rate limit exceeded

@mleonidas has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 45 minutes and 15 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b17eefb3-e1b7-4eac-9738-fb95b99f0499

📥 Commits

Reviewing files that changed from the base of the PR and between 04b1156 and ea9bd76.

📒 Files selected for processing (1)
  • apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go
📝 Walkthrough

Walkthrough

This PR migrates candidate version fetching from eager, buffered loading to lazy, keyset-paginated iteration with optional SQL-level pushdown filters. Dependencies are bumped, a new paginated SQL query and CEL-to-SQL pushdown module are introduced, the Getter interface switches to an iterator API, and consumers (policyeval, reconcile) adapt to stream versions while applying policy-derived WHERE clauses.

Changes

Dependency Updates

Layer / File(s) Summary
Go Module Versions
apps/workspace-engine/go.mod
Bumped CEL-go, pgx, OpenTelemetry, and indirect dependencies (antlr, mysql, compress, pq, sqlite, golang.org/x/\*, grpc, etc.) to newer releases.

Lazy Version Iteration with SQL Pushdown

Layer / File(s) Summary
SQL Pagination Foundation
apps/workspace-engine/pkg/db/queries/deployment_versions.sql, apps/workspace-engine/pkg/db/deployment_versions.sql.go
New ListDeployableVersionsByDeploymentIDAfter query with cursor-based pagination on (created_at, id) and parameter struct; excludes rejected/building statuses.
CEL-to-SQL Pushdown
apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown.go, pushdown_test.go
New module translating version-selector CEL expressions to SQL WHERE clauses with a constrained schema (version.id, tag, name, status, created_at) and tests covering supported shapes, safety, and unsupported field rejection.
Getter Interface & Iterator API
apps/workspace-engine/svc/controllers/desiredrelease/getters.go
Getter interface replaces GetCandidateVersions(ctx, deploymentID) with IterCandidateVersions(ctx, deploymentID, extraWhere), returning an iterator instead of a buffered slice.
PostgresGetter Implementation
apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go, getters_postgres_test.go
IterCandidateVersions uses keyset pagination with first-batch deduplication via firstBatchSF (keyed by deploymentID + hash of pushdown clauses), dynamic SQL building with inlined extraWhere predicates, and OTel span attributes; replaces eager GetCandidateVersions.
Policy Evaluation Consumption
apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go, policyeval_test.go
FindDeployableVersion now accepts iter.Seq2[*oapi.DeploymentVersion, error] instead of a slice, consuming versions lazily and recording versions.scanned and version.found span attributes.
Reconciler Integration
apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go, reconcile_test.go
findDeployableVersion builds deterministic pushdown clauses via collectPushdownClauses, calls IterCandidateVersions with those clauses, and passes the iterator to policyeval; removed eager version loading from loadInput.
Test Infrastructure
apps/workspace-engine/test/controllers/harness/mocks.go, svc/controllers/desiredrelease/pushdown_test.go, variableresolver/getters_postgres.go
Harness mock switches to IterCandidateVersions; removed stale GetCandidateVersions from variableresolver; added TestCollectPushdownClauses unit tests.

Sequence Diagram

sequenceDiagram
    participant Reconciler
    participant Pushdown as Pushdown Module
    participant Getter
    participant Database
    participant PolicyEval
    participant Iterator

    Reconciler->>Pushdown: Collect clause translations<br/>from enabled policy selectors
    Pushdown-->>Reconciler: []clause strings (deterministic order)
    
    Reconciler->>Getter: IterCandidateVersions<br/>(deploymentID, pushdown clauses)
    Getter->>Getter: Hash pushdown clauses
    Getter->>Database: First batch: SELECT versions<br/>WHERE deployment_id=? AND (clause AND...)<br/>ORDER BY created_at DESC, id DESC<br/>LIMIT 500
    Database-->>Getter: First batch: []*DeploymentVersion
    Getter-->>Reconciler: iter.Seq2[*DeploymentVersion]<br/>(yields first batch)
    
    Reconciler->>PolicyEval: FindDeployableVersion<br/>(iterator, policies, evaluators)
    PolicyEval->>Iterator: for version := range iter
    Iterator->>Reconciler: (next version, nil)
    PolicyEval->>PolicyEval: Eval version against policies
    alt Version is deployable
        PolicyEval-->>Reconciler: Found: DeploymentVersion
    else More versions needed
        Iterator->>Getter: Next batch needed
        Getter->>Database: SELECT versions WHERE<br/>(created_at, id) < (cursor)<br/>LIMIT 500
        Database-->>Iterator: []*DeploymentVersion
        Iterator-->>PolicyEval: (next version, nil)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • ctrlplanedev/ctrlplane#851: Adds cached implementations of releasetargets getters that interface with the updated desiredrelease getter wiring.
  • ctrlplanedev/ctrlplane#794: Rewrites policy selectors into a unified CEL expression and provides migration logic; directly related to this PR's version-selector pushdown and policy evaluation changes.

Poem

🐰 Swift as a hare, we page through the versions,
No eager buffering slows our conversions!
With pushdown where clauses, SQL does the sift,
And keyset cursors give lazy a lift.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: pagination via iterator and versionselector-to-SQL pushdown optimization.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 1101-bug-updating-version-selector-is-not-triggering-reconcile-of-targets

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

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 2

🧹 Nitpick comments (8)
apps/workspace-engine/test/controllers/harness/mocks.go (1)

170-182: 💤 Low value

Mock ignores extraWhere — fine today, but worth a comment so future test authors know.

A unit test that wants to verify "policy X with selector version.tag == 'v1.0.0' filters out other versions" can't do so against this mock — every version in g.Versions is yielded regardless of pushdown clauses. That's perfectly correct for the existing tests (which test reconcile logic, not the pushdown itself; pushdown has its own tests in pushdown_test.go and the integration-style coverage hits the real Postgres path). But it's a non-obvious limitation.

A one-line comment above the function would prevent surprise:

// IterCandidateVersions yields every version in g.Versions in order. It
// intentionally ignores extraWhere — pushdown filtering is exercised
// against the real Postgres getter; this mock is for testing reconcile
// logic given a candidate stream.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/workspace-engine/test/controllers/harness/mocks.go` around lines 170 -
182, Add a one-line comment above the DesiredReleaseGetter.IterCandidateVersions
function explaining that this mock intentionally ignores the extraWhere
parameter and yields every version in g.Versions in order (i.e., it does not
apply pushdown filtering), and note that pushdown filtering is tested against
the real Postgres getter (pushdown_test.go) so this mock is only for testing
reconcile logic given a candidate stream.
apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go (1)

233-248: ⚖️ Poor tradeoff

Trust boundary on extraWhere is documented, but a defensive sentinel would harden it further.

The doc at lines 127–130 plus the interface‑level note in getters.go make the contract clear: only versionselector.TryPushDown output should ever flow in here. That's the right call given cel2sql's escaping is what makes pushdown safe. One small belt‑and‑suspenders option: have TryPushDown return a typed wrapper (e.g. type SafeWhereClause string) instead of a bare string, and accept []SafeWhereClause here. That makes "I assembled this fragment by hand" a compile error rather than a code-review catch. Optional.

Also worth a sanity check at call time: panic-or-skip if a fragment contains a literal ; outside of a quoted run, since cel2sql's translatable shapes never need one. Cheap defense against a future regression in the translator.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go`
around lines 233 - 248, The extraWhere loop currently accepts raw strings which
relies on external discipline; change the API so TryPushDown (e.g.
versionselector.TryPushDown) returns a typed wrapper like type SafeWhereClause
string and update this function to accept []SafeWhereClause instead of []string,
then iterate over those wrappers when building the SQL (referencing extraWhere
and the candidateVersionColumns builder) to make accidental raw fragments a
compile-time error; additionally add a cheap runtime sanity-check inside the
loop to detect any fragment containing an unquoted semicolon (panic or skip the
fragment) to guard against future translator regressions.
apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown_test.go (2)

67-81: 💤 Low value

Empty selector subtest name is unprintable — give it a label.

t.Run(sel, ...) with sel == "" produces a subtest path like TestTryPushDown_FailsClosed/ which is awkward in -v output and can collide if more empty-named cases ever get added. Switch the case list to {name, sel} pairs.

♻️ Proposed fix
-func TestTryPushDown_FailsClosed(t *testing.T) {
-	cases := []string{
-		`environment.name == "prod"`,
-		`resource.kind == "Server"`,
-		`deployment.name == "api"`,
-		`version.tag == "x" && environment.name == "prod"`,
-		``, // empty
-	}
-	for _, sel := range cases {
-		t.Run(sel, func(t *testing.T) {
-			_, ok := TryPushDown(sel)
-			assert.False(t, ok, "selector %q must NOT push down", sel)
-		})
-	}
-}
+func TestTryPushDown_FailsClosed(t *testing.T) {
+	cases := []struct {
+		name string
+		sel  string
+	}{
+		{"environment ref", `environment.name == "prod"`},
+		{"resource ref", `resource.kind == "Server"`},
+		{"deployment ref", `deployment.name == "api"`},
+		{"mixed version+environment", `version.tag == "x" && environment.name == "prod"`},
+		{"empty", ``},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			_, ok := TryPushDown(tc.sel)
+			assert.False(t, ok, "selector %q must NOT push down", tc.sel)
+		})
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown_test.go`
around lines 67 - 81, The subtest for an empty selector uses t.Run(sel, ...) so
the empty name is unprintable; update TestTryPushDown_FailsClosed to use a slice
of {name, sel} pairs (or a small struct) instead of []string and change the loop
to t.Run(name, func(t *testing.T) { _, ok := TryPushDown(sel); assert.False(t,
ok, "selector %q must NOT push down", sel) }), giving the empty case an explicit
label like "empty" (or "empty-case") so all subtest names are printable and
stable; keep references to TryPushDown and TestTryPushDown_FailsClosed when
making the change.

100-113: 💤 Low value

The parameterized heuristic is loose — anchor it to the assert.NotContains for actual safety.

parameterized := strings.Contains(clause, "$") && !strings.Contains(clause, "DROP") is fragile: a $ can appear inside an escaped Postgres E-string (E'$1') without meaning a placeholder, and a clause that legitimately mentions DROP (say in a future test fixture) would flip the flag for an unrelated reason.

The real guarantee is on Line 111: assert.NotContains(t, clause, "test'; DROP TABLE"). That's the assertion that catches actual injection. The safe := doubled || backslash || parameterized block is a positive sanity check that would silently pass on something like 'test\'; DROP TABLE...' where the backslash escape isn't actually honored by Postgres' default standard_conforming_strings = on.

Tightening suggestion: drop the parameterized heuristic, keep only the doubled-quote check (cel2sql's actual behavior — confirmed in their docs which emit 'John Doe'-style literals), and rely on the NotContains for the hard fail.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown_test.go`
around lines 100 - 113, The parameterized/backslash heuristic is too loose —
remove the parameterized and backslash checks and make the positive sanity check
rely only on the doubled check; specifically, delete or stop using the
parameterized and backslash variables, set safe := doubled (using the existing
doubled := strings.Contains(clause, `''`)), and leave the assert.NotContains(t,
clause, `test'; DROP TABLE`) as the definitive safety check; also update the
assert.True message to reflect that only doubled-quote escaping is considered a
positive sanity check.
apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown.go (1)

53-70: 💤 Low value

Consider distinguishing "won't translate" from "translator failed".

Right now compilation errors, conversion errors, and "empty SQL" all collapse into the same (", false) return. That's correct for the runtime fallback contract, but it makes operational debugging harder when a selector that should push down silently doesn't (e.g. a cel2sql regression after a library bump). A trace span attribute or debug log on the failure paths — recording the selector hash and which step failed — would let you spot capability regressions without changing the public contract.

Not a blocker for merge; the silent fallback behavior is intentional. As per coding guidelines: "Write comments that explain why, document complex logic/algorithms, provide non-obvious context".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown.go`
around lines 53 - 70, The TryPushDown function currently collapses all failures
into ("", false), which hides whether compilation (env.Compile), conversion
(cel2sql.Convert) or an empty-SQL outcome failed; update TryPushDown to emit a
debug/trace record on each failure path (e.g., using your tracing/logging
helper) that records the selector identifier (hash of selector) and which step
failed (getPushdownEnv error, env.Compile issues, cel2sql.Convert error, or
empty SQL) without changing the public return values; reference TryPushDown,
getPushdownEnv, env.Compile and cel2sql.Convert when adding the logs/trace
attributes so operational teams can detect regressions while preserving the ("",
false) contract.
apps/workspace-engine/pkg/db/queries/deployment_versions.sql (1)

29-38: Add a composite index to back this keyset query.

The query filters on deployment_id, excludes two status values, and paginates by (created_at DESC, id DESC) with a tuple‑comparison cursor. Without a matching composite index, Postgres will fall back to a sort over the full deployable subset for every page, which defeats the purpose of moving to keyset pagination — especially under the singleflight burst fan‑out and per‑batch second/third page fetches added in getters_postgres.go.

A partial index on the deployable subset is the tightest fit:

CREATE INDEX deployment_version_deployable_keyset_idx
  ON deployment_version (deployment_id, created_at DESC, id DESC)
  WHERE status NOT IN ('rejected', 'building');

If you can't use a partial index (e.g. status set churns), a covering (deployment_id, status, created_at DESC, id DESC) works too but is bulkier.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/workspace-engine/pkg/db/queries/deployment_versions.sql` around lines 29
- 38, Add a composite index to support the keyset pagination used by the SQL
query named ListDeployableVersionsByDeploymentIDAfter on table
deployment_version: create a partial index (recommended) named
deployment_version_deployable_keyset_idx on (deployment_id, created_at DESC, id
DESC) with a WHERE clause excluding status IN ('rejected','building'); if
partial indexes aren’t allowed, create a covering index including status
(deployment_id, status, created_at DESC, id DESC) instead so the (created_at,id)
tuple comparison and ORDER BY can use the index without a full sort.
apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go (1)

124-176: ⚡ Quick win

Set span attributes via defer so error paths still record versions.scanned / version.found.

The early returns on lines 143 and 148 (when GetPolicySkips or evaluateVersion fails) bypass the span.SetAttributes(...) block at lines 169-173, so traces for those failure modes lose the deployment id and the scan progress. Moving the attribute set into a defer keeps the instrumentation contract uniform on all paths (success, iterator error, getter error).

♻️ Proposed refactor
 	_, span := tracer.Start(ctx, "FindDeployableVersion")
 	defer span.End()

 	var earliest *time.Time
 	var allEvaluations []VersionedEvaluation
 	var found *oapi.DeploymentVersion
 	var iterErr error
 	var scanned int
+
+	defer func() {
+		span.SetAttributes(
+			attribute.String("deployment.id", rt.DeploymentId),
+			attribute.Int("versions.scanned", scanned),
+			attribute.Bool("version.found", found != nil),
+		)
+	}()

 	for version, err := range versions {
 		...
 	}
-	span.SetAttributes(
-		attribute.String("deployment.id", rt.DeploymentId),
-		attribute.Int("versions.scanned", scanned),
-		attribute.Bool("version.found", found != nil),
-	)
 	if iterErr != nil {
 		return nil, iterErr
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go`
around lines 124 - 176, Move the span attribute setting into a deferred closure
immediately after creating the span so attributes are always recorded even on
early returns: after tracer.Start(ctx, "FindDeployableVersion") add defer func()
{ span.SetAttributes(attribute.String("deployment.id", rt.DeploymentId),
attribute.Int("versions.scanned", scanned), attribute.Bool("version.found",
found != nil)) }() (leave span.End() deferred as-is or combine end in the same
defer), so errors from GetPolicySkips or evaluateVersion and the iterator error
still record deployment id, scanned and found using the existing scanned and
found variables.
apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go (1)

188-206: ⚡ Quick win

Use slices.Sort instead of a hand-rolled insertion sort.

The "keep the import surface small" rationale doesn't hold — slices is part of the Go standard library and is already imported elsewhere in this module (e.g. policyeval.go). The custom O(n²) implementation adds maintenance surface without any benefit, and slices.Sort will outperform it once a deployment accumulates many translatable selectors.

♻️ Proposed refactor
 import (
 	"context"
 	"fmt"
+	"slices"
 	"time"

 	"github.com/charmbracelet/log"
@@
-	if len(clauses) > 1 {
-		// Stable order so the singleflight key is deterministic across
-		// reconciles that see the same logical clause set.
-		sortStrings(clauses)
-	}
+	// Stable order so the singleflight key is deterministic across
+	// reconciles that see the same logical clause set.
+	slices.Sort(clauses)
 	return clauses
 }
-
-// sortStrings is an in-place insertion sort. We use it instead of
-// sort.Strings to keep the import surface small for this hot path.
-func sortStrings(s []string) {
-	for i := 1; i < len(s); i++ {
-		j := i
-		for j > 0 && s[j-1] > s[j] {
-			s[j-1], s[j] = s[j], s[j-1]
-			j--
-		}
-	}
-}

slices.Sort no-ops on length-0/1 slices, so the len(clauses) > 1 guard isn't needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go` around
lines 188 - 206, Replace the hand-rolled insertion sort with the standard
library implementation: remove the custom sortStrings function and its call, and
use slices.Sort(clauses) where clauses is sorted (you can drop the len(clauses)
> 1 guard since slices.Sort is a no-op for short slices); update any references
to sortStrings accordingly (e.g., the call site in reconcile.go that currently
calls sortStrings(clauses)). Ensure the package imports include "slices" (it
already exists elsewhere) and remove the unused sortStrings function to avoid
dead code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go`:
- Around line 161-167: queryCandidateVersionsBatch is already wrapping errors
with fmt.Errorf("list versions for deployment %s: %w", deploymentID, err), so
avoid double-wrapping when calling it from the batch loop: remove the extra
fmt.Errorf wrapper and call yield(nil, err) directly (use the returned err from
queryCandidateVersionsBatch). Ensure the call-site that handles the first-batch
path uses the same strategy (yield the err returned by
queryCandidateVersionsBatch) so error messages are consistent; reference
symbols: queryCandidateVersionsBatch, yield, deploymentID.
- Around line 193-208: fetchFirstBatch uses g.firstBatchSF.Do which runs the
closure in the first caller's goroutine, so when that caller's ctx is cancelled
the shared queryCandidateVersionsBatch call fails for all waiters; change the
closure passed to g.firstBatchSF.Do in fetchFirstBatch to detach from the
caller's cancellation by creating qCtx := context.WithoutCancel(ctx) (and
optionally wrap with a shared timeout via context.WithTimeout and defer
cancel()) and pass qCtx into queryCandidateVersionsBatch instead of the original
ctx so individual caller cancellations don’t abort the shared singleflight work.

---

Nitpick comments:
In `@apps/workspace-engine/pkg/db/queries/deployment_versions.sql`:
- Around line 29-38: Add a composite index to support the keyset pagination used
by the SQL query named ListDeployableVersionsByDeploymentIDAfter on table
deployment_version: create a partial index (recommended) named
deployment_version_deployable_keyset_idx on (deployment_id, created_at DESC, id
DESC) with a WHERE clause excluding status IN ('rejected','building'); if
partial indexes aren’t allowed, create a covering index including status
(deployment_id, status, created_at DESC, id DESC) instead so the (created_at,id)
tuple comparison and ORDER BY can use the index without a full sort.

In
`@apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown_test.go`:
- Around line 67-81: The subtest for an empty selector uses t.Run(sel, ...) so
the empty name is unprintable; update TestTryPushDown_FailsClosed to use a slice
of {name, sel} pairs (or a small struct) instead of []string and change the loop
to t.Run(name, func(t *testing.T) { _, ok := TryPushDown(sel); assert.False(t,
ok, "selector %q must NOT push down", sel) }), giving the empty case an explicit
label like "empty" (or "empty-case") so all subtest names are printable and
stable; keep references to TryPushDown and TestTryPushDown_FailsClosed when
making the change.
- Around line 100-113: The parameterized/backslash heuristic is too loose —
remove the parameterized and backslash checks and make the positive sanity check
rely only on the doubled check; specifically, delete or stop using the
parameterized and backslash variables, set safe := doubled (using the existing
doubled := strings.Contains(clause, `''`)), and leave the assert.NotContains(t,
clause, `test'; DROP TABLE`) as the definitive safety check; also update the
assert.True message to reflect that only doubled-quote escaping is considered a
positive sanity check.

In
`@apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown.go`:
- Around line 53-70: The TryPushDown function currently collapses all failures
into ("", false), which hides whether compilation (env.Compile), conversion
(cel2sql.Convert) or an empty-SQL outcome failed; update TryPushDown to emit a
debug/trace record on each failure path (e.g., using your tracing/logging
helper) that records the selector identifier (hash of selector) and which step
failed (getPushdownEnv error, env.Compile issues, cel2sql.Convert error, or
empty SQL) without changing the public return values; reference TryPushDown,
getPushdownEnv, env.Compile and cel2sql.Convert when adding the logs/trace
attributes so operational teams can detect regressions while preserving the ("",
false) contract.

In `@apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go`:
- Around line 233-248: The extraWhere loop currently accepts raw strings which
relies on external discipline; change the API so TryPushDown (e.g.
versionselector.TryPushDown) returns a typed wrapper like type SafeWhereClause
string and update this function to accept []SafeWhereClause instead of []string,
then iterate over those wrappers when building the SQL (referencing extraWhere
and the candidateVersionColumns builder) to make accidental raw fragments a
compile-time error; additionally add a cheap runtime sanity-check inside the
loop to detect any fragment containing an unquoted semicolon (panic or skip the
fragment) to guard against future translator regressions.

In
`@apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go`:
- Around line 124-176: Move the span attribute setting into a deferred closure
immediately after creating the span so attributes are always recorded even on
early returns: after tracer.Start(ctx, "FindDeployableVersion") add defer func()
{ span.SetAttributes(attribute.String("deployment.id", rt.DeploymentId),
attribute.Int("versions.scanned", scanned), attribute.Bool("version.found",
found != nil)) }() (leave span.End() deferred as-is or combine end in the same
defer), so errors from GetPolicySkips or evaluateVersion and the iterator error
still record deployment id, scanned and found using the existing scanned and
found variables.

In `@apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go`:
- Around line 188-206: Replace the hand-rolled insertion sort with the standard
library implementation: remove the custom sortStrings function and its call, and
use slices.Sort(clauses) where clauses is sorted (you can drop the len(clauses)
> 1 guard since slices.Sort is a no-op for short slices); update any references
to sortStrings accordingly (e.g., the call site in reconcile.go that currently
calls sortStrings(clauses)). Ensure the package imports include "slices" (it
already exists elsewhere) and remove the unused sortStrings function to avoid
dead code.

In `@apps/workspace-engine/test/controllers/harness/mocks.go`:
- Around line 170-182: Add a one-line comment above the
DesiredReleaseGetter.IterCandidateVersions function explaining that this mock
intentionally ignores the extraWhere parameter and yields every version in
g.Versions in order (i.e., it does not apply pushdown filtering), and note that
pushdown filtering is tested against the real Postgres getter (pushdown_test.go)
so this mock is only for testing reconcile logic given a candidate stream.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 16ecb207-2bf1-4b20-836f-b2c41925b27c

📥 Commits

Reviewing files that changed from the base of the PR and between 46b08f5 and 04b1156.

⛔ Files ignored due to path filters (1)
  • apps/workspace-engine/go.sum is excluded by !**/*.sum
📒 Files selected for processing (15)
  • apps/workspace-engine/go.mod
  • apps/workspace-engine/pkg/db/deployment_versions.sql.go
  • apps/workspace-engine/pkg/db/queries/deployment_versions.sql
  • apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown.go
  • apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/pushdown_test.go
  • apps/workspace-engine/svc/controllers/desiredrelease/getters.go
  • apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres.go
  • apps/workspace-engine/svc/controllers/desiredrelease/getters_postgres_test.go
  • apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval.go
  • apps/workspace-engine/svc/controllers/desiredrelease/policyeval/policyeval_test.go
  • apps/workspace-engine/svc/controllers/desiredrelease/pushdown_test.go
  • apps/workspace-engine/svc/controllers/desiredrelease/reconcile.go
  • apps/workspace-engine/svc/controllers/desiredrelease/reconcile_test.go
  • apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters_postgres.go
  • apps/workspace-engine/test/controllers/harness/mocks.go
💤 Files with no reviewable changes (1)
  • apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/getters_postgres.go

* singleflight.Group.Do runs the closure in the first caller's goroutine,
  so the first caller's ctx cancellation aborted the shared DB query for
  every other waiter on the same key. fetchFirstBatch now passes
  context.WithoutCancel(ctx) into the closure — cancellation is detached
  from any individual caller while trace context is preserved. Per-pool
  timeouts still bound runaway queries.
* The batch-loop in IterCandidateVersions wrapped errors a second time
  even though queryCandidateVersionsBatch already formats them as
  "list versions for deployment %s: %w". The first-batch path passed the
  error through unwrapped, so the two sites produced inconsistent messages
  ("list versions for deployment X: list versions for deployment X: ...").
  Drop the outer wrap so both paths surface a single layer.
@mleonidas mleonidas merged commit c32246b into main May 5, 2026
19 checks passed
@mleonidas mleonidas deleted the 1101-bug-updating-version-selector-is-not-triggering-reconcile-of-targets branch May 5, 2026 20:39
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.

bug: updating version selector is not triggering reconcile of targets

2 participants