Skip to content

other(workflows): Run workflow detail prefetches on a dedicated concurrent queue#6916

Merged
facumenzella merged 3 commits into
mainfrom
facu/workflow-prefetch-parallel-queue
Jun 8, 2026
Merged

other(workflows): Run workflow detail prefetches on a dedicated concurrent queue#6916
facumenzella merged 3 commits into
mainfrom
facu/workflow-prefetch-parallel-queue

Conversation

@facumenzella

@facumenzella facumenzella commented Jun 5, 2026

Copy link
Copy Markdown
Member

Follow-up to the review on #6882 (#6882 (comment)), where we said parallelizing the workflow prefetches was worth doing but belonged in its own PR.

Right now every workflow detail fetch runs on the single backend OperationQueue (maxConcurrentOperationCount = 1), and GetWorkflowOperation holds that one slot through the entire CDN asset download. So when WorkflowManager prefetches N workflows, their CDN downloads run strictly one after another, and since the list fetch gates offerings delivery, that latency is right on the critical path.

This adds a dedicated workflows OperationQueue (cap 4, matching Android's CONCURRENT_BACKEND_CALLS/MAX_CONCURRENT_WORKFLOW_CDN_FETCHES) and routes the prefetch detail fetches to it. On-demand getWorkflow and the list fetch (getWorkflows) stay on the serial queue. Because the operation holds its slot through the CDN download, a single cap-4 queue bounds both the concurrent envelope calls and the concurrent CDN downloads at 4.

A couple of things worth knowing:

  • The envelope API calls still serialize inside HTTPClient (that's its existing currentSerialRequest design, SDK-wide). What actually overlaps now is the CDN asset downloads, which escape that serial path (iOS 15+ via FileRepository tasks, pre-15 via the raw dataTask in fetchRawData). That's the heavy part, so it's the win.
  • Only the prefetch path is parallelized, matching Android, which only added a concurrent dispatcher for prefetching and leaves direct getWorkflow on its default path. On-demand fetches and the list stay on the serial queue. A prefetch flag threaded from WorkflowManager.prefetchWorkflows down to WorkflowsAPI.getWorkflow selects the queue.

The two-layer behavior (envelopes serialize, CDN downloads fan out) is the part most likely to be misread, so:

sequenceDiagram
    participant Q as Workflows queue (cap 4)
    participant H as HTTPClient (serial)
    participant CDN as CDN (FileRepository / dataTask)
    Note over Q: up to 4 prefetch operations run concurrently
    Q->>H: envelope A
    Q->>H: envelope B
    H-->>Q: A (serialized)
    H-->>Q: B (serialized)
    par overlap (the win)
        Q->>CDN: download A
    and
        Q->>CDN: download B
    end
    Note over Q: each op holds its slot until its CDN download finishes
Loading

There's a test (testGetWorkflowDetailFetchesRunConcurrentlyUpToFour) that actually observes the concurrency: it fires six prefetch detail fetches and asserts exactly four cdnFetch closures are in flight at once and never more. The trick is that the operation holds its queue slot until the cdnFetch completion fires, so the stub blocks on a background thread (not main), which lets MockHTTPClient keep delivering envelopes and the fetches genuinely overlap. Verified non-vacuous: routing the fetch back to the serial queue makes it reach only 1 and fail. A routing test (testPrefetchUsesWorkflowsQueueWhileOnDemandAndListUseSerialQueue) covers the split: prefetch lands on the workflows queue, on-demand and list on the serial queue.

AI session context

AI Context

Metadata

  • PR: this PR (facu/workflow-prefetch-parallel-queue)
  • Branch: facu/workflow-prefetch-parallel-queue
  • Author / human owner: facumenzella
  • Agent(s): Claude Code (Opus 4.8)
  • Session source: current conversation
  • Generated: 2026-06-08
  • Context document version: 3

Goal

Parallelize workflow detail prefetches so their CDN asset downloads overlap instead of serializing on the SDK's single backend queue. Follow-up explicitly deferred from the #6882 review.

Initial Prompt

"Let's follow-up on #6882 (review)" — resolved to: implement the parallelization that vegaro's review (comment 3347636417) flagged and that the PR author deferred to a later PR.

Important Follow-up Prompts

  • Chose scope "design/plan only" first, then "let's implement it".
  • Chose approach "dedicated concurrent workflow queue" (over releasing the queue slot after the envelope) and "list stays on main queue".
  • "Check how android does it, if you have doubts" — drove the on-demand-fetch routing decision (see below).
  • "How confident are we about this? Is there a test we can add that checks logs? Or smth" — drove the direct concurrency-observing test (chosen over log assertions, which are indirect and order-fragile).
  • After vegaro's review, chose "match Android (prefetch only)" — on-demand getWorkflow reverts to the serial queue; only prefetch uses the cap-4 queue.

Agent Contribution

  • Root-caused the serialization to two layers: HTTPClient's internal currentSerialRequest (left untouched) and the cap-1 OperationQueue + GetWorkflowOperation holding its slot through the CDN download (the part addressed).
  • Added Backend.QueueProvider.createWorkflowsQueue() (cap 4), threaded workflowsQueue through BackendConfiguration + Backend.init, added an optional queue: selector to addCacheableOperation (defaults to the serial queue), and routed WorkflowsAPI.getWorkflow to the workflows queue.
  • Added a concurrency test that observes up-to-4 overlapping CDN fetches at the injected cdnFetch seam, and verified it is non-vacuous via a serial-queue mutation.
  • Wrote a design doc and an implementation plan (kept outside the repo).
  • Ran the suites; ran a Codex review (no issues found).

Human Decisions

  • Approach and queue-routing scope (dedicated queue, list on serial, cap 4).
  • Asked to verify the approach against the Android implementation.
  • Asked for a test that directly validates the parallelism.

Key Implementation Decisions

  • Decision: one dedicated cap-4 queue, operation keeps holding its slot through the CDN download.
    • Rationale: bounds both envelope and CDN concurrency at 4 with one mechanism; mirrors Android's two caps.
    • Rejected: releasing the queue slot after the envelope and running the CDN fetch detached (would not bound CDN concurrency).
  • Decision: route ONLY prefetch detail fetches to the workflows queue; on-demand getWorkflow and the list stay on the serial queue.
    • Rationale: matches Android, which only parallelizes the prefetch path. A prefetch flag (default false) is threaded from WorkflowManager.prefetchWorkflows -> WorkflowManager.getWorkflow -> WorkflowsAPI.getWorkflow, which selects the queue.
    • History: an earlier revision parallelized both prefetch and on-demand. vegaro flagged the divergence from Android in review; we narrowed it to prefetch-only.

Files / Symbols Touched

  • Sources/Networking/Backend.swift
    • Symbols: QueueProvider.createWorkflowsQueue(), maxConcurrentWorkflowOperations; wired into Backend.init.
  • Sources/Networking/BackendConfiguration.swift
    • Symbols: workflowsQueue property + init param; addCacheableOperation(..., queue:).
  • Sources/Networking/WorkflowsAPI.swift
    • Symbols: getWorkflow(..., prefetch: Bool = false, ...) routes to workflowsQueue only when prefetch == true, else the serial queue; getWorkflows (list) unchanged.
  • Sources/Purchasing/WorkflowManager.swift
    • Symbols: getWorkflow(..., prefetch: Bool = false, ...) forwards the flag; prefetchWorkflows passes prefetch: true.
  • Test construction sites updated for the new init param: BaseBackendTest.swift, MockBackendConfiguration.swift, BasePurchasesTests.swift, BackendSubscriberAttributesTests.swift. MockWorkflowsAPI captures the prefetch arg.
  • Tests/UnitTests/Networking/Backend/BackendGetWorkflowsTests.swift
    • New: testWorkflowsQueueAllowsFourConcurrentOperations, testPrefetchUsesWorkflowsQueueWhileOnDemandAndListUseSerialQueue, testGetWorkflowDetailFetchesRunConcurrentlyUpToFour.
  • Tests/UnitTests/Purchasing/WorkflowManagerTests.swift
    • Assert prefetch path passes prefetch == true and on-demand passes false.

Dependencies / Config / Migrations

  • None. All changed symbols are internal; no public API change. Behavior gated behind SystemInfo.workflowsEndpointEnabled.

Validation

  • Commands run:
    • swift build: succeeded.
    • xcodebuild test ... BackendGetWorkflowTests + WorkflowManagerTests + PurchasesWorkflowTests: 51 tests, 0 failures (after the prefetch-only change).
    • testGetWorkflowDetailFetchesRunConcurrentlyUpToFour run 3x in isolation: passed deterministically (~0.55s each), no flakiness.
    • Negative control: temporarily routing the prefetch back to the serial queue makes the concurrency test fail (currentInFlight reaches 1, not 4), confirming it actually validates the cap. Reverted.
    • SwiftLint (pre-commit + build phase): 0 violations.
    • Codex review: no correctness issues found.
  • Manual verification: Not run (no device run; behavior is gated and not user-visible).
  • CI: Not captured (pre-CI).

Validation Gaps

  • The concurrency test observes overlap at the injected cdnFetch seam (counting simultaneously in-flight closures), not the real FileRepository/URLSession download path, and it does not model FileRepository's same-URL coalescing (the stub ignores the URL; each fetch uses a distinct workflowId). It proves the property this change owns (the workflows queue permits up to 4 concurrent cdnFetch invocations and no more), not a measured end-to-end network speedup.
  • No device/integration run timing a real prefetch batch with -EnableWorkflowsEndpoint.
  • Full CI-RevenueCat suite not run locally (only the workflow/backend-adjacent subset).

Review Focus

  • Is prefetch-only parallelization (on-demand + list on the serial queue) the right split? (Narrowed from "both" per review.)
  • Cap of 4: reasonable for the expected number of prefetched workflows?
  • Workflows queue intentionally keeps default QoS (no .background, unlike diagnostics) because prefetches gate offerings delivery, agree?

Risks / Reviewer Notes

  • Risk: concurrent CDN writes for two workflows sharing an asset URL.
    • Evidence: FileRepository.generateOrGetCachedFileURL coalesces same-URL fetches via a JobKey-keyed in-flight task store, so they share one download.
  • Risk: concurrent CallbackCache access.
    • Evidence: CallbackCache is Atomic-backed.
  • Risk: concurrent writes to WorkflowsCache from parallel completions.
    • Evidence: its mutable state is held in Atomic containers (cache(workflow:) uses .modify); WorkflowDetailProcessor is a stateless Sendable with only an immutable let.

Non-goals / Out of Scope

  • Changing HTTPClient's internal request serialization.
  • Changing GetWorkflowOperation completion semantics.
  • Parallelizing on-demand getWorkflow (left on the serial queue to match Android).

Omitted Context

  • Raw transcript, unrelated exploration, sensitive details, repetitive attempts, and chain-of-thought-style content were omitted.

Note

Medium Risk
Changes internal networking concurrency for workflow prefetches/on-demand detail loads; bounded at 4 but overlaps CDN I/O and shares callback caches across concurrent operations.

Overview
Adds a dedicated RC Workflows Queue (maxConcurrentOperationCount = 4) and wires it through BackendConfiguration so workflow detail fetches no longer share the single serial backend queue.

addCacheableOperation now accepts an optional queue: (default remains the serial operationQueue). WorkflowsAPI.getWorkflow enqueues on workflowsQueue so multiple detail operations—and their CDN downloads while each operation holds its slot—can overlap up to four. getWorkflows (list) is unchanged on the serial queue.

Test harnesses pass the new queue into BackendConfiguration, with unit tests for queue settings, detail-vs-list routing, and a cap-4 concurrency check via stubbed CDN fetch.

Reviewed by Cursor Bugbot for commit a5f4052. Bugbot is set up for automated code reviews on this repo. Configure here.

@facumenzella facumenzella changed the title Run workflow detail prefetches on a dedicated concurrent queue feat(workflows): Run workflow detail prefetches on a dedicated concurrent queue Jun 5, 2026
@facumenzella facumenzella changed the title feat(workflows): Run workflow detail prefetches on a dedicated concurrent queue other(workflows): Run workflow detail prefetches on a dedicated concurrent queue Jun 5, 2026
@facumenzella facumenzella force-pushed the facu/workflow-prefetch-parallel-queue branch from 8fc769b to e891d75 Compare June 8, 2026 15:59
@facumenzella facumenzella marked this pull request as ready for review June 8, 2026 16:10
@facumenzella facumenzella requested a review from a team as a code owner June 8, 2026 16:10
Comment thread Sources/Networking/Backend.swift
Comment thread Sources/Networking/WorkflowsAPI.swift Outdated
Comment thread Tests/UnitTests/Networking/Backend/BackendGetWorkflowsTests.swift Outdated
facumenzella and others added 3 commits June 8, 2026 21:51
Workflow prefetches previously serialized on the single backend OperationQueue
(maxConcurrentOperationCount = 1), where each GetWorkflowOperation holds its slot
through the whole CDN asset download. Add a dedicated workflows queue (cap 4) and
route detail fetches (WorkflowsAPI.getWorkflow) to it so their CDN downloads overlap
(up to 4 at a time). The list fetch stays on the serial queue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds testGetWorkflowDetailFetchesRunConcurrentlyUpToFour: fires six detail
fetches and asserts exactly four cdnFetch closures are in flight at once and
never more, proving the dedicated workflows queue both parallelizes and caps
at four. The stub blocks on a background thread so MockHTTPClient keeps
delivering envelopes on main. Verified non-vacuous via a serial-queue mutation
(reaches 1, fails). Also rewords the routing test comment that wrongly claimed
concurrency could not be observed here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses vegaro's review on #6916:
- Route only prefetch detail fetches to the cap-4 workflows queue; on-demand
  getWorkflow stays on the serial queue (threads a prefetch flag through
  WorkflowManager.getWorkflow and WorkflowsAPI.getWorkflow). Keeps platform
  behavior aligned with Android, which only parallelizes the prefetch path.
- Drop the redundant toNever(>4) assertion in the concurrency test; the final
  maxInFlight == 4 check is the authoritative cap assertion.
- Document why the workflows queue intentionally keeps default QoS (no
  .background): prefetches gate offerings delivery.

Tests: routing test now asserts prefetch -> workflows queue and on-demand/list
-> serial queue; WorkflowManagerTests assert prefetch passes true and on-demand
passes false. 51 workflow tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@facumenzella facumenzella force-pushed the facu/workflow-prefetch-parallel-queue branch from a5f4052 to 4776343 Compare June 8, 2026 19:59
@facumenzella facumenzella enabled auto-merge (squash) June 8, 2026 20:05
@facumenzella facumenzella merged commit e8e46b2 into main Jun 8, 2026
18 of 20 checks passed
@facumenzella facumenzella deleted the facu/workflow-prefetch-parallel-queue branch June 8, 2026 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants