Skip to content

THOTH-GQL-DATALOADER-01: add request-scoped GraphQL DataLoader foundation - #802

Merged
ja573 merged 29 commits into
developfrom
feature/shared-architecture/graphql-dataloader-foundation
Aug 12, 2026
Merged

THOTH-GQL-DATALOADER-01: add request-scoped GraphQL DataLoader foundation#802
ja573 merged 29 commits into
developfrom
feature/shared-architecture/graphql-dataloader-foundation

Conversation

@ja573

@ja573 ja573 commented Aug 11, 2026

Copy link
Copy Markdown
Member

THOTH-GQL-DATALOADER-01 — request-scoped GraphQL DataLoader foundation

STATUS: READY FOR INDEPENDENT EXACT-HEAD REVIEW

Task identity

  • Programme: Shared Thoth GraphQL / Backend Architecture
  • Task: THOTH-GQL-DATALOADER-01
  • Risk: HIGH
  • Base: develop at 060052b47490d3d977db3b9d9f188c4c70760a9a
  • Branch: feature/shared-architecture/graphql-dataloader-foundation
  • Final implementation head: a1d73aaf0d65b01df2d8cbcd07daad60c2be269d
  • Specification: repository-authoritative docs/engineering/ai-delivery/tasks/THOTH-GQL-DATALOADER-01.md (APPROVED)
  • Architecture: repository-authoritative ADR-0007 (APPROVED; supersedes ADR-0006)
  • CTO implementation authorization: recorded on merged PR THOTH-GQL-DATALOADER-01: specify GraphQL DataLoader foundation #801 (2026-08-11)
  • Implementation report: docs/engineering/ai-delivery/implementation-reports/THOTH-GQL-DATALOADER-01-implementation-report.md

This PR is the one bounded implementation authorized by the CTO. It adopts
no production GraphQL field and carries no merge, deployment,
release, production-access or activation authorization. It remains DRAFT
pending independent exact-head review and separate explicit CTO merge
authorization.

What this PR delivers

Foundation (ADR-0007 B0)

  • dataloader 0.18.0 (default features off, runtime-tokio on) and direct
    tokio (rt) in thoth-api; Cargo.lock committed.
  • Request-local, non-cached loader bundle RequestLoaders owned directly
    by the real GraphQL Context (Context.loaders), created and dropped per
    request; no global/static/app-data loader; availability independent of
    mutation guard mode.
  • Explicit loader construction: max batch size 200, yield count 10
    (configured_loader), never crate defaults.
  • try_load-only load API for database loaders; total, fail-closed batch
    functions; batch-wide failure yields an error for every requested key,
    never fabricated empty success, never per-key fallback/retry SQL.
  • Synchronous Diesel behind tokio::task::spawn_blocking: Arc<PgPool> and
    owned keys enter the closure; connection acquired, used and dropped
    entirely inside it; set-based eq_any SQL; no connection across .await.
  • Cloneable, non-serde SharedBatchError projection preserving each field
    family's actual current GraphQL error convention (generic Display or
    explicit extensions.type); grep-level test proves no serde round trip.

Async test execution (spec §3.8)

  • The general GraphQL unit-test path migrated from juniper::execute_sync
    to async Juniper through one bounded central bridge (block_on_graphql);
    all 7 former execute_sync sites migrated; nested-runtime misuse fails
    explicitly with a clear message, with tests for both behaviours.

A2 retirement (spec §7) and guard decoupling (spec §8)

  • Removed: batching.rs (store), prefetch.rs, scope.rs,
    batching_fixture.rs, batching_tests.rs, the Context.batch_store
    field, Context::with_guard_mode, test_context_with_guard_mode,
    MutationGuardMode::store_available(), and all ADR-0006
    guard-as-store-switch documentation. The temporary handoff-time
    GraphqlBatchStore alias is gone.
  • Preserved unchanged: the mutation guard mechanism, its evaluation
    semantics, run_mutation_guard at the request boundary, the CLI/env mode
    resolution, and default/production mode OFF. No activation, no
    request-acceptance change.
  • Rehosted A2-independently: guard rejection/observe/off suites, query-path
    regressions, baseline-invalid matrix, @skip/@include directive
    cross-checks against Juniper's actual observed execution counts, and the
    duplicate top-level mutation execution evidence (still executes once per
    occurrence under async execution — this defect is NOT fixed by this PR
    and remains separately controlled under ADR-0007 §4.13/§7.4).

Acceptance evidence (spec §10)

  • Batch boundaries, both Tokio runtimes: 1→[1], 100→[100], 200→[200],
    201→[200, 1], 500→[200, 200, 100].
  • Scheduling: immediate and benign-yield cohorts coalesce to 1 dispatch;
    deliberately delayed cohorts fragment (documented as expected scheduler
    behaviour — why loader-first is a binding adoption rule);
    loader-behind-loader keeps the target at 1 dispatch.
  • Request isolation, sequential same-key non-caching, pending-coalescing
    (batching, not caching), and read-write-read freshness without
    invalidation.
  • Missing-key fails closed through try_load (no panic, no fabricated
    success); batch-wide failure is an error for every key from exactly one
    dispatch.
  • Real-Diesel query count: 250 parents → DataLoader dispatches [200, 50]
    exactly 2 set-based (= ANY) imprint statements captured by Diesel
    connection Instrumentation (external observation, not an internal
    counter).
  • Direct-vs-loader backend-failure equality for both current error
    conventions (full serialized GraphQL response equality; no retry/fallback
    dispatch).
  • Production SDL byte-identical:
base  bytes: 160799  sha256: 1e08b46b565ef719c404bbe6b3131e6a733df09c7abdc4538b66c2b24d2d899c
head  bytes: 160799  sha256: 1e08b46b565ef719c404bbe6b3131e6a733df09c7abdc4538b66c2b24d2d899c
byte-identical: YES (cmp exit 0; generated by `cargo check --workspace` via thoth-client/build.rs at base worktree 060052b4 and at the implementation head)

Validation gate (local, disposable Postgres 17 + Redis; exact results in the implementation report)

cargo fmt --all -- --check                                        exit 0
git diff --check                                                  exit 0
cargo check --workspace                                           exit 0
cargo clippy --workspace --all-targets --all-features -- -D warnings   exit 0
cargo test -p thoth-api --features backend                        exit 0
cargo test --workspace                                            exit 0

Operational effects

Database migration:        NONE
Data migration:            NONE
GraphQL schema migration:  NONE (SDL byte-identical)
Deployment:                NONE
Production configuration:  NONE
Guard mode:                unchanged (OFF)
Request acceptance:        unchanged
Production DataLoader consumer: NONE

Authorization boundaries — unchanged

NOT authorized by this PR: merge, deployment, release, production access,
workflow dispatch, BE-02, Thoth Metrics adoption, OBSERVE, ENFORCE,
PR #799 remediation/merge/closure, or architecture changes outside ADR-0007.
PR #799 remains outside this task and untouched.

Next gate

Independent exact-head review of a1d73aaf0d65b01df2d8cbcd07daad60c2be269d, then separate
explicit CTO merge authorization. The implementing agent does not approve or
merge its own work.

ja573 added 29 commits August 11, 2026 19:44
Context now owns RequestLoaders directly (loaders field) with the single
four-argument Context::new constructor; with_guard_mode, the batch_store
field, the temporary GraphqlBatchStore alias module and the obsolete
test_context_with_guard_mode helper are removed, together with the
RequestLoaders::new legacy-bridge constructor. The API server handler
constructs the context guard-independently; its preserved guard HTTP test
documentation is restored from base.
Two inherited tests failed for real defects in the fixtures, not in the
foundation. batch_wide_in_memory_failure asserted an impossible response
shape: children is a non-null list, so a per-key error null-propagates the
whole data object; the test now proves batch totality at the loader level
and correct propagation at the GraphQL level. event_contains_shape_metadata
used a baseline-invalid request: pinned Juniper rejects an absent non-null
variable even when the definition has a default, so the guard correctly
emitted no event; the variable is now supplied. Also strengthens assertions
that previously indexed into JSON null and passed vacuously, and renames
fixture access to the migrated Context.loaders field.
Replaces juniper::execute_sync in the central execute_graphql helper and
the six direct call sites with async juniper::execute driven through one
bounded block_on_graphql bridge. The bridge builds a current-thread Tokio
runtime per call and fails explicitly with an actionable message when
invoked from inside a running runtime; both behaviours are covered by
dedicated tests. Async execution is now the supported general GraphQL test
execution shape; no synchronous compatibility requirement remains for
future loader-backed fields.
cargo-generated resolution of the approved dataloader 0.18.x line: exactly
dataloader 0.18.0 with its single tokio dependency edge, matching the
default-features-off, runtime-tokio configuration in thoth-api/Cargo.toml.
The inherited scheduling fixture slept 1 ms on half the cohort and asserted
that dispatch fragmentation must occur; on a loaded CI runner the whole
cohort still coalesced into one dispatch and the test failed. ADR-0007 only
claims a delayed cohort MAY fragment, so the fixture now demonstrates the
property deterministically: the delayed resolvers perform unrelated awaited
work until the loader's first dispatch has demonstrably happened, then
register their keys, which can only land in a later batch. This removes the
wall-clock dependence (and the now-unneeded dev-only tokio time feature)
while still characterizing why loader-first is a binding adoption rule.
@ja573
ja573 marked this pull request as ready for review August 12, 2026 10:59
@ja573
ja573 merged commit 8dcf031 into develop Aug 12, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant