Skip to content

feat(refit): add immutable revision catalog service for S3 delta weight sync - #610

Open
nv-hwoo wants to merge 3 commits into
mainfrom
hwoo/mx-delta-revision-pr
Open

feat(refit): add immutable revision catalog service for S3 delta weight sync#610
nv-hwoo wants to merge 3 commits into
mainfrom
hwoo/mx-delta-revision-pr

Conversation

@nv-hwoo

@nv-hwoo nv-hwoo commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Add an immutable revision catalog service for coordinating live model-weight updates. The catalog records revision identity and lifecycle state, while weight payloads remain in external object storage. It provides the control-plane contract used by the publisher and receiver in the following PRs.

This is a parallel effort along the current refit API work (#604). A separate follow-up PRs will merge this into the standard refit API.

Changes

  • Add the RevisionCatalogService gRPC API:
    • PublishRevision
    • GetRevision
    • CommitRevision
  • Define immutable revision manifests containing:
    • model and version identity
    • exact base version and digest
    • target and format digests
    • versioned S3 payload identity
  • Add the READY -> COMMITTED revision lifecycle.
  • Add a Redis-backed catalog with atomic publish and commit behavior.
  • Register the catalog with the ModelExpress server and health service.
  • Add Rust and Python representations with exact protobuf conversion.
  • Add a Python gRPC catalog client for publisher and receiver integrations.
  • Preserve the existing weight-sync service while registering the revision catalog alongside it.

Payload bytes do not pass through the catalog. The service stores only immutable revision metadata and lifecycle state.

Correctness

  • Publishing the same revision identity is idempotent.
  • Reusing a model/version key with different immutable metadata is rejected.
  • Commit is allowed only for the exact published revision.
  • Revision identity preserves S3 bucket, key, checksum, and optional object version.
  • The launch anchor can omit a payload; subsequent revisions retain their exact base and target identities.

Validation

  • pre-commit run --from-ref origin/main --to-ref hwoo/mx-delta-revision-pr
  • Revision catalog, manifest, and protobuf tests: 17 passed
  • cargo fmt
  • cargo clippy
  • cargo check

Summary by CodeRabbit

  • New Features

    • Added a revision catalog for publishing, retrieving, and committing immutable model revisions.
    • Added revision manifests with optional base-version and storage payload metadata.
    • Added Python and Rust client support for revision catalog operations.
    • Added Redis-backed catalog storage with idempotent publishing, conflict detection, and commit tracking.
    • Integrated the revision catalog into server startup and authentication flows.
  • Bug Fixes

    • Added validation for incomplete, inconsistent, or invalid revision manifests and requests.
    • Improved handling of repeated operations and missing revisions.

Signed-off-by: Hyunjae Woo <hwoo@nvidia.com>
@copy-pr-bot
copy-pr-bot Bot deployed to automated-release August 12, 2026 05:45 Active
@copy-pr-bot
copy-pr-bot Bot deployed to automated-release August 12, 2026 05:45 Active
@github-actions github-actions Bot added the feat label Aug 12, 2026
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This change adds a revision protobuf API, manifest models and validation, Redis and in-memory catalog backends, gRPC server integration, generated bindings, and a Python client with lifecycle tests.

Changes

Revision catalog

Layer / File(s) Summary
Revision contract and generated bindings
modelexpress_common/proto/revision.proto, modelexpress_common/build.rs, modelexpress_common/src/lib.rs, modelexpress_client/python/modelexpress/..., modelexpress_client/python/generate_proto.sh, modelexpress_client/python/tests/test_revision_proto.py
Defines revision messages, states, and three RPCs. Generates Rust and Python bindings. Tests the minimal contract and generated client methods.
Manifest models and validation
modelexpress_common/src/revision.rs, modelexpress_client/python/modelexpress/refit/manifest.py, modelexpress_client/python/tests/test_revision_manifest.py
Adds immutable manifest DTOs and protobuf conversion. Validates launch and later revision metadata, base fields, and S3 payload fields.
Catalog backend and lifecycle state
modelexpress_server/src/revision.rs, modelexpress_server/src/revision/backend*, modelexpress_server/src/revision/state.rs
Adds backend abstractions, Redis and in-memory implementations, idempotent publication, lookup, conflict handling, and commit transitions.
gRPC service and server startup
modelexpress_server/src/revision/service.rs, modelexpress_server/src/server.rs, modelexpress_server/tests/in_process_server.rs
Adds gRPC request validation, error mapping, backend initialization, service registration, health reporting, and server lifecycle integration tests.
Python client adapter and loopback coverage
modelexpress_client/python/modelexpress/refit/catalog.py, modelexpress_client/python/tests/test_revision_catalog.py
Adds the typed RevisionCatalog boundary and GrpcRevisionCatalog. Tests publish, get, commit, conflicts, missing revisions, and loopback RPC behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Poem

I’m a rabbit with records, hopping in line,
Publish, get, and commit now work fine.
Redis guards each revision’s name,
Rust and Python speak the same.
S3 crumbs and states stay bright—
Three tiny RPCs make the catalog right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding an immutable revision catalog service for S3-based delta weight synchronization.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (5)
modelexpress_server/src/revision/service.rs (1)

62-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The created flag is discarded.

publish returns PublicationResult { record, created }, and this handler returns only record. A publisher cannot distinguish a first publication from an idempotent replay. If clients need that signal, expose it in the response message; otherwise this is fine as designed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modelexpress_server/src/revision/service.rs` around lines 62 - 76, Review the
publish_revision handler and expose PublicationResult.created in the
PublishRevision response contract so clients can distinguish new publications
from idempotent replays. Update the protobuf response message and
generated/server usage as needed, while preserving the existing RevisionRecord
payload and publication behavior.
modelexpress_server/src/revision/state.rs (2)

173-207: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a test for the invalid lifecycle path.

The tests cover publish idempotence, conflict, commit, idempotent commit, and not-found. CatalogError::InvalidLifecycle has no coverage. A test that stores a record with an unrecognized state value would close that gap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modelexpress_server/src/revision/state.rs` around lines 173 - 207, Add a test
alongside immutable_publication_is_idempotent_and_rejects_conflicts and
commit_is_an_idempotent_ready_to_committed_transition that inserts or stores a
revision record with an unrecognized state value, invokes the relevant lifecycle
operation, and asserts it returns CatalogError::InvalidLifecycle.

62-67: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove RevisionCatalogState::connect if no external API requires it. The factory connects the backend before state construction, and no repository code calls the state-level method.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modelexpress_server/src/revision/state.rs` around lines 62 - 67, Remove the
unused RevisionCatalogState::connect method, since the factory already connects
the backend before constructing the state and no repository code invokes this
state-level API.
modelexpress_server/src/revision/backend/redis.rs (1)

42-47: 🗄️ Data Integrity & Integration | 🔵 Trivial

Consider a retention policy for revision keys.

revision_key creates one Redis hash per model and target version. Nothing in this file removes or expires those keys, so the catalog grows without bound as versions accumulate. Decide whether a TTL, an explicit delete path, or an external compaction job owns cleanup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modelexpress_server/src/revision/backend/redis.rs` around lines 42 - 47,
Define and implement a cleanup policy for keys generated by revision_key, such
as applying a TTL when they are created, deleting obsolete revisions through an
explicit path, or documenting and wiring an external compaction owner. Ensure
the selected policy prevents unbounded Redis growth while preserving access to
active revision data.
modelexpress_server/src/revision/backend.rs (1)

53-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Report the actual reason when the memory backend is not available.

If memory-backend is enabled without integration-tests, the factory returns "revision catalog currently supports only the Redis backend". That message hides the real cause. State that the in-memory catalog needs the integration-tests feature.

Proposed message change
             #[cfg(not(feature = "integration-tests"))]
             {
-                Err("revision catalog currently supports only the Redis backend".into())
+                Err("in-memory revision catalog requires the 'integration-tests' feature".into())
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modelexpress_server/src/revision/backend.rs` around lines 53 - 79, Update the
cfg(not(feature = "integration-tests")) branch of
create_revision_catalog_backend for BackendConfig::Memory to return an error
stating that the in-memory revision catalog requires the integration-tests
feature, rather than claiming only Redis is supported.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modelexpress_client/python/modelexpress/refit/catalog.py`:
- Around line 59-80: Add a configurable finite timeout to the catalog client and
pass it as the timeout keyword argument in publish_revision, get_revision, and
commit_revision when invoking the corresponding stub RPCs. Update FakeStub to
record RPC keyword arguments, and extend the tests to assert that each call
receives the configured timeout.
- Around line 42-45: Update the channel initialization around
RevisionCatalogServiceStub to preserve TLS for https:// endpoints: use
grpc.secure_channel with appropriate TLS credentials, or explicitly reject HTTPS
when only plaintext host:port targets are supported. Keep grpc.insecure_channel
for http:// endpoints and add coverage verifying the transport selection.

In `@modelexpress_client/python/modelexpress/revision_pb2_grpc.py`:
- Line 24: Update generate_proto.sh so the generated revision_pb2_grpc.py does
not contain an f-string without replacement fields, or configure Ruff to exclude
generated bindings from F541; then regenerate the binding and ensure the fix is
reproducible from the generation script rather than editing revision_pb2_grpc.py
directly.

In `@modelexpress_server/src/revision/backend/redis.rs`:
- Around line 166-200: Update commit_revision and COMMIT_LUA so the
compare-and-swap does not depend on current.encode_to_vec() matching stored
record bytes exactly: perform the state transition atomically in Lua using the
stored hash field and compare an independent lifecycle-state field or the bytes
observed within the same script invocation. Preserve NotFound, AlreadyCommitted,
InvalidState, successful commit, and conflict retry outcomes, and add backoff
between retries to avoid immediate repeated round trips under contention.

In `@modelexpress_server/src/revision/backend/testing.rs`:
- Around line 4-5: Update the module-level documentation in testing.rs to state
that TestRevisionCatalogBackend is selected by create_revision_catalog_backend
for BackendConfig::Memory when the integration-tests feature is enabled,
removing the inaccurate claim that it is not selectable.

In `@modelexpress_server/src/server.rs`:
- Around line 122-127: Wrap the await of create_revision_catalog_backend in
tokio::time::timeout with the same 10-second duration used by the registry and
P2P connection paths. Handle timeout and connection errors through the existing
error logging and propagation flow so startup fails fast while preserving the
successful revision_backend initialization.

In `@modelexpress_server/tests/in_process_server.rs`:
- Around line 104-109: Bound the connection-retry loop that calls
RevisionCatalogServiceClient::connect with tokio::time::timeout, or reuse the
file’s existing wait helper if available. Preserve the retry and delay behavior
within the timeout, then fail the test with a clear timeout error instead of
waiting indefinitely when run_server does not become reachable.

---

Nitpick comments:
In `@modelexpress_server/src/revision/backend.rs`:
- Around line 53-79: Update the cfg(not(feature = "integration-tests")) branch
of create_revision_catalog_backend for BackendConfig::Memory to return an error
stating that the in-memory revision catalog requires the integration-tests
feature, rather than claiming only Redis is supported.

In `@modelexpress_server/src/revision/backend/redis.rs`:
- Around line 42-47: Define and implement a cleanup policy for keys generated by
revision_key, such as applying a TTL when they are created, deleting obsolete
revisions through an explicit path, or documenting and wiring an external
compaction owner. Ensure the selected policy prevents unbounded Redis growth
while preserving access to active revision data.

In `@modelexpress_server/src/revision/service.rs`:
- Around line 62-76: Review the publish_revision handler and expose
PublicationResult.created in the PublishRevision response contract so clients
can distinguish new publications from idempotent replays. Update the protobuf
response message and generated/server usage as needed, while preserving the
existing RevisionRecord payload and publication behavior.

In `@modelexpress_server/src/revision/state.rs`:
- Around line 173-207: Add a test alongside
immutable_publication_is_idempotent_and_rejects_conflicts and
commit_is_an_idempotent_ready_to_committed_transition that inserts or stores a
revision record with an unrecognized state value, invokes the relevant lifecycle
operation, and asserts it returns CatalogError::InvalidLifecycle.
- Around line 62-67: Remove the unused RevisionCatalogState::connect method,
since the factory already connects the backend before constructing the state and
no repository code invokes this state-level API.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6b8e2e20-8b47-4326-8926-a59458da1fa6

📥 Commits

Reviewing files that changed from the base of the PR and between 7e0421e and 56d591e.

📒 Files selected for processing (21)
  • modelexpress_client/python/generate_proto.sh
  • modelexpress_client/python/modelexpress/refit/catalog.py
  • modelexpress_client/python/modelexpress/refit/manifest.py
  • modelexpress_client/python/modelexpress/revision_pb2.py
  • modelexpress_client/python/modelexpress/revision_pb2_grpc.py
  • modelexpress_client/python/tests/test_revision_catalog.py
  • modelexpress_client/python/tests/test_revision_manifest.py
  • modelexpress_client/python/tests/test_revision_proto.py
  • modelexpress_common/build.rs
  • modelexpress_common/proto/revision.proto
  • modelexpress_common/src/lib.rs
  • modelexpress_common/src/revision.rs
  • modelexpress_server/src/lib.rs
  • modelexpress_server/src/revision.rs
  • modelexpress_server/src/revision/backend.rs
  • modelexpress_server/src/revision/backend/redis.rs
  • modelexpress_server/src/revision/backend/testing.rs
  • modelexpress_server/src/revision/service.rs
  • modelexpress_server/src/revision/state.rs
  • modelexpress_server/src/server.rs
  • modelexpress_server/tests/in_process_server.rs

Comment thread modelexpress_client/python/modelexpress/refit/catalog.py
Comment thread modelexpress_client/python/modelexpress/refit/catalog.py
Comment thread modelexpress_client/python/modelexpress/revision_pb2_grpc.py Outdated
Comment thread modelexpress_server/src/revision/backend/redis.rs
Comment thread modelexpress_server/src/revision/backend/testing.rs Outdated
Comment thread modelexpress_server/src/server.rs Outdated
Comment thread modelexpress_server/tests/in_process_server.rs Outdated
Signed-off-by: Hyunjae Woo <hwoo@nvidia.com>
@copy-pr-bot
copy-pr-bot Bot deployed to automated-release August 12, 2026 06:38 Active
@copy-pr-bot
copy-pr-bot Bot deployed to automated-release August 12, 2026 06:38 Active
@nv-hwoo

nv-hwoo commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the actionable CodeRabbit findings in 41e1d5b and resolved the seven inline threads.

Additional nitpick triage:

  • Removed the unused state-level RevisionCatalogState::connect method.
  • Added invalid-lifecycle state coverage.
  • Improved the non-integration memory-backend error.
  • Kept PublishRevision returning the canonical RevisionRecord: idempotent replay intentionally has the same public result, and no current publisher/orchestrator caller requires a separate created signal.
  • Did not add a Redis TTL: immutable revisions can remain valid reconstruction bases, so arbitrary expiry could break a live chain. Retention should be driven by orchestration-owned reachability/GC rather than per-key expiry in this catalog PR.

CI startup root cause and fix: Kubernetes P2P jobs use the Kubernetes metadata backend, but the new Redis-only revision factory was called unconditionally. The revision service is now registered only for supported backends; existing Kubernetes registry/P2P server behavior remains available.

Validation includes workspace Rust tests, integration-feature server tests, two real-Redis lifecycle regressions, affected Python revision tests, Clippy/fmt/cargo-check, generated-binding reproduction with grpcio-tools 1.66.2, changed-file Ruff/pre-commit, and secret-pattern scan.

Signed-off-by: Hyunjae Woo <hwoo@nvidia.com>
@copy-pr-bot
copy-pr-bot Bot deployed to automated-release August 12, 2026 06:53 Active
@copy-pr-bot
copy-pr-bot Bot deployed to automated-release August 12, 2026 06:53 Active
@nv-hwoo

nv-hwoo commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up review found and fixed one Redis lifecycle blocker in 191aa3c.

The first fix made a separate Redis state field authoritative for the atomic Lua transition, but commit still re-encoded and overwrote the protobuf record, while reads decoded the record without overlaying state. That could discard unknown protobuf fields and expose stale lifecycle state.

The corrected storage contract is now:

  • the stored protobuf record is immutable after publication;
  • commit atomically updates only the separate state field;
  • get, commit responses, and idempotent publish replay overlay the authoritative state onto the decoded record;
  • legacy records without a state field use their encoded state and migrate atomically on first commit.

A real-Redis regression now asserts exact stored bytes are unchanged after commit (including an appended unknown protobuf field), a subsequent read reports COMMITTED, and publish replay after commit also reports COMMITTED. Both live-Redis lifecycle tests pass.

Additional validation after this correction: full Rust workspace tests, integration-feature server suite, affected Python revision tests (20 passed), Clippy with all targets/features and warnings denied, cargo fmt/check, changed-file pre-commit, diff check, and secret-pattern scan.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant