Skip to content

Add generate_jittered_key_between for concurrent inserts - #3

Merged
lukemerrett merged 1 commit into
mainfrom
feat/jittered-keys
Jul 31, 2026
Merged

Add generate_jittered_key_between for concurrent inserts#3
lukemerrett merged 1 commit into
mainfrom
feat/jittered-keys

Conversation

@lukemerrett

Copy link
Copy Markdown
Collaborator

What

New generate_jittered_key_between() — like generate_key_between(), but appends jitter_length (default 3) random digits to the generated key.

Why: generate_key_between() is deterministic, so two devices concurrently inserting between the same pair of keys generate the same key — and a later insert between two equal keys raises FIError. The random suffix makes simultaneous identical inserts diverge with probability ≈ 1 − 62⁻³ (~1 in 238k per simultaneous identical insert at the default length), at the cost of 3 extra characters per key. This is the conflict-resolution mechanism the filters order_key plan calls for.

Design decisions

  • Separate function, not a parameter (as discussed): keeps the core parity surface signature-identical to the JS reference and unconditionally deterministic, so cross-language byte-for-byte fixtures stay meaningful. Cross-referenced from generate_key_between()'s docstring.
  • Prefix guard: appending a suffix always keeps the key above the lower bound, but when the generated key is a prefix of the upper bound (e.g. 'a5' between 'a4' and 'a52'), any suffix could push it past the bound. In that corner the plain key is returned unchanged. Bounds are normalised first, since generate_key_between() accepts them in either order.
  • No trailing zero: the suffix's last character is drawn from digits[1:], since order keys may not end in the zero digit.
  • Injectable RNG: optional rng: random.Random for reproducible tests; defaults to a module-level instance. Documented as collision avoidance, not security.
  • No jittered generate_n_keys_between: its results routinely contain prefix-related neighbours (51, 511), so suffixes could reorder them — and bulk generation (e.g. backfill) is single-writer with no collision problem.

Testing

11 new tests (98 total, plus doctests): suffix shape/validity, seeded reproducibility, default RNG, jitter_length=0 equivalence, negative length rejected, the prefix corner in both bound orders, base-2 trailing-zero avoidance, and 1000-insert ordering property tests at jitter lengths 1/3/8 asserting every key stays strictly in bounds and the list stays sorted.

Release

Please review and merge — I'll hold off on releasing until you give the word. After merge: Actions → Release with bump: minor publishes 1.1.0 (this also picks up the unreleased 1.0.1 negative-n guard from #2).

🤖 Generated with Claude Code

generate_key_between is deterministic, so two writers concurrently
inserting between the same pair of keys generate the same key - and a
later insert between two equal keys raises FIError. The new function
appends jitter_length (default 3) random digits so simultaneous
identical inserts diverge with probability ~1 - 62^-3.

Design notes:
- Separate function rather than a parameter, so the core parity surface
  stays signature-identical to the JS reference and deterministic.
- The suffix is skipped when the generated key is a prefix of the
  effective upper bound (e.g. 'a5' between 'a4' and 'a52'), where
  appending anything could push the key past the bound. Bounds are
  normalised first since they may be passed in either order.
- The suffix's last character is never the zero digit (order keys may
  not end in zero).
- Optional rng (random.Random) for reproducible tests; the default is a
  module-level Random. Collision avoidance only, not security.
- No jittered generate_n_keys_between: its results contain keys that
  are prefixes of their neighbours, so suffixes could reorder them, and
  bulk generation is single-writer anyway.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lukemerrett lukemerrett self-assigned this Jul 31, 2026
@lukemerrett
lukemerrett merged commit af02659 into main Jul 31, 2026
13 checks passed

@doistbot doistbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds generate_jittered_key_between() to produce order keys with random suffixes, preventing concurrent inserts from generating identical keys.

Few things worth tightening:

  • The module-level RNG instance is shared across forked processes, so two workers can produce identical suffixes for the same first insert — reseding via os.register_at_fork (or per-process instantiation) would keep the concurrent-insert protection effective.
  • The documented collision probability slightly overstates the suffix space because the final digit excludes the zero character; for positive jitter_length, the formula should account for that excluded trailing digit rather than using 62⁻³ directly.

I also left one optional follow-up note in the details below.

Optional follow-up note (1)
  • P3 fractional_indexing.py:450: This exact seeded value is unverified. test_jittered_key_appends_suffix makes the identical call — generate_jittered_key_between(None, None, rng=random.Random(42)) — but only asserts the prefix and length, and CI runs pytest tests.py without --doctest-modules, so the doctest never executes. Adding assert key == 'a0e72' to that test would verify the documented example (and surface it immediately if the value is currently off).

Share FeedbackReview Logs

Comment thread fractional_indexing.py

# Default RNG for generate_jittered_key_between(). Jitter is collision
# avoidance, not security, so a plain seeded-from-OS Random is fine.
_jitter_rng = random.Random()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 This generator's state is copied into child processes when an application imports the module before forking. Two prefork workers handling the same insert as their first equivalent RNG use will therefore produce identical suffixes, defeating the concurrent-insert protection deterministically. Reseed this instance in after_in_child via os.register_at_fork, or create/reseed it per process.

Comment thread fractional_indexing.py
inserting between the same pair of keys generate the *same* key - and a
later insert between two equal keys raises FIError. The random suffix makes
simultaneous identical inserts diverge with probability roughly
1 - len(digits) ** -jitter_length, at the cost of `jitter_length` extra

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The collision formula overstates the suffix space because the final digit excludes digits[0]. For example, base 2 with jitter_length=3 has four possible suffixes, so its collision probability is 1/4 rather than 1/8. For positive lengths, use 1 - 1 / ((len(digits) - 1) * len(digits) ** (jitter_length - 1)) (with zero handled separately).

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.

2 participants