Skip to content

Make Electric SDK snapshot oracle retry reach deterministic #2347

Open

Description

@KyleAMathews

The Electric SDK snapshot-delivery oracle can fail because its random campaign does not generate one declared reach state. The reach guard is correctly rejecting missing coverage; the campaign needs a deterministic witness.

This blocked Test (collections) on #2346 at head e02e09c70ada77d5bbc52b51ebcdd4ee801fe028. CI tested merge commit 0d7e08d22b6d3d29a53e621ea0bd4bdcab458285 against main 1443dc92e5417394acbdc98b782caa9802848ce6. The failing test and retry grammar already exist on that main commit; #2346 changes the shared observer and has no Electric changes.

Failure

CI job

tests/electric-sdk-delivery-oracle.property.test.ts
applies installed-SDK snapshot deliveries against an independent overlapping source (random)

property: electric.sdk-snapshot-delivery
seed: -387674631

Error: electric.sdk-snapshot-delivery: generated histories reached these states fewer than 1 times,
so their laws were not enforced there: live-outage-crossed-by-throw (0)

The failure comes from snapshotReach.assertReached() after the 40-history campaign. No row or health refinement assertion failed in this recorded case.

Reproduce

Run against main at the commit above. Use the seed without a shrink path: path replay intentionally disables aggregate reach checks.

TANSTACK_DB_ORACLE_PROPERTY=electric.sdk-snapshot-delivery \
TANSTACK_DB_ORACLE_SEED=-387674631 \
pnpm --dir packages/electric-db-collection exec vitest run \
  tests/electric-sdk-delivery-oracle.property.test.ts \
  -t 'applies installed-SDK snapshot deliveries.*random' \
  --coverage.enabled=false --maxWorkers=2

An independent replay of the same fast-check input grammar confirms the missing case without invoking production:

import fc from 'fast-check'

const histories = []
fc.assert(
  fc.property(
    fc.integer({ min: 2, max: 4 }),
    fc.string({ maxLength: 10 }),
    fc.record({
      failures: fc.array(fc.nat(7), { maxLength: 6 }),
      extraLoads: fc.nat(3),
      live: fc.array(fc.constantFrom('http', 'throw'), { maxLength: 7 }),
    }),
    (_width, _name, retries) => { histories.push(retries.live) },
  ),
  { numRuns: 40, seed: -387674631 },
)
console.log({
  histories: histories.length,
  fiveOrMoreAttempts: histories.filter((live) => live.length >= 5).length,
  fifthAttemptThrows: histories.filter((live) => live[4] === 'throw').length,
})
// { histories: 40, fiveOrMoreAttempts: 6, fifthAttemptThrows: 0 }

The driver declares live-outage-crossed-by-throw when the fifth observed attempt throws. This seed supplies no plan with a throw in that position. The input generator can therefore explain the missing reach independently of the observer implementation. This test uses a direct Collection and ShapeStream; it does not instantiate the shared observer.

Relevant source:

Suggested repair and acceptance criteria

  • Add shared histories that deterministically reach each required retry boundary, including a throw on the fifth attempt. Use the existing model, SDK production driver, and comparison in both fixed and random campaigns.
  • Require fixed-example reach so a future seed cannot remove these witnesses. Check actual SDK attempts at the existing observation cut, as the current driver does.
  • Preserve the reach requirements and the 40 generated histories per campaign. Do not address this by lowering the minimum, removing a state, or repeatedly reseeding until green.
  • Show the reported seed passes after the grammar repair, and retain calibration that rejects a wrong outage/health result at the intended checkpoint.

This report establishes a sampling-dependent coverage failure. It does not establish a production behavior defect or that retrying CI repairs the oracle.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions