8000 feat(ffi): expose OpenOptions over UniFFI — the close-time checkpoint cannot be suppressed from Python · Issue #322 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content
8000

feat(ffi): expose OpenOptions over UniFFI — the close-time checkpoint cannot be suppressed from Python #322

Closed

Description

@adityamukho

Summary

OpenOptions is not reachable from the UniFFI surface, so an embedder cannot
opt out of the checkpoint that Drop for Inner performs on every handle close.
For a workload that opens and closes handles frequently, that turns #315's
O(graph size) checkpoint() into a mandatory per-close cost with no escape
hatch — even when the embedder is already managing compaction itself.

What is reachable today

The exported UniFFI metadata on libminigraf_ffi.so (1.2.3) is exactly four
entry points:

UNIFFI_META_MINIGRAF_FFI_CONSTRUCTOR_MINIGRAFDB_OPEN
UNIFFI_META_MINIGRAF_FFI_CONSTRUCTOR_MINIGRAFDB_OPEN_IN_MEMORY
UNIFFI_META_MINIGRAF_FFI_METHOD_MINIGRAFDB_CHECKPOINT
UNIFFI_META_MINIGRAF_FFI_METHOD_MINIGRAFDB_EXECUTE

and the Python constructor signature is MiniGrafDb.open(path: str). There is
no open_with_options, so wal_checkpoint_threshold, page_cache_size,
max_derived_facts and max_results are all Rust-only.

Why it matters

Drop for Inner (src/db.rs:196) runs a best-effort do_checkpoint on close,
and src/db.rs:203 already documents the intended escape hatch:

// Skip if wal_checkpoint_threshold is usize::MAX — that sentinel suppresses
// all checkpointing (used by benchmarks to keep WAL entries pending).
if self.options.wal_checkpoint_threshold == usize::MAX {
    return;
}

The sentinel exists and works. It is simply unreachable from any UniFFI
consumer, so only Rust benchmarks can use it.

Measured cost of that close, after exactly one dirty fact (so this is the
graph-size term, not the dirty-bytes term — consistent with #315):

graph handle drop
100k facts 337.5 ms
1M facts 4241.0 ms

Downstream case: project-minigraf/temporal_reasoning runs a git-history
ingestion that takes a refcounted lease per commit and drops the handle when the
count reaches zero. Measured over 220 real commits, that is 1.02 handle drops
per commit, 19.1 s of 40.5 s of write time (47.3%), growing 3.20x within the
run
— while the embedder's own checkpoint policy accounted for 4.9%. The
embedder already schedules checkpoint() on a duty-cycle budget; it just has no
way to tell minigraf to stop doing it on close.

Proposed

Expose OpenOptions over UniFFI, e.g. MiniGrafDb.open_with_options(path, options) with a record carrying the four existing fields, open() staying as
the default-options shorthand. Same shape as #180 and #181.

The minimal version, if the full record is not wanted, is a single
suppress_close_checkpoint: bool on a new constructor — but exposing the
existing struct seems cleaner than adding a one-off flag.

Notes

Refs #315
Refs #180
Refs #181

Activity

  1. added this to the 2.0 milestone on Aug 24, 2026
  2. modified the milestones: v3.0.0, v3.1.0 on Sep 26, 2026
  3. adityamukho commented on Oct 7, 2026

    @adityamukho
    CollaboratorAuthor

    Resolved by #465 on v3 (minigraf-python#6 and the other UniFFI bindings): MiniGrafDb.open_with_options(path, OpenOptions(...)) / minigraf.open(path, wal_checkpoint_threshold=...) now exposes every OpenOptions field. Setting wal_checkpoint_threshold to its maximum (2**63-1 over the FFI, which maps to the usize::MAX sentinel) suppresses the close-time checkpoint. Ships in v3.0.0, earlier than this issue's v3.1.0 milestone.

  4. modified the milestones: v3.1.0, v3.0.0 on Oct 7, 2026
  5. adityamukho commented on Oct 7, 2026

    @adityamukho
    CollaboratorAuthor

    Reopening: my closing comment was wrong. The UniFFI shim (and Node) carry wal_checkpoint_threshold as a signed i64, so the largest value a binding can pass is 263-1. Core suppresses the close-time checkpoint only for exactly usize::MAX (264-1), so the escape hatch is still unreachable from Python/Java/Android/Swift/Node. The C binding (usize JSON) can reach it. A fix follows: the bindings map i64::MAX to usize::MAX, with a test that dropping the handle leaves the WAL in place.

  6. adityamukho commented on Oct 7, 2026

    @adityamukho
    CollaboratorAuthor

    Done, for real this time. On v3, open_with_options exposes every OpenOptions field in every binding (#465), and the never-checkpoint value now reaches core's usize::MAX sentinel:

    • Python: minigraf.open(path, wal_checkpoint_threshold=minigraf.WAL_CHECKPOINT_NEVER) (minigraf-python#7). Java/Kotlin Minigraf.WAL_CHECKPOINT_NEVER (java#10), Android Long.MAX_VALUE (android#7), Swift walCheckpointNever (swift#7), Node WAL_CHECKPOINT_NEVER (node#6), and the template (binding-template#2).
    • C passes usize JSON, so "wal_checkpoint_threshold": 18446744073709551615 already worked.
    • Each binding tests that a closed handle keeps its WAL with this value and still checkpoints on close with one less. The mapping is recorded in the binding spec (docs(spec): bindings map their largest count to the never-checkpoint sentinel (#322) #472).

    Ships in v3.0.0.

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

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      0