Skip to content

[NIDX-02C] Cut the Property database over to the native writer #14075

Closed
apache/skywalking-banyandb
#1383

Description

@hanahmily

Parent: #14007
Prerequisites: #14074 (NIDX-02B) and #14073 (NIDX-02A) are merged. The policy decision is recorded below; the implementation PR must restate it before its first commit.

Current status — READY SINGLE EXECUTION LEAF

The encoder, lifecycle adapter, neutral bridge, ownership architecture, and rollback contract are accepted. This issue is one executable Property cutover leaf; no child issues or decomposition are required. The implementation PR must repeat the accepted neutral-bridge decision and exact scope before its first commit; that is a procedural review requirement, not an unresolved policy gate.

The single primary regression below exercises the reversible cutover through the real OpenDB → newShard → store path. Supplementary assertions cover the independent safety invariants without splitting the execution leaf. Existing zero-value/non-Property inverted.StoreOpts callers remain legacy.

This is the leaf that puts native bytes under a live Property writer. It is deliberately last and deliberately small: by the time it starts, the encoder is proven against two independent readers (#14073) and the plugin contract is proven against the lifecycle manager (#14074), so the diff here is registration plus selection plus rollback.

Boundary

  1. Register the native plugin in inverted.NewStore's configuration.
  2. Resolve one Property-database-wide implementation selection at startup, before scanning the directory or opening any shard store, and have banyand/property/db/shard.go:newShard use that selection for every existing and newly created shard. Today that line unconditionally calls inverted.NewStore.
  3. Establish exclusive writer ownership before either implementation opens a shard writer; a process that cannot establish ownership fails rather than proceeding.
  4. Implement rollback: stop admission, drain durable callbacks, close every active shard writer, change the selection, reopen the same files with the retained legacy constructor.

Concrete activation and readiness anchors

  • Both production entrypoints, banyand/property/service.go and banyand/metadata/schema/schemaserver/service.go, set the same explicit Property-wide writer selection in db.IndexConfig. They must select the native implementation for this cutover; a zero-value/non-Property inverted.StoreOpts retains the legacy implementation, while Property passes its resolved mode to the neutral native/legacy constructor path so this leaf does not switch unrelated callers globally.
  • OpenDB acquires the database <Location>/lock before load scans directories or newShard opens any writer. Ownership failure returns before a shard opens, and every later startup error releases the acquired lock.
  • OpenDB resolves the selection once into the database state; newShard passes that resolved value to inverted.NewStore for every existing and newly created shard. No shard, segment, snapshot, CRC32 value, or writer-origin heuristic may alter it.
  • Native mode uses the existing lifecycle configuration's DirectoryFunc to return a package-local wrapper around indexAPI.NewFileSystemDirectory(path): it delegates Setup/List/Load/Persist/Remove/Stats/Sync and makes only Lock/Unlock no-ops. The wrapper is private and reachable only from a Property-owned native factory after the database lease is held; no unrestricted lockless native constructor is exported, and an unowned direct call fails closed. Legacy mode retains the default directory. The Property database lock is the sole native ownership mechanism; tests must assert that native startup creates no index-local marker and that rollback leaves no native marker.
  • The root database lease remains held through rollback admission stop, callback drain, every shard close, and legacy reopen. Any partial reopen or ownership error fails closed and admits no writer; native close never trusts or deletes a stale marker created by a legacy writer.
  • Rollback is a controlled lifecycle transition: stop new admission, drain durable callbacks, close every active shard, then reopen the same Location with the retained legacy selection. Tests must exercise legacy-created files and preserve repeated values, deletions, and explicit order, while proving native and legacy writers are never simultaneous.

Constraints carried from the workstream

  • Selection is Property-database-wide, never inferred per shard, and never inferred from ICE segments, snapshot manifests, CRC32 values, or other writer-origin heuristics.
  • No index-local PID file, lock file, or writer-origin marker. Such runtime files are outside ICE/snapshot compatibility.
  • Simultaneous native and legacy writers for the Property database are prohibited.
  • Legacy-created shards open without rewrite and retain documents, repeated stored values, deletions, and explicit order.
  • The retained legacy constructor is an explicit rollback choice during the compatibility window, not a second active writer.

The dependency-policy decision — neutral package-local bridge selected

The pinned lifecycle manager accepts a concrete SegmentPlugin through Config.WithSegmentPlugin. The existing inverted.go import already supplies that lifecycle package and the adapter already supplies the required New, Load, and Merge functions. A neutral package-local bridge is feasible without reflection, unsafe code, dependency modification, or exported segment types: rename the existing package alias to a neutral name such as indexAPI, then register the plugin through that alias and keep the segment/plugin vocabulary behind inverted.

Selected decision: use the existing import path under a neutral package-local alias and register the native plugin through the existing NewStore configuration seam. The registration binds nativeSegmentPluginNew, nativeSegmentPluginLoad, and nativeSegmentPluginMerge to the ICE v3 selector. The Native constructor sets DirectoryFunc to the package-local no-lock wrapper described above; the Legacy constructor keeps the default directory. Property adds a database-scoped writer mode and neutral native/legacy constructor path; both Property production entrypoints select Native explicitly, while existing zero-value/non-Property NewStore callers remain legacy.

This bridge adds no dependency edge, import path, exported segment type, reflection, or unsafe operation. The production importer/path inventory must remain unchanged relative to main; renaming the already-present alias is vocabulary cleanup, not a new dependency occurrence. No new retired-package identifier/token may be introduced anywhere else, and test-only compatibility imports remain approved test evidence only.

The implementation PR must repeat this decision verbatim enough to identify the existing import/neutral alias, NewStore configuration seam, the three native callbacks, ICE v3, the Native DirectoryFunc wrapper and its no-op-only Lock/Unlock behavior, the Property-wide writer mode, zero-value legacy behavior, both production entrypoints, and the unchanged importer/path inventory before its first implementation commit. After cutover, the existing RED and end-to-end lifecycle acceptance still applies; real-data lifecycle validation cannot widen or bypass this policy.

The rejected alternative is a production exemption: this bridge avoids one rather than permitting a retired-package token in new production lines. A new public bridge, reflection, or dependency fork is also out of scope because the existing package-local alias seam is sufficient.

Primary executable regression

Test: TestPropertyNativeCutoverPersistsAndRollsBackWithoutRewrite in the focused Property database package (proposed acceptance test; not yet executed).

Setup and operation:

  1. Seed one legacy-created shard through the existing Property path with the stable key compatibility-probe and visible value legacy-v1; close it and record the existing immutable segment inventory for diagnostics. The test must not require old segment byte identity after legitimate merge/compaction.
  2. Open the same database with an explicit Native selection through OpenDB, whose root lease is acquired before any newShard opens. Read the key, update it to native-v2, and wait for the durable callback.
  3. Close the database, reopen Native on the same directory, and read the key.
  4. Close it, explicitly reopen the same directory with Legacy, and read the key; update it to legacy-v3, wait for durability, close, reopen Legacy, and read it again.

Primary exact observable: the visible value trace is [legacy-v1, native-v2, native-v2, legacy-v3]. Existing data is readable without bulk conversion or an opening rewrite; the root lease is never shared by concurrent writers; and the same files support the Native → Legacy restart rollback.

Focused commands: go test ./banyand/property/db; go test ./pkg/index/inverted; go test ./banyand/metadata/schema/schemaserver; and go test -race ./banyand/property/db ./pkg/index/inverted (confirm any narrower package command during implementation).

Supplementary acceptance assertions in the same leaf

  • Lock-before-scan: a held <Location>/lock causes a second OpenDB to return an ownership error before any shard writer opens or persistent bytes change; later startup failure releases the lease.
  • Both production entrypoints (banyand/property/service.go and banyand/metadata/schema/schemaserver/service.go) select Native explicitly, and existing/new shards share that resolved database-wide mode; unrelated zero-value callers remain Legacy.
  • The private Native DirectoryFunc wrapper is reachable only with a live Property ownership token, delegates Setup/List/Load/Persist/Remove/Stats/Sync unchanged, and no-ops only Lock/Unlock. Native creates no index-local marker and never trusts/deletes a stale Legacy marker.
  • Durable callbacks drain before close; the root lease remains held through rollback close/reopen; partial open or reopen failure fails closed with no writer admitted; Native and Legacy are never simultaneous.

No default activation bypass, child issue, hot-reload API, or unexecuted test result is implied; these are acceptance contracts for this single execution leaf.

Non-scope

Merge, expiry, GC, backup, repair integration, range and boolean query, and explicit sort beyond what Property's current callers already exercise. Those follow once the writer is live.

Activity

  1. added this to the BanyanDB - 0.12.0 milestone on Sep 11, 2026
  2. HesandaLiyanage commented on Sep 18, 2026

    @HesandaLiyanage

    @hanahmily Hi i would like to work on this issue. can you assign this for me

  3. hanahmily commented on Sep 18, 2026

    @hanahmily
    ContributorAuthor

    @HesandaLiyanage this task is blocked by #14074, which should be fixed firstly. I'm working on it.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions