Skip to content

feat(workflow-engine): Adds pagination and optimizes db hot path(s)#18366

Merged
danielskovli merged 14 commits into
mainfrom
feature/workflow-engine-pagination
Apr 10, 2026
Merged

feat(workflow-engine): Adds pagination and optimizes db hot path(s)#18366
danielskovli merged 14 commits into
mainfrom
feature/workflow-engine-pagination

Conversation

@danielskovli
Copy link
Copy Markdown
Contributor

@danielskovli danielskovli commented Mar 31, 2026

Summary

  • Adds cursor-based pagination to workflow list endpoints, returning a paginated envelope with data, nextCursor, and optional totalCount
  • Prevents unbounded responses — previously returned all active workflows with no limit
  • Improves stampede resilience and reduces database contention under heavy load

Pagination

  • Cursor-based pagination (?pageSize=25&cursor=<guid>) on GET /api/v1/{namespace}/workflows and related endpoints
  • PaginationSettings in EngineSettings controls default/max page sizes
  • Updates k6 stress test drain logic to use ?pageSize=1 for lightweight polling

Stampede / DB contention improvements

  • Heartbeat piggyback: Status writes now set HeartbeatAt = @now for Processing workflows, eliminating a deadlock vector between the HeartbeatService and UpdateBuffer
  • Heartbeat skip: HeartbeatService skips workflows with a recent UpdatedAt, avoiding redundant row locks on actively-processing workflows
  • Update buffer simplification: Removed concurrent flush support (FlushConcurrency) — sequential flushing with batch deduplication and a coalescing yield before drain
  • Fire-and-forget step-started updates: SubmitAndForget() for step Processing status — workers proceed to execution immediately without awaiting the DB write
  • Configurable DB instrumentation: EF Core + Npgsql tracing/metrics are now opt-in via EnableDatabaseInstrumentation (defaults to false)

Stress test results (50k workflows, 550k steps)

  • Eliminated all Postgres deadlocks (previously sporadic but consistent under load)
  • ~40% fewer DB round-trips (larger batches + fire-and-forget)
  • ~20% faster per-step service time
  • Zero errors, zero reclaims

Breaking change

The response shape for workflow list endpoints changes from a bare JSON array to a paginated envelope object. Consumers will need to read .data for the workflow list.

Test plan

  • All unit tests pass (148 core + 75 app)
  • New test: Submit_DeduplicatesSameWorkflowInBatch
  • Stress tested at 50k workflows / 550k steps — no deadlocks, no regressions
  • Manual: verify pagination works end-to-end
  • Manual: verify k6 stress test completes correctly

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 31, 2026

📝 Walkthrough

Walkthrough

This pull request introduces pagination support to the workflow engine's active workflows listing API endpoint. The changes replace single-response calls with paginated results including metadata (Page, PageSize, TotalCount, TotalPages), add configuration for pagination defaults and bounds, and update all corresponding tests and documentation.

Changes

Cohort / File(s) Summary
Documentation Updates
AGENTS.md, README.md, docs/presentation-technical.md
Updated API endpoint documentation to reflect pagination parameters (page, pageSize) for the GET /api/v1/{namespace}/workflows endpoint.
Pagination Models
src/WorkflowEngine.Models/PaginatedResponse.cs, src/WorkflowEngine.Models/EngineSettings.cs
Added new PaginatedResponse<T> generic record with Data, Page, PageSize, TotalCount, and computed TotalPages properties. Introduced PaginationSettings configuration with defaultPageSize (25) and maxPageSize (100).
Repository Layer
src/WorkflowEngine.Data/Repository/IEngineRepository.cs, src/WorkflowEngine.Data/Repository/EngineRepository.Reads.cs
Replaced GetActiveWorkflowsByCorrelationId() with new GetActiveWorkflowsPaginated(page, pageSize, ...) method returning tuple of (Workflows, TotalCount). Implemented pagination logic with filtered query, total count computation, and skip/take operations.
Endpoint Handler
src/WorkflowEngine.Core/Endpoints/EngineEndpoints.cs
Updated ListActiveWorkflows to accept optional pagination query parameters, apply pagination settings bounds (clamping), call the paginated repository method, return 204 NoContent when no results, and serialize response as PaginatedResponse<WorkflowStatusResponse>.
Load Testing
.k6/lib/helpers.js
Modified waitForQueueDrain() polling to include pageSize=1 query parameter and log totalCount from JSON response instead of array length.
Unit & Integration Tests
tests/WorkflowEngine.Core.Tests/Endpoints/EngineEndpointTests.cs, tests/WorkflowEngine.Repository.Tests/WorkflowQueryTests.cs, tests/WorkflowEngine.Integration.Tests/.snapshots/..., tests/WorkflowEngine.Integration.Tests/TelemetryTests.cs
Updated test mocks to use GetActiveWorkflowsPaginated with tuple returns, added assertions for PaginatedResponse structure, verified page-size clamping behaviour, and updated snapshot output and telemetry expectations.
Test Client
tests/WorkflowEngine.TestKit/EngineApiClient.cs
Added new ListActiveWorkflowsPaginated(page?, pageSize?, ns?) method with paginated response deserialisation and 204 handling. Reimplemented original ListActiveWorkflows() as a wrapper that iterates through all pages and aggregates results.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Endpoint as EngineEndpoints
    participant Repo as EngineRepository
    participant DB as Database
    
    Client->>Endpoint: GET /workflows?page=1&pageSize=25
    Endpoint->>Endpoint: Apply pagination settings<br/>(clamp pageSize to max)
    Endpoint->>Repo: GetActiveWorkflowsPaginated<br/>(page=1, pageSize=25)
    Repo->>DB: SELECT COUNT(*) FROM workflows<br/>(with filters)
    DB-->>Repo: totalCount
    alt totalCount == 0
        Repo-->>Endpoint: Return 204 NoContent
        Endpoint-->>Client: 204 No Content
    else totalCount > 0
        Repo->>DB: SELECT * FROM workflows<br/>ORDER BY CreatedAt<br/>SKIP 0 TAKE 25
        DB-->>Repo: workflows (page data)
        Repo-->>Endpoint: (workflows, totalCount)
        Endpoint->>Endpoint: Map to PaginatedResponse<br/>(data, page, pageSize,<br/>totalCount, totalPages)
        Endpoint-->>Client: 200 OK + PaginatedResponse JSON
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Pagination hops with delight,
Pages numbered, data's just right,
TotalCounts and pageSize too,
Workflows paginated, fresh and new! 🌿

🚥 Pre-merge checks | ✅ 1 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is largely incomplete and differs significantly from the template requirements; it lacks verification checkboxes and manual testing confirmation. Complete the PR description by adding the required verification section with checkboxes for related issues, code build status, manual testing confirmation, and relevant automated test details.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main changes: adding pagination to the workflow-engine and optimizing database queries, which aligns with the comprehensive changeset.

✏️ 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 feature/workflow-engine-pagination

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.

@github-actions github-actions Bot added the skip-releasenotes Issues that do not make sense to list in our release notes label Mar 31, 2026
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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/Runtime/workflow-engine/tests/WorkflowEngine.Repository.Tests/Fixtures/WorkflowTestHelper.cs (1)

13-23: ⚠️ Potential issue | 🟡 Minor

Avoid silently overriding metadata.Namespace in the test helper.

CreateRequest() now returns namespace-bearing metadata, but EnqueueWorkflows() still defaults ns to "test-namespace" and overwrites the metadata at Line 23. Any test that passes only metadata will enqueue into the wrong namespace and stop exercising the intended case.

Suggested fix
     public static async Task<BatchEnqueueResult[]> EnqueueWorkflows(
         IEngineRepository repository,
         WorkflowRequestMetadata metadata,
         IReadOnlyList<WorkflowRequest> workflows,
         string? idempotencyKey = null,
-        string ns = "test-namespace",
+        string? ns = null,
         Dictionary<string, string>? labels = null
     )
     {
+        ns ??= metadata.Namespace;
         var resolvedKey =
             idempotencyKey
             ?? (string.IsNullOrEmpty(metadata.IdempotencyKey) ? Guid.NewGuid().ToString("N") : metadata.IdempotencyKey);
         var resolvedMetadata = metadata with { Namespace = ns, IdempotencyKey = resolvedKey };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Runtime/workflow-engine/tests/WorkflowEngine.Repository.Tests/Fixtures/WorkflowTestHelper.cs`
around lines 13 - 23, The helper currently overwrites metadata.Namespace
unconditionally; change the logic in EnqueueWorkflows so resolvedMetadata
preserves an existing metadata.Namespace when present instead of always using
the ns parameter. Concretely, when constructing resolvedMetadata (the spot that
sets Namespace and IdempotencyKey), set Namespace to metadata.Namespace if it is
non-empty, otherwise fall back to the ns parameter, and still apply the
resolvedKey to IdempotencyKey; this keeps CreateRequest-provided namespaces
intact while preserving existing idempotency handling (look for the resolvedKey
and resolvedMetadata variables in the method).
src/Runtime/workflow-engine/docs/technical-guide.md (1)

361-369: ⚠️ Potential issue | 🟡 Minor

Documentation example inconsistent with implementation.

The request body example still shows idempotencyKey and correlationId as JSON body fields, but according to the PR changes and HTTP snapshots, these are now passed via query parameters (?idempotencyKey=...&correlationId=...) or headers rather than in the request body.

Consider updating the example to reflect the new API contract:

📝 Suggested documentation update
-POST /api/v1/{namespace}/workflows
+POST /api/v1/{namespace}/workflows?idempotencyKey=process-next-abc123&correlationId=a1b2c3d4-e5f6-7890-abcd-ef1234567890

And remove idempotencyKey and correlationId from the JSON body example, or add a note explaining these are extracted from query parameters/headers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Runtime/workflow-engine/docs/technical-guide.md` around lines 361 - 369,
The JSON example in the technical-guide shows idempotencyKey and correlationId
as body fields (see the example block containing "idempotencyKey" and
"correlationId" and the sample id "process-next-abc123"), but the implementation
extracts those values from query parameters or headers; update the example to
remove idempotencyKey and correlationId from the JSON payload (leave the
"labels" object/other body fields intact) and either show the request URL with
?idempotencyKey=...&correlationId=... or add a short note stating that
idempotencyKey and correlationId are provided via query parameters or headers as
per the new API contract.
src/Runtime/workflow-engine/tests/WorkflowEngine.TestKit/EngineApiClient.cs (1)

131-154: ⚠️ Potential issue | 🟠 Major

Keep workflow reads namespace-aware as well.

A workflow created via Enqueue(..., ns: otherNs) can no longer be fetched or awaited with the same client instance, because these reads always go to _defaultNamespace. That turns valid non-default-namespace workflows into 404s and polling time-outs.

🔧 Possible fix
-public Task<HttpResponseMessage> GetWorkflowRaw(Guid workflowId) =>
-    _client.GetAsync($"{GetBasePath()}/{workflowId}", CancellationToken.None);
+public Task<HttpResponseMessage> GetWorkflowRaw(Guid workflowId, string? ns = null) =>
+    _client.GetAsync($"{GetBasePath(ns)}/{workflowId}", CancellationToken.None);

-public async Task<WorkflowStatusResponse?> GetWorkflow(Guid workflowId)
+public async Task<WorkflowStatusResponse?> GetWorkflow(Guid workflowId, string? ns = null)
 {
-    using var response = await GetWorkflowRaw(workflowId);
+    using var response = await GetWorkflowRaw(workflowId, ns);

Then thread the same optional ns through WaitForWorkflowStatus(...), otherwise polling still stays pinned to the default namespace.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Runtime/workflow-engine/tests/WorkflowEngine.TestKit/EngineApiClient.cs`
around lines 131 - 154, GetWorkflow/GetWorkflowRaw always call GetBasePath()
which uses the default namespace, causing reads of workflows created with
Enqueue(..., ns: otherNs) to 404; make namespace-aware reads by adding an
optional ns parameter to GetWorkflowRaw and GetWorkflow (and thread that ns
through any polling helper like WaitForWorkflowStatus), update GetBasePath usage
to include the ns when provided (e.g., GetBasePath(ns)), and ensure callers
(including tests that call GetWorkflow/WaitForWorkflowStatus) pass the same ns
used in Enqueue so polling and direct fetches target the correct namespace.
🧹 Nitpick comments (8)
src/Runtime/workflow-engine-app/tests/WorkflowEngine.App.Tests/Integration/AppCommandIntegrationTests.cs (1)

159-173: Assert the callback side effect once as well.

This only proves deduplicated creation. A regression could still execute the same workflow twice and this test would stay green. Given the test name, I'd also wait for completion and assert a single WireMock hit.

Suggested test hardening
         var response1 = await _client.Enqueue(request, idempotencyKey: "idempotent-test-key");
         var response2 = await _client.Enqueue(request, idempotencyKey: "idempotent-test-key");
 
         // Both should return the same workflow ID
-        Assert.Equal(response1.Workflows.Single().DatabaseId, response2.Workflows.Single().DatabaseId);
+        var workflowId = response1.Workflows.Single().DatabaseId;
+        Assert.Equal(workflowId, response2.Workflows.Single().DatabaseId);
+        await _client.WaitForWorkflowStatus(workflowId, PersistentItemStatus.Completed);
+        Assert.Single(fixture.WireMock.LogEntries);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Runtime/workflow-engine-app/tests/WorkflowEngine.App.Tests/Integration/AppCommandIntegrationTests.cs`
around lines 159 - 173, The test AppCommand_IdempotentResubmit_DoesNotDuplicate
only asserts deduplicated creation but not that the workflow executed once;
after enqueuing (using _client.Enqueue and inspecting
response1.Workflows.Single().DatabaseId) wait for the workflow to complete (use
the existing test helper that polls by DatabaseId or implement a short retry
loop against the workflow status) and then assert the external callback/mock was
hit exactly once (assert the WireMock/mock server hit count == 1) to ensure the
workflow side effect ran only once.
src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/query.js (1)

76-79: Avoid silently dropping namespace filters when more than one value exists.

If values.size > 1, the current branch emits no namespace constraint at all. Consider a fallback so filter intent is preserved.

Defensive fallback
-                if (key === 'namespace') {
-                    // Send as dedicated namespace param when a single namespace is selected
-                    if (values.size === 1) params.set('namespace', [...values][0]);
+                if (key === 'namespace') {
+                    // Dedicated param supports one namespace; preserve filtering intent for multi-value sets
+                    if (values.size === 1) {
+                        params.set('namespace', [...values][0]);
+                    } else {
+                        for (const v of values) labelPairs.push(`namespace:${v}`);
+                    }
                 } else if (key === 'correlationId') {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/query.js`
around lines 76 - 79, The current namespace branch only sets
params.set('namespace', ...) when values.size === 1 and drops the filter for
multi-value sets; update the branch in query.js (the block checking if (key ===
'namespace')) to preserve intent by falling back for values.size > 1 — e.g.,
serialize multiple namespaces into the query (use params.set('namespace',
Array.from(values).join(',')) or alternatively append each value with
params.append('namespace', v)) while keeping the existing single-value behavior
so callers that read 'namespace' still receive a clear constraint.
src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/index.html (1)

39-47: Consider adding basic ARIA wiring for the new namespace dropdown.

A small a11y improvement: connect the toggle/panel and give the search box an explicit accessible name.

Possible markup improvement
-    <button class="dropdown-toggle" onclick="toggleDropdown('ns-dropdown')">
+    <button class="dropdown-toggle" onclick="toggleDropdown('ns-dropdown')" aria-controls="ns-panel" aria-expanded="false">
@@
-    <div class="dropdown-panel">
-      <input class="dropdown-search" type="text" placeholder="Search..." autocomplete="off" spellcheck="false">
+    <div class="dropdown-panel" id="ns-panel">
+      <input class="dropdown-search" type="text" placeholder="Search..." aria-label="Search namespaces" autocomplete="off" spellcheck="false">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/index.html`
around lines 39 - 47, Add basic ARIA wiring for the namespace dropdown: give the
panel element the id referenced by the toggle (use id="ns-dropdown" on the
div.dropdown-panel), add aria-controls="ns-dropdown", aria-expanded (update
true/false when toggleDropdown runs) and aria-haspopup="listbox" to the
button.dropdown-toggle, give the search input.dropdown-search an explicit
accessible name (e.g., aria-label="Search namespaces"), and mark the list
container (div#ns-list) with role="listbox" and ensure the selected span
(id="ns-selected") is announced (e.g., aria-live="polite" or aria-atomic) so
screen readers know selection changes; update toggleDropdown to flip
aria-expanded accordingly.
src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/.snapshots/EngineTests.WebhookCommand_FullHttpChatter_DocumentsExchange.http (1)

77-77: Clarify: Double-slash in step idempotency keys is intentional?

The step idempotency keys show chatter-idem-key//step-1 and chatter-idem-key//step-2 (double slash). This appears to result from the operationId being /step-1 (with leading slash), combined with the format {workflowIdempotencyKey}/{stepOperationId}.

If this is intentional, consider adding a brief comment in the snapshot or test explaining the double-slash pattern to avoid future confusion.

Also applies to: 93-93

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/.snapshots/EngineTests.WebhookCommand_FullHttpChatter_DocumentsExchange.http`
at line 77, The snapshot shows an Idempotency-Key value with a double slash
("chatter-idem-key//step-1") caused by concatenating workflowIdempotencyKey and
stepOperationId where stepOperationId comes from an operationId with a leading
slash (e.g., "/step-1"); either normalize the step id before concatenation by
trimming any leading slashes (so the code that builds the header removes leading
'/' from stepOperationId/operationId), or if the double-slash is intentional,
add a short explanatory comment in the test/snapshot near the Idempotency-Key to
document that "{workflowIdempotencyKey}/{stepOperationId}" can produce a
double-slash when operationId starts with '/'.
src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/state-modal.js (1)

72-76: JSDoc missing wfNamespace parameter documentation.

The function signature was updated to include wfNamespace, but the JSDoc comment only documents wfId.

📝 Proposed fix
 /**
  * Fetch and render the state trail.
  * `@param` {string} wfId - workflow databaseId
+ * `@param` {string} wfNamespace - workflow namespace
  */
 const fetchAndRender = async (wfId, wfNamespace) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/state-modal.js`
around lines 72 - 76, The JSDoc for fetchAndRender is missing documentation for
the new wfNamespace parameter; update the comment block above the function
(fetchAndRender) to add a `@param` entry for wfNamespace describing its purpose
(e.g., workflow namespace or context) alongside the existing `@param` for wfId so
the signature and documentation match.
src/Runtime/workflow-engine/.k6/lib/helpers.js (1)

130-135: Spin-wait sleep implementation is CPU-intensive.

The sleepMs function uses a busy-wait loop which consumes CPU cycles unnecessarily. While k6 lacks a native millisecond sleep, consider using sleep() from k6 with fractional seconds.

🔧 Alternative using k6's sleep
+import { sleep } from 'k6';

-function sleepMs(ms) {
-    const end = Date.now() + ms;
-    while (Date.now() < end) {
-        // spin — k6 doesn't have a millisecond sleep
-    }
-}
+function sleepMs(ms) {
+    sleep(ms / 1000);
+}

k6's sleep() accepts fractional seconds (e.g., sleep(0.5) for 500ms), which is more efficient than spinning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Runtime/workflow-engine/.k6/lib/helpers.js` around lines 130 - 135, The
current sleepMs function uses a CPU-intensive busy-wait loop; replace its
implementation to call k6's sleep with fractional seconds instead of spinning
(update the sleepMs function to convert milliseconds to seconds and call
sleep(ms/1000)). Locate the sleepMs function in this file and ensure the k6
sleep import is present where needed so callers of sleepMs continue to work with
millisecond input converted to seconds.
src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/modal.js (1)

447-454: Consider clearing _openWfNamespace on modal close.

_openWfId and _openStepKey are cleared but _openWfNamespace is not. This creates inconsistency and could lead to stale namespace values if the modal is reopened.

🔧 Suggested fix
 window.closeModal = () => {
     _openWfId = '';
+    _openWfNamespace = '';
     _openStepKey = '';
     clearTimeout(_refreshTimer);
     dom.modal.classList.remove('open');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/modal.js`
around lines 447 - 454, The modal close handler (window.closeModal) clears
_openWfId and _openStepKey but omits _openWfNamespace, leaving a stale
namespace; update the window.closeModal implementation to also reset
_openWfNamespace (alongside _openWfId and _openStepKey), and ensure any
associated state cleanup (e.g., clearTimeout(_refreshTimer), hiding
dom.modalTabs/dom.modalSubtabs) remains intact so the namespace cannot persist
when the modal is reopened.
src/Runtime/workflow-engine/src/WorkflowEngine.Core/Endpoints/DashboardEndpoints.cs (1)

579-588: Inconsistent variable naming: nsProp2 vs nsProp.

The variable is named nsProp2 but there's no nsProp in this handler's scope (unlike the retry handler). This appears to be copy-paste residue.

🔧 Rename for consistency
                     if (
-                        !doc.RootElement.TryGetProperty("namespace", out var nsProp2)
-                        || nsProp2.ValueKind != JsonValueKind.String
-                        || string.IsNullOrWhiteSpace(nsProp2.GetString())
+                        !doc.RootElement.TryGetProperty("namespace", out var nsProp)
+                        || nsProp.ValueKind != JsonValueKind.String
+                        || string.IsNullOrWhiteSpace(nsProp.GetString())
                     )
                     {
                         return Results.BadRequest("Missing namespace");
                     }
 
-                    string ns = nsProp2.GetString() ?? throw new UnreachableException();
+                    string ns = nsProp.GetString() ?? throw new UnreachableException();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Runtime/workflow-engine/src/WorkflowEngine.Core/Endpoints/DashboardEndpoints.cs`
around lines 579 - 588, The handler contains a leftover variable name nsProp2
(used when reading the "namespace" property) which is inconsistent with other
handlers that use nsProp; rename nsProp2 to nsProp and update its usages
(including the subsequent GetString() call that assigns string ns) so the
variable naming matches the rest of the code and removes the copy-paste residue
in the JSON property parsing block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/Runtime/workflow-engine-app/tests/WorkflowEngine.App.Tests/Fixtures/AppTestHelpers.cs`:
- Around line 89-93: The test helper assigns a nullable parameter using the
null-forgiving operator to the required non-nullable property
AppWorkflowContext.LockToken; change the helper method signature to make the
lockToken parameter non-nullable (remove the '?' from its type) and stop using
the null-forgiving operator so you can assign LockToken = lockToken directly, or
alternatively add an explicit guard (e.g., throw ArgumentNullException if
lockToken is null) before assigning to AppWorkflowContext.LockToken to satisfy
the nullability contract.

In `@src/Runtime/workflow-engine/docs/presentation-technical.md`:
- Around line 266-267: Update the documentation for the list endpoint GET
/api/v1/{namespace}/workflows to explicitly describe the new pagination
contract: document the request query parameters page and pageSize (including
defaults and limits if any) and show the paginated response envelope shape with
keys { data, page, pageSize, totalCount, totalPages }; also give a small example
request and response snippet illustrating a filtered, paginated call and a
response containing the data array and those pagination fields so consumers know
how to page through results.

In `@src/Runtime/workflow-engine/README.md`:
- Line 57: Update the documentation for the GET /api/v1/{namespace}/workflows
endpoint to reflect the new pagination contract: state that the endpoint accepts
optional query parameters ?page and ?pageSize, and that responses return a
paginated envelope with the fields data (array of workflows), page, pageSize,
totalCount, and totalPages; update the description text where GET
/api/v1/{namespace}/workflows is listed and add a short example or sentence
showing the query params and the envelope field names (data, page, pageSize,
totalCount, totalPages).

In
`@src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/filters.js`:
- Around line 119-148: The event listener is attached to dom.nsList without
checking it exists, which throws if the element is not in the DOM; guard the
attachment by first checking dom.nsList (e.g., if (!dom.nsList) return or wrap
the listener registration in if (dom.nsList) { ... }) before calling
dom.nsList.addEventListener, then keep the existing handler logic (uses
state.namespaceFilter, syncNamespaceToLabelFilter, rebuildNamespaceDropdown)
unchanged.

In `@src/Runtime/workflow-engine/src/WorkflowEngine.Models/PaginatedResponse.cs`:
- Around line 25-38: The TotalPages getter trusts PageSize and can divide by
zero or negative values; validate PageSize on assignment to ensure it's
positive. Update the PaginatedResponse model so PageSize (the required int
PageSize { get; init; }) enforces >0 (throw ArgumentOutOfRangeException or
similar during init) or replace the auto-init with a validated init/backing
field, and keep TotalPages as (int)Math.Ceiling((double)TotalCount / PageSize)
assuming PageSize is guaranteed positive.

In `@src/Runtime/workflow-engine/tests/WorkflowEngine.TestKit/EngineApiClient.cs`:
- Around line 221-224: ListActiveWorkflows currently returns only the first page
from ListActiveWorkflowsPaginated (dropping subsequent pages). Update
EngineApiClient.ListActiveWorkflows to aggregate all pages: call
ListActiveWorkflowsPaginated to get the first page, append its Data, then
iterate using the paginated response's next-page mechanism (e.g., next token /
page fetch or a provided Pages/HasMore iterator) to fetch and append Data from
each subsequent page until no more pages remain; return the full concatenated
List<WorkflowStatusResponse>. If first-page-only behavior was intended, rename
the method to make that explicit.

---

Outside diff comments:
In `@src/Runtime/workflow-engine/docs/technical-guide.md`:
- Around line 361-369: The JSON example in the technical-guide shows
idempotencyKey and correlationId as body fields (see the example block
containing "idempotencyKey" and "correlationId" and the sample id
"process-next-abc123"), but the implementation extracts those values from query
parameters or headers; update the example to remove idempotencyKey and
correlationId from the JSON payload (leave the "labels" object/other body fields
intact) and either show the request URL with
?idempotencyKey=...&correlationId=... or add a short note stating that
idempotencyKey and correlationId are provided via query parameters or headers as
per the new API contract.

In
`@src/Runtime/workflow-engine/tests/WorkflowEngine.Repository.Tests/Fixtures/WorkflowTestHelper.cs`:
- Around line 13-23: The helper currently overwrites metadata.Namespace
unconditionally; change the logic in EnqueueWorkflows so resolvedMetadata
preserves an existing metadata.Namespace when present instead of always using
the ns parameter. Concretely, when constructing resolvedMetadata (the spot that
sets Namespace and IdempotencyKey), set Namespace to metadata.Namespace if it is
non-empty, otherwise fall back to the ns parameter, and still apply the
resolvedKey to IdempotencyKey; this keeps CreateRequest-provided namespaces
intact while preserving existing idempotency handling (look for the resolvedKey
and resolvedMetadata variables in the method).

In `@src/Runtime/workflow-engine/tests/WorkflowEngine.TestKit/EngineApiClient.cs`:
- Around line 131-154: GetWorkflow/GetWorkflowRaw always call GetBasePath()
which uses the default namespace, causing reads of workflows created with
Enqueue(..., ns: otherNs) to 404; make namespace-aware reads by adding an
optional ns parameter to GetWorkflowRaw and GetWorkflow (and thread that ns
through any polling helper like WaitForWorkflowStatus), update GetBasePath usage
to include the ns when provided (e.g., GetBasePath(ns)), and ensure callers
(including tests that call GetWorkflow/WaitForWorkflowStatus) pass the same ns
used in Enqueue so polling and direct fetches target the correct namespace.

---

Nitpick comments:
In
`@src/Runtime/workflow-engine-app/tests/WorkflowEngine.App.Tests/Integration/AppCommandIntegrationTests.cs`:
- Around line 159-173: The test AppCommand_IdempotentResubmit_DoesNotDuplicate
only asserts deduplicated creation but not that the workflow executed once;
after enqueuing (using _client.Enqueue and inspecting
response1.Workflows.Single().DatabaseId) wait for the workflow to complete (use
the existing test helper that polls by DatabaseId or implement a short retry
loop against the workflow status) and then assert the external callback/mock was
hit exactly once (assert the WireMock/mock server hit count == 1) to ensure the
workflow side effect ran only once.

In `@src/Runtime/workflow-engine/.k6/lib/helpers.js`:
- Around line 130-135: The current sleepMs function uses a CPU-intensive
busy-wait loop; replace its implementation to call k6's sleep with fractional
seconds instead of spinning (update the sleepMs function to convert milliseconds
to seconds and call sleep(ms/1000)). Locate the sleepMs function in this file
and ensure the k6 sleep import is present where needed so callers of sleepMs
continue to work with millisecond input converted to seconds.

In
`@src/Runtime/workflow-engine/src/WorkflowEngine.Core/Endpoints/DashboardEndpoints.cs`:
- Around line 579-588: The handler contains a leftover variable name nsProp2
(used when reading the "namespace" property) which is inconsistent with other
handlers that use nsProp; rename nsProp2 to nsProp and update its usages
(including the subsequent GetString() call that assigns string ns) so the
variable naming matches the rest of the code and removes the copy-paste residue
in the JSON property parsing block.

In `@src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/index.html`:
- Around line 39-47: Add basic ARIA wiring for the namespace dropdown: give the
panel element the id referenced by the toggle (use id="ns-dropdown" on the
div.dropdown-panel), add aria-controls="ns-dropdown", aria-expanded (update
true/false when toggleDropdown runs) and aria-haspopup="listbox" to the
button.dropdown-toggle, give the search input.dropdown-search an explicit
accessible name (e.g., aria-label="Search namespaces"), and mark the list
container (div#ns-list) with role="listbox" and ensure the selected span
(id="ns-selected") is announced (e.g., aria-live="polite" or aria-atomic) so
screen readers know selection changes; update toggleDropdown to flip
aria-expanded accordingly.

In
`@src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/modal.js`:
- Around line 447-454: The modal close handler (window.closeModal) clears
_openWfId and _openStepKey but omits _openWfNamespace, leaving a stale
namespace; update the window.closeModal implementation to also reset
_openWfNamespace (alongside _openWfId and _openStepKey), and ensure any
associated state cleanup (e.g., clearTimeout(_refreshTimer), hiding
dom.modalTabs/dom.modalSubtabs) remains intact so the namespace cannot persist
when the modal is reopened.

In
`@src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/query.js`:
- Around line 76-79: The current namespace branch only sets
params.set('namespace', ...) when values.size === 1 and drops the filter for
multi-value sets; update the branch in query.js (the block checking if (key ===
'namespace')) to preserve intent by falling back for values.size > 1 — e.g.,
serialize multiple namespaces into the query (use params.set('namespace',
Array.from(values).join(',')) or alternatively append each value with
params.append('namespace', v)) while keeping the existing single-value behavior
so callers that read 'namespace' still receive a clear constraint.

In
`@src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/state-modal.js`:
- Around line 72-76: The JSDoc for fetchAndRender is missing documentation for
the new wfNamespace parameter; update the comment block above the function
(fetchAndRender) to add a `@param` entry for wfNamespace describing its purpose
(e.g., workflow namespace or context) alongside the existing `@param` for wfId so
the signature and documentation match.

In
`@src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/.snapshots/EngineTests.WebhookCommand_FullHttpChatter_DocumentsExchange.http`:
- Line 77: The snapshot shows an Idempotency-Key value with a double slash
("chatter-idem-key//step-1") caused by concatenating workflowIdempotencyKey and
stepOperationId where stepOperationId comes from an operationId with a leading
slash (e.g., "/step-1"); either normalize the step id before concatenation by
trimming any leading slashes (so the code that builds the header removes leading
'/' from stepOperationId/operationId), or if the double-slash is intentional,
add a short explanatory comment in the test/snapshot near the Idempotency-Key to
document that "{workflowIdempotencyKey}/{stepOperationId}" can produce a
double-slash when operationId starts with '/'.
🪄 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: 711ea517-ab7d-4a8f-be37-255ec7f43ffb

📥 Commits

Reviewing files that changed from the base of the PR and between 43a2da5 and 94033fb.

📒 Files selected for processing (80)
  • src/Runtime/workflow-engine-app/.k6/README.md
  • src/Runtime/workflow-engine-app/.k6/constant-rate.js
  • src/Runtime/workflow-engine-app/.k6/continuous-process-next.js
  • src/Runtime/workflow-engine-app/.k6/payloads/process-next.json
  • src/Runtime/workflow-engine-app/.k6/stress-test.js
  • src/Runtime/workflow-engine-app/Makefile
  • src/Runtime/workflow-engine-app/src/WorkflowEngine.App/Commands/AppCommand/AppCommand.cs
  • src/Runtime/workflow-engine-app/src/WorkflowEngine.App/appsettings.Docker.json
  • src/Runtime/workflow-engine-app/tests/WorkflowEngine.App.Tests/.snapshots/AppCommandIntegrationTests.AppCommand_FullHttpChatter_DocumentsExchange.http
  • src/Runtime/workflow-engine-app/tests/WorkflowEngine.App.Tests/Fixtures/AppTestHelpers.cs
  • src/Runtime/workflow-engine-app/tests/WorkflowEngine.App.Tests/Integration/AppCommandIntegrationTests.HttpChatter.cs
  • src/Runtime/workflow-engine-app/tests/WorkflowEngine.App.Tests/Integration/AppCommandIntegrationTests.cs
  • src/Runtime/workflow-engine/.k6/README.md
  • src/Runtime/workflow-engine/.k6/constant-rate.js
  • src/Runtime/workflow-engine/.k6/lib/helpers.js
  • src/Runtime/workflow-engine/.k6/payloads/webhook.json
  • src/Runtime/workflow-engine/.k6/stress-test.js
  • src/Runtime/workflow-engine/AGENTS.md
  • src/Runtime/workflow-engine/Makefile
  • src/Runtime/workflow-engine/README.md
  • src/Runtime/workflow-engine/docs/presentation-technical.md
  • src/Runtime/workflow-engine/docs/technical-guide.md
  • src/Runtime/workflow-engine/src/WorkflowEngine.Commands/Extensions/OutboundHeaderExtensions.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Commands/Webhook/WebhookCommand.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/Endpoints/DashboardEndpoints.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/Endpoints/EngineEndpoints.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/Engine.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/Extensions/WorkflowEngineBuilderExtensions.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/Metadata/InboundMetadata.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/Metadata/MetadataExtractor.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/Metadata/WorkflowMetadataOperationTransformer.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/WorkflowWriteBuffer.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/index.html
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/core/state.js
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/filters.js
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/modal.js
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/query.js
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/state-modal.js
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/features/url.js
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/shared/cards.js
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/shared/dropdown.js
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/wwwroot/modules/shared/pipeline.js
  • src/Runtime/workflow-engine/src/WorkflowEngine.Data/Constants/WorkflowNamespace.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Data/Extensions/WorkflowRequestExtensions.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Data/Repository/EngineRepository.Reads.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Data/Repository/EngineRepository.Writes.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Data/Repository/IEngineRepository.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Models/PaginatedResponse.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Models/StepStatusResponse.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Models/WorkflowEnqueueRequest.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Models/WorkflowMetadataConstants.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Models/WorkflowRequestMetadata.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Models/WorkflowStatusResponse.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Core.Tests/Endpoints/EngineEndpointTests.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Core.Tests/WorkflowWriteBufferTests.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/.snapshots/EngineTests.Response_GetWorkflow_AfterRetries_ShowsRetryState.verified.txt
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/.snapshots/EngineTests.Response_GetWorkflow_CompletedWebhook_ReturnsFullDetailsShape.verified.txt
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/.snapshots/EngineTests.Response_GetWorkflow_MultipleSteps_ReturnsAllSteps.verified.txt
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/.snapshots/EngineTests.Response_GetWorkflow_WithDependencies_ShowsDependencyStatus.verified.txt
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/.snapshots/EngineTests.Response_GetWorkflow_WithStepLabels_ReturnsLabelsInShape.verified.txt
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/.snapshots/EngineTests.Response_ListActiveWorkflows_WhileProcessing_ReturnsWorkflowsShape.verified.txt
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/.snapshots/EngineTests.WebhookCommand_FullHttpChatter_DocumentsExchange.http
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/DashboardEndpointTests.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/DashboardRetryTests.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/EngineCancellationTests.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/EngineGracefulShutdownTests.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/EngineResumeTests.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/EngineTests.HttpChatter.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/EngineTests.Responses.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/EngineTests.Validation.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/TelemetryTests.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Repository.Tests/Fixtures/WorkflowTestHelper.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Repository.Tests/QueryPlanTests.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Repository.Tests/WorkflowCrudTests.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Repository.Tests/WorkflowQueryTests.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.TestKit/EngineApiClient.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.TestKit/EngineAppFixture.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.TestKit/HttpChatterHelpers.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.TestKit/HttpExchangeRecorder.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.TestKit/TestHelpers.cs
💤 Files with no reviewable changes (3)
  • src/Runtime/workflow-engine/.k6/payloads/webhook.json
  • src/Runtime/workflow-engine-app/.k6/payloads/process-next.json
  • src/Runtime/workflow-engine/src/WorkflowEngine.Models/WorkflowEnqueueRequest.cs

Comment thread src/Runtime/workflow-engine/docs/presentation-technical.md Outdated
Comment thread src/Runtime/workflow-engine/README.md Outdated
Comment thread src/Runtime/workflow-engine/src/WorkflowEngine.Models/PaginatedResponse.cs Outdated
Comment thread src/Runtime/workflow-engine/tests/WorkflowEngine.TestKit/EngineApiClient.cs Outdated
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: 1

🧹 Nitpick comments (1)
src/Runtime/workflow-engine/tests/WorkflowEngine.Core.Tests/Endpoints/EngineEndpointTests.cs (1)

21-36: Use non-default pagination values in these tests.

These assertions still pass if the handler keeps hardcoded 25/100, because _defaultSettings inherits the model defaults. Please override Pagination with non-default values in the test options and assert against those, so the configurable behaviour added in this PR is actually exercised.

Also applies to: 333-352, 427-507

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Runtime/workflow-engine/tests/WorkflowEngine.Core.Tests/Endpoints/EngineEndpointTests.cs`
around lines 21 - 36, The test options created in _defaultSettings use
EngineSettings but don't override the Pagination property, so tests can pass
with hardcoded defaults; update the EngineSettings instance used in
_defaultSettings to set a non-default Pagination value (e.g., specify Pagination
= new PaginationSettings { PageSize = X, PageLimit = Y } or whatever the project
uses) and then update assertions in the tests that reference pagination behavior
to assert against these non-default values; modify all similar test option
blocks mentioned (lines around 333-352 and 427-507) to set explicit, non-default
Pagination on the EngineSettings so the configurable pagination behavior is
actually exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/Runtime/workflow-engine/src/WorkflowEngine.Core/Endpoints/EngineEndpoints.cs`:
- Around line 142-149: The effectivePage value can still be so large that
(page-1)*pageSize overflows in repository.GetActiveWorkflowsPaginated; clamp
effectivePage to a safe maximum before the call (e.g., compute safeMaxPage =
Math.Max(1, int.MaxValue / effectivePageSize) and set effectivePage =
Math.Min(effectivePage, safeMaxPage)) or add the same validation inside
GetActiveWorkflowsPaginated so that the Skip offset cannot overflow; update the
code paths that call effectivePage (the variables effectivePage,
effectivePageSize and the call to repository.GetActiveWorkflowsPaginated)
accordingly.

---

Nitpick comments:
In
`@src/Runtime/workflow-engine/tests/WorkflowEngine.Core.Tests/Endpoints/EngineEndpointTests.cs`:
- Around line 21-36: The test options created in _defaultSettings use
EngineSettings but don't override the Pagination property, so tests can pass
with hardcoded defaults; update the EngineSettings instance used in
_defaultSettings to set a non-default Pagination value (e.g., specify Pagination
= new PaginationSettings { PageSize = X, PageLimit = Y } or whatever the project
uses) and then update assertions in the tests that reference pagination behavior
to assert against these non-default values; modify all similar test option
blocks mentioned (lines around 333-352 and 427-507) to set explicit, non-default
Pagination on the EngineSettings so the configurable pagination behavior is
actually exercised.
🪄 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: 4780488c-a305-45e0-be04-51ae9b99ed3c

📥 Commits

Reviewing files that changed from the base of the PR and between 94033fb and 80d33a2.

📒 Files selected for processing (3)
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/Endpoints/EngineEndpoints.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Models/EngineSettings.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Core.Tests/Endpoints/EngineEndpointTests.cs

Comment thread src/Runtime/workflow-engine/src/WorkflowEngine.Core/Endpoints/EngineEndpoints.cs Outdated
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.

🧹 Nitpick comments (2)
src/Runtime/workflow-engine/src/WorkflowEngine.Data/Repository/EngineRepository.Reads.cs (2)

567-570: Consider defensive validation for pagination parameters.

If page < 1 is passed, Skip((page - 1) * pageSize) produces a negative offset, which throws ArgumentOutOfRangeException in EF Core. While the endpoint likely validates these parameters, adding a guard clause here would make the repository more defensive.

💡 Suggested defensive guard
 try
 {
     logger.FetchingWorkflows("active (paginated)");
+
+    if (page < 1)
+        throw new ArgumentOutOfRangeException(nameof(page), "Page must be >= 1");
+    if (pageSize < 1)
+        throw new ArgumentOutOfRangeException(nameof(pageSize), "PageSize must be >= 1");

     await using var context = await dbContextFactory.CreateDbContextAsync(cancellationToken);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Runtime/workflow-engine/src/WorkflowEngine.Data/Repository/EngineRepository.Reads.cs`
around lines 567 - 570, The repository method using baseQuery with
OrderBy(...).Skip((page - 1) * pageSize).Take(pageSize) should defensively
validate pagination inputs: ensure page is at least 1 and pageSize is positive
before computing Skip/Take (e.g., clamp page to 1 or throw an
ArgumentException), so update the method around the baseQuery/Skip/Take usage
(referencing baseQuery, page, pageSize, OrderBy, Skip, Take, and the workflows
variable) to validate or normalize those parameters and avoid passing a negative
skip value into EF Core.

149-176: Missing success logging for consistency with other methods.

Other read methods in this file (e.g., GetScheduledWorkflows, GetDistinctLabelValues) log success via logger.SuccessfullyFetchedWorkflows(...) after completing the query. This method lacks that logging, which creates an observability gap.

💡 Suggested fix
         await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
-        return await dbContext
+        var result = await dbContext
             .Workflows.Select(w => w.Namespace)
             .Distinct()
             .OrderBy(n => n)
             .ToListAsync(cancellationToken);
+
+        logger.SuccessfullyFetchedWorkflows(result.Count);
+
+        return result;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Runtime/workflow-engine/src/WorkflowEngine.Data/Repository/EngineRepository.Reads.cs`
around lines 149 - 176, GetDistinctNamespaces currently returns the distinct
namespace list but does not emit the success metric/log like other reads; after
the ToListAsync call and before returning, call
logger.SuccessfullyFetchedWorkflows with an appropriate argument (e.g.,
"namespaces" or the resulting list count) and ensure activity remains consistent
(use activity?.SetTag or leave as-is), so add the
logger.SuccessfullyFetchedWorkflows(...) invocation in the GetDistinctNamespaces
method right after obtaining the result and before the return, mirroring how
GetScheduledWorkflows/GetDistinctLabelValues do it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@src/Runtime/workflow-engine/src/WorkflowEngine.Data/Repository/EngineRepository.Reads.cs`:
- Around line 567-570: The repository method using baseQuery with
OrderBy(...).Skip((page - 1) * pageSize).Take(pageSize) should defensively
validate pagination inputs: ensure page is at least 1 and pageSize is positive
before computing Skip/Take (e.g., clamp page to 1 or throw an
ArgumentException), so update the method around the baseQuery/Skip/Take usage
(referencing baseQuery, page, pageSize, OrderBy, Skip, Take, and the workflows
variable) to validate or normalize those parameters and avoid passing a negative
skip value into EF Core.
- Around line 149-176: GetDistinctNamespaces currently returns the distinct
namespace list but does not emit the success metric/log like other reads; after
the ToListAsync call and before returning, call
logger.SuccessfullyFetchedWorkflows with an appropriate argument (e.g.,
"namespaces" or the resulting list count) and ensure activity remains consistent
(use activity?.SetTag or leave as-is), so add the
logger.SuccessfullyFetchedWorkflows(...) invocation in the GetDistinctNamespaces
method right after obtaining the result and before the return, mirroring how
GetScheduledWorkflows/GetDistinctLabelValues do it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dbf5613c-e6dd-4868-97c8-9aa4dea7441c

📥 Commits

Reviewing files that changed from the base of the PR and between 80d33a2 and 4a1dfb2.

📒 Files selected for processing (1)
  • src/Runtime/workflow-engine/src/WorkflowEngine.Data/Repository/EngineRepository.Reads.cs

danielskovli and others added 4 commits March 31, 2026 12:57
The GET /api/v1/{namespace}/workflows endpoint now supports offset-based
pagination via ?page and ?pageSize query params, returning a PaginatedResponse
envelope with data, page, pageSize, totalCount, and totalPages. This prevents
unbounded responses during high-volume scenarios (e.g. k6 stress tests with
10k workflows) and gives frontends the metadata needed for page controls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…neSettings

Adds PaginationSettings (DefaultPageSize, MaxPageSize) to EngineSettings
so hosts can tune pagination limits through configuration rather than
relying on hardcoded values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… on empty count

EF Core strips Include() for aggregate queries, so the count is already
lightweight. Add early return when totalCount is 0 to skip the data query
entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r, add pagination repo tests

- ListActiveWorkflows now iterates through all pages instead of returning
  only the first page, matching the "full list" contract callers expect
- Add 5 repository-level pagination tests verifying total count, complete
  non-overlapping iteration, empty results, out-of-range pages, and
  terminal workflow exclusion
- Update docs (README, AGENTS, presentation) to reflect paginated endpoint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@danielskovli danielskovli force-pushed the feature/workflow-engine-pagination branch from 3209fbe to 3f07aee Compare March 31, 2026 10:57
@danielskovli
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

🧹 Nitpick comments (1)
src/Runtime/workflow-engine/tests/WorkflowEngine.Core.Tests/Endpoints/EngineEndpointTests.cs (1)

21-36: Consider explicitly setting Pagination in test settings for clarity.

The _defaultSettings fixture doesn't explicitly set the Pagination property, yet tests assert specific values like PageSize == 25 and MaxPageSize == 100. While this works if EngineSettings.Pagination has default initialisers, explicitly setting the pagination configuration would make the test assumptions clearer and more resilient to future changes in default values.

💡 Suggested improvement
 private static readonly IOptions<EngineSettings> _defaultSettings = Options.Create(
     new EngineSettings
     {
         MaxWorkflowsPerRequest = 100,
         MaxStepsPerWorkflow = 50,
         MaxLabels = 10,
         MetricsCollectionInterval = TimeSpan.FromSeconds(10),
         DefaultStepCommandTimeout = TimeSpan.FromSeconds(30),
         DefaultStepRetryStrategy = new() { MaxDelay = TimeSpan.FromMinutes(5) },
         DatabaseCommandTimeout = TimeSpan.FromSeconds(30),
         DatabaseRetryStrategy = new() { MaxDelay = TimeSpan.FromMinutes(1) },
         HeartbeatInterval = TimeSpan.FromSeconds(10),
         StaleWorkflowThreshold = TimeSpan.FromMinutes(1),
         MaxReclaimCount = 3,
+        Pagination = new() { DefaultPageSize = 25, MaxPageSize = 100 },
     }
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Runtime/workflow-engine/tests/WorkflowEngine.Core.Tests/Endpoints/EngineEndpointTests.cs`
around lines 21 - 36, The test fixture _defaultSettings should explicitly set
the Pagination property on the EngineSettings instance so assertions about
PageSize and MaxPageSize are clear and robust; update the new EngineSettings
initializer to include Pagination = new PaginationSettings { PageSize = 25,
MaxPageSize = 100 } (or whatever values the tests assert) so the tests no longer
rely on EngineSettings.Pagination defaults when referencing PageSize and
MaxPageSize.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@src/Runtime/workflow-engine/tests/WorkflowEngine.Core.Tests/Endpoints/EngineEndpointTests.cs`:
- Around line 21-36: The test fixture _defaultSettings should explicitly set the
Pagination property on the EngineSettings instance so assertions about PageSize
and MaxPageSize are clear and robust; update the new EngineSettings initializer
to include Pagination = new PaginationSettings { PageSize = 25, MaxPageSize =
100 } (or whatever values the tests assert) so the tests no longer rely on
EngineSettings.Pagination defaults when referencing PageSize and MaxPageSize.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4ab523a5-bc3d-44fb-903e-59232352b640

📥 Commits

Reviewing files that changed from the base of the PR and between 4a1dfb2 and 3f07aee.

📒 Files selected for processing (14)
  • src/Runtime/workflow-engine/.k6/lib/helpers.js
  • src/Runtime/workflow-engine/AGENTS.md
  • src/Runtime/workflow-engine/README.md
  • src/Runtime/workflow-engine/docs/presentation-technical.md
  • src/Runtime/workflow-engine/src/WorkflowEngine.Core/Endpoints/EngineEndpoints.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Data/Repository/EngineRepository.Reads.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Data/Repository/IEngineRepository.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Models/EngineSettings.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Models/PaginatedResponse.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Core.Tests/Endpoints/EngineEndpointTests.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/.snapshots/EngineTests.Response_ListActiveWorkflows_WhileProcessing_ReturnsWorkflowsShape.verified.txt
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/TelemetryTests.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Repository.Tests/WorkflowQueryTests.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.TestKit/EngineApiClient.cs
✅ Files skipped from review due to trivial changes (5)
  • src/Runtime/workflow-engine/README.md
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/TelemetryTests.cs
  • src/Runtime/workflow-engine/docs/presentation-technical.md
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Integration.Tests/.snapshots/EngineTests.Response_ListActiveWorkflows_WhileProcessing_ReturnsWorkflowsShape.verified.txt
  • src/Runtime/workflow-engine/src/WorkflowEngine.Models/PaginatedResponse.cs
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/Runtime/workflow-engine/.k6/lib/helpers.js
  • src/Runtime/workflow-engine/AGENTS.md
  • src/Runtime/workflow-engine/src/WorkflowEngine.Models/EngineSettings.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.Repository.Tests/WorkflowQueryTests.cs
  • src/Runtime/workflow-engine/tests/WorkflowEngine.TestKit/EngineApiClient.cs
  • src/Runtime/workflow-engine/src/WorkflowEngine.Data/Repository/IEngineRepository.cs

danielskovli and others added 6 commits April 9, 2026 10:03
Piggyback heartbeats onto status writes to eliminate deadlocks between
HeartbeatService and WorkflowUpdateBuffer. Simplify update buffer to
single-flush with batch deduplication and fire-and-forget step-started
updates. Make EF Core/Npgsql instrumentation opt-in.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@danielskovli danielskovli changed the title feat(workflow-engine): paginate list active workflows endpoint feat(workflow-engine): Adds pagination and optimizes db hot path(s) Apr 9, 2026
danielskovli and others added 4 commits April 9, 2026 16:22
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SubmitAndForget now logs + emits a metric when TryWrite fails due to a full channel
- Clarify dedup safety invariant in WorkflowUpdateBuffer comment
- Fix two stale ExecuteAsync references in technical-guide.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@danielskovli danielskovli merged commit 396ec8b into main Apr 10, 2026
8 checks passed
@danielskovli danielskovli deleted the feature/workflow-engine-pagination branch April 10, 2026 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-releasenotes Issues that do not make sense to list in our release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants