mirror of
https://github.com/kvcache-ai/Mooncake.git
synced 2026-11-05 01:17:55 +00:00
[GH-ISSUE #4487] [Store] remove + re-put of the same key never re-offloads with the bucket backend #1140
Labels
No labels
ascend
auto-closed
bug
bug
CI/Build
Common
documentation
enhancement
feature request
good first issue
help wanted
high priority
MetaX
nightly-failure
pull-request
PyTorch Backend
question
RFC
Roadmap
stale
Store
Tests
Transfer Engine
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
kvcache-ai/Mooncake#1140
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @he-yufeng on GitHub (Oct 6, 2026).
Original GitHub issue: https://github.com/kvcache-ai/Mooncake/issues/4487
While extending the regression coverage on #3889 I hit an offload quirk that looks worth its own report.
Repro, in-proc master harness with
bucket_storage_backendandMOONCAKE_OFFLOAD_BUCKET_KEYS_LIMIT=1:put(key)then the object offloads, LOCAL_DISK replica R1 registered.remove(key), master record erased.put(key)again, fresh memory replica, PutEnd queues the offload task.data_size=-1) and no new disk replica ever registers. The object stays memory-only indefinitely.What happens on the client:
FileStorage::OffloadObjectsfinds the local slices fine, butBucketStorageBackendstill holds the key inobject_bucket_map_from the first offload (remove never reaches the backend), soBatchOffloadskips it as an already-persisted duplicate without invoking the completion callback, andFileStorageNACKs it per the "a successful batch may skip duplicates" contract. The master erases the offload marker on the NACK, so the key can never be offloaded again until the backend evicts the stale entry.Two consequences:
ReRegisterOffloadedObjects) looks like it could re-register the old bytes under the new object. I have not verified that second path end to end.Possible directions: propagate
remove()to the storage backend so the persisted entry drops with the master record, or letBatchOffloadoverwrite a persisted entry under a generation check instead of skipping it.Observed on current main (ubuntu 22.04 container, gcc) while testing #3889. I have the full glog trace (heartbeat takes the task, backend processes zero keys, NACK goes back, master cleans the marker) if useful.
@github-actions[bot] commented on GitHub (Oct 6, 2026):
Thanks for opening this issue, @he-yufeng!
40085740A maintainer will triage this when possible. To help us respond faster, please include:
Useful links: Documentation · Contributing guide
@feichai0017 commented on GitHub (Oct 8, 2026):
Confirmed on upstream main
b7595de3fab45a580e4ee8819c2a2940d43183cdin an independent Mooncake checkout, with production sources unchanged and no OrbitKV code. I also reproduced the stale-data rescan path mentioned in the report, with the scope described below.Reproduction results
The test-only patch below uses the existing
FileStorageTestfixture with the production embedded Master,Client,FileStorage, andBucketStorageBackend. It performs actual puts/gets and disk I/O. Offload heartbeats,OffloadObjects, and recovery rescan are invoked synchronously to make the sequence deterministic; the FileStorage background scheduler is not started.Environment: Debian 12 container, GCC 12.2,
USE_CUDA=OFF,USE_TENT=OFF, classic TCP configured, bucket key limit 1, 4,109-byte values. This is a single-process Store reproducer, not a TENT or cross-host transport test.ReRegisterOffloadedObjects()Each round has a different deterministic byte pattern. The test compares all bytes, poisons the
Client::Getdestination, checks both boundary guards, and uses ordinaryRemoveonly after the read lease expires. The first offload and disk-byte check pass in all runs. The next heartbeat after the second offload returns zero tasks.Representative second-round observations:
The rescan result proves that the production rescan/registration path can attach a disk replica backed by old bytes to the new object. I have not tested an actual Master restart/HA transition, nor a subsequent user
Getforced onto that disk replica. The new-valueClient::Getcheck in this reproducer happens before rescan; the stale-byte observation is a direct backendBatchLoadafterward.Root cause and proposed fix
One refinement to the trace in the issue: in this sequential reproduction, the stale entry is filtered before
BatchOffload, by theIsExist(key)check inGroupOffloadingKeysByBucket.OffloadObjectsNACKs keys skipped during grouping, and the Master clears the offload task. Fixing only the duplicate check insideBatchOffloadwould miss this path.For rescan,
ReRegisterOffloadedObjectsreports persisted metadata throughNotifyOffloadSuccess. With no pending offload task, the Master falls back to replica registration for the current key.OffloadTaskItemcarries tenant/key/size, andStorageObjectMetadatacarries location/size, but neither carries the object incarnation needed to distinguish the old and new values. Equal key and size are insufficient; both values in the reproducer deliberately have the same size.My proposed fix is to keep the change within the offload lifecycle and distinguish same-incarnation retry from new-incarnation re-put:
ScanMeta. The persisted metadata needs a replacement/invalidation rule, with conservative handling of legacy entries whose incarnation cannot be established. If recovery is intended to preserve explicit removals across complete Master metadata loss, deletion/tombstone semantics must be defined as well; an incarnation field alone does not establish which objects remain live.This is a proposed design, not a tested fix. Before changing the wire/disk metadata, I would align the existing-bucket compatibility and recovery policy with maintainers. An alternative based on propagating
remove()could work, but it would need equivalent conditional invalidation and ordering against in-flight offloads; a key-only delete or blind overwrite does not address late notifications and rescan.The fix should preserve #3135's valid retry behavior. #3889 addresses missing backing files; here the file exists and contains old bytes, so a missing-file check alone would not detect this case. Regression coverage should include the three cases below plus delayed old completion/NACK, same-incarnation retry, a shared bucket with another live key, and persisted-state reopen/rescan.
Reproduction commands and test-only patch
In a standalone checkout of
b7595de3fab45a580e4ee8819c2a2940d43183cd, save the diff below asreproduction.patchoutside the checkout and apply it. With the normal Mooncake C++ build dependencies available (including Boost, msgpack, xxhash and zstd development headers/libraries):For my run, missing development packages were extracted into an external private dependency prefix and passed via
CMAKE_PREFIX_PATH/-isystem; no production code changes or CUDA stubs were used. The executable, runtime libraries, patch and configuration were frozen and SHA256-checked before and after testing. Patch SHA256:ba13e352058e582841a1e59bb6328d4d942afcd7225798429657e9f465c713dc.The following launches each new case three times with separate working directories and a timeout, then the two existing controls. It prints expected nonzero statuses instead of stopping at the first reproduced failure. Log/XML output stays outside the checkout.
The patch adds tests only, using the fixture's existing friendship for deterministic calls to production offload/rescan methods. Both bug cases assert the desired correct behavior and therefore fail on the tested main revision.