* fixup! Implement `ChangeAliases` operation
* fixup! Add `ChangeAliases` to replay-safety tests
* fixup! Add `ChangeAliases` tests
* De-slop ⛷️
* De-slop 🏂
* Add `TestSlowDown` and `TestTransientError` actions
These would have to be implemented on `TableOfContent` when switching
to `ConsensusStateMachine` as main consensus impl
* Handle more stupid corner-cases for `ChangeAliases` prop tests
* Add `AliasMapping::remove` and `AliasMapping::rename` methods
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Implement `ChangeAliases` operation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add `ChangeAliases` to replay-safety tests
Multi-action operations that rename an alias are skipped in the convergence
property: the current implementation does not replay them convergently, and
the machine reproduces that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add `ChangeAliases` tests
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add test-only `PeerMetadata::new` constructor
Test state needs peers at a version other than this build. The `version` field
is crate-private and `current()` is the only constructor, so gate the new one
on the `testing` feature and enable it for the `storage` test build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Implement `UpdatePeerMetadata` operation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add `UpdatePeerMetadata` to replay-safety tests
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add `UpdatePeerMetadata` tests
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Implement `UpdateClusterMetadata` operation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add `UpdateClusterMetadata` to replay-safety tests
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add `UpdateClusterMetadata` tests
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Implement `SetQuotaConfig` operation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add `SetQuotaConfig` to replay-safety tests
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add `SetQuotaConfig` tests
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Implement `TestSlowDown` and `TestTransientError` operations
Both are node-local: one sleeps, the other fails at random. They plan no
actions, like `Nop`, so the replay-safety properties have nothing to add.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add `TestSlowDown` and `TestTransientError` tests
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fixup! Add `UpdatePeerMetadata` to replay-safety tests
* fixup! Add `UpdateClusterMetadata` tests
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
[audit-K] Make create_shard_key crash-safe with a single commit point
create_shard_key persisted shard_key_mapping.json incrementally, one add_shard
per placement entry, so the operation was not atomic across a crash. The
re-apply gate state.shards_key_mapping.contains_key(&shard_key) becomes true
after the first loop iteration, so a crash mid-loop on a multi-shard placement
left the peer permanently holding a subset of the key's shards while every other
peer had all of them.
Write the mapping exactly once instead, after every shard of the key exists on
disk, so there is no partial state to observe. SaveOnDisk writes atomically,
load_shards derives the shard id list from the mapping alone (unreferenced
directories are invisible after restart), create_shard_dir wipes leftovers, and
max_shard_id reads only the mapping - so a replay allocates exactly the ids the
crashed attempt did, which are the ids every other peer allocated too. The
contains_key gate then holds as intended: it fires only for an operation that
already completed in full, or for a genuine duplicate.
ShardHolder::add_shards registers a batch and persists the mapping once, with
add_shard as a one-element wrapper; the mapping write moved ahead of the
in-memory updates, being the only fallible step. No caller changes behavior:
Collection::new and load_shards already hit the write_optional early return, and
start_resharding_unchecked still adds a single id. An empty placement is now
rejected rather than silently returning Ok without creating the key.
Covered by create_shard_key_test.rs: id allocation, replay before and after the
commit, duplicate rejection, and the empty placement guard.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
drop_shard_key force-aborted any resharding on the key being dropped, but
swallowed a failed abort_resharding with a log-only error and dropped the
shards anyway. That could leave resharding_state.json referencing the
just-dropped key — a latent inconsistent load-time state.
Propagate the error with `?`. A ServiceError halts consensus and retries after
restart, which is safe because the abort and everything else in drop_shard_key
is replay-tolerant; a user error dismisses the entry before any shard is
dropped, which is equally consistent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A ServiceError returned from apply halts consensus on every peer (the entry can
never be applied), deterministically stalling the whole cluster. The transfer
Start validations that report a missing source or destination shard are
reachable — e.g. a committed Start racing a resharding-abort or shard-key-drop
that removed the shard — and all run before any durable write, so dismissing
the entry with a user error is safe and correct.
Convert five such sites to CollectionError::bad_request:
- validate_transfer: source shard missing, and destination shard missing in
both the resharding and filtered branches (helpers.rs);
- start_shard_transfer: the source and target get_shard lookups
(shard_transfer.rs).
The genuine "single node deployment" service_errors in collection_meta_ops.rs
are left untouched — those are real misconfiguration guards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Start's first durable write registers the transfer record; its last write sets
the destination replica state. On re-apply after a crash between the two,
check_transfer_conflicts found the operation's own half-applied transfer and
returned bad_request, which is dismissed and never retried — so the peer
permanently lacked the destination replica entry. Worse, a later Finish on
that peer then silently skips both the destination promotion and the source
removal, pinning a replica-set divergence.
Exclude the transfer's own key from the conflict scan so a replay falls through
and re-runs the (idempotent) start: register_start_shard_transfer is a
set-insert and the destination replica-state write is absolute, so re-running
reconciles the partial state instead of dismissing it. A genuinely conflicting
transfer (different key touching the same shard/peers) is still rejected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
start_resharding, finish_resharding and abort_resharding each saved the
updated collection config with a log-only `if let Err(err) = config.save(..)`
that swallowed the failure. A swallowed save let the operation report success
with a stale shard_number persisted on disk, arming a shard-dir/loader panic
(or a silently unloaded shard) at the next restart.
Propagate the error with `config.save(&self.path)?;` instead. The resulting IO
error is a ServiceError, so consensus halts and retries the entry after
restart. That is safe because in all three functions the config save is
value-idempotent (guarded by `shard_number != new_shard_number`) and every
step is replay-tolerant, so the retry converges.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In the up-direction of abort_resharding, ShardHolder::abort_resharding drops
the new shard's directory, but the config.params.shard_number update was
persisted only at the very end of the function — after the transfers abort.
A crash anywhere in that wide window left shard_number pointing at an
already-deleted shard directory, which makes the auto-sharding loader panic
on the missing dir at startup (crash loop), before the consensus replay that
would reconcile the state can run.
Move the shard-count update block to before the shard_holder.abort_resharding
call. The block keeps its value-idempotence guard, so replay converges. The
config write lock is taken while holding the shard_holder write guard, matching
the shard_holder -> config ordering already used in finish_resharding, and is
released before abort_resharding. The reverse crash window (dir still present
but count already decremented) is benign and reconciled by replay.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In the down-direction of finish_resharding, the config.params.shard_number
update was persisted *after* drop_and_remove_shard. A crash between the two
left shard_number pointing at an already-deleted shard directory, which makes
the auto-sharding loader panic on the missing dir at startup (crash loop) —
before the consensus replay that would reconcile the state can run.
Move the shard-count update block to before drop_and_remove_shard (still after
remove_shard_from_key_mapping). The block keeps its value-idempotence guard, so
replay converges. The reverse crash window (dir still present but count already
decremented) is benign and reconciled by replay.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When Medium/Strong write-ordering forwards an update to the leader and it
fails with a transient error, the failure path proposed deactivating the
leader replica while passing its raw peer state as from_state. Unlike the
sibling deactivation site, this did not filter out transient/resharding
states, so a Resharding/ReshardingScaleDown leader could be proposed
Dead with from_state=Some(Resharding*).
On re-apply after a crash inside abort-resharding (which reverts the gated
replica to Active or removes it), the from_state gate no longer matches the
current state -> bad_input -> the entry is dismissed and never retried, so
the peer keeps Active while the rest of the cluster has Dead.
Filter the leader state with `.filter(|state| !state.is_partial_or_recovery())`,
matching the sibling site (PR #7849), so the proposal omits from_state for
transient/resharding states and converges on replay.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Simplify ownership and locking in `set_shard_replica_state`
Since #8830, replica sets are wrapped in `Arc`, so we don't *need* to
hold `shard_holder` read-lock during `set_shard_replica_state`
* Abort transfers *before* switching replica state to `Dead`
idempotency, am I right boys??
* Empty commit to open a PR
* Refactor `MultivectorOffsetsStorageMmap` to use `MmapFile` instead of `MmapSlice`
* simpler result types
---------
Co-authored-by: generall <andrey@vasnetsov.com>
* Add `EncodedStorage::for_each_in_batch` method
* Implement `for_each_in_batch` method for `QuantizedChunkedMmapStorage`
* Add `EncodedVectors::for_each_in_batch` method
* Add `EncodedVectors::score` method
* Implement `score_stored_batch` for `QuantizedQueryScorer` and `Quanti…
* Remove `TElement` and `TMetric` type parameters from `QuantizedMultiQ…
* Use `QuantizedMultiQueryScorer` when building `raw_internal_scorer`...
* Cleanup lifetimes and generic type parameters
- Rename read pipeline lifetime from `'a` into `'file`
- Use explicit `where` clauses everywhere
* Cleanup
* fixup! Cleanup lifetimes and generic type parameters
* Improvements for `IoUringFile`
- handle read and write requests more explicitly
- assert against partial reads/writes
* Implement `read_multi`/`write_multi`
* Implement `populate` and `clear_ram_cache`
* Rename internal structures as `IoUringSomething`
* [ai] Make `UniversalRead`/`UniversalWrite` impl generic over `T`
* Handle `io_uring` initialization error
* Check that `io_uring` is initialized and supported, when opening `IoUringFile`
* fixup! Check that `io_uring` is initialized and supported, when opening `IoUringFile`
Fix typo
* test for reading u64 from file with uring
* fmt
* clippy
* fix(io_uring): allocate Vec<MaybeUninit<T>> for reads to fix alignment for T (#8353)
* fix(io_uring): allocate Vec<MaybeUninit<T>> for reads to fix alignment for T
Refactor IoUringState::read to take generic T and item_offset/items_length
instead of byte_offset/byte_length. Allocate Vec<MaybeUninit<T>> so the
kernel writes into correctly aligned memory, then convert to Vec<T> in
finalize. Fixes bytemuck::cast_vec alignment panic for types like u64.
Made-with: Cursor
* refactor(io_uring): only make read/write methods generic, not state or runtime
- IoUringState and IoUringRuntime no longer generic over T
- read<T>() allocates Vec<MaybeUninit<T>>, transmutes to Vec<MaybeUninit<u8>> for storage
- finalize returns ReadBuffer; callers use .into_vec::<T>() to get Vec<T>
- Write path unchanged (callers pass bytes via bytemuck::cast_slice)
Made-with: Cursor
* Cleanup
* review n1
* review n2
* fmt
---------
Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Roman Titov <ffuugoo@users.noreply.github.com>
Co-authored-by: Andrey Vasnetsov <andrey@vasnetsov.com>
* use AHash instead of just hash
* add ahash to deps
---------
Co-authored-by: Andrey Vasnetsov <andrey@vasnetsov.com>
Co-authored-by: qdrant-cloud-bot <111755117+qdrant-cloud-bot@users.noreply.github.com>
Co-authored-by: Cursor Agent <agent@cursor.com>
* Implement `__repr__` for `PyJsonPath`
* Implement `__repr__` for `PyFilter`
* Implement `__repr__` for `PyQuery`
* Implement `__repr__` for `PyQueryRequest`
* Implement `__repr__` for `PySearchRequest`
* Implement `__repr__` for `PyFilter` using `pyclass_repr` attribute
* Implement `__repr__` for `PyQuery` using `pyclass_repr` attribute
* Implement `__repr__` for `PyQueryRequest` using `pyclass_repr` attribute
* Implement `__repr__` for `PySearchRequest` using `pyclass_repr` attribute
* Refactor `PyUpdateOperation` constructors
* Add default parameters to `PyVectorDataConfig::new`
* Add `Repr` trait and `WriteExt` helper
* Implement `__repr__` for config types
* fixup! Implement `__repr__` for config types
Use `Copy` instead of `Clone`
* fixup! Implement `__repr__` for config types
Add basic test
* Implement `__repr__` for `PyPointId`
* Implement `__repr__` for `PyVector`
* Implement `__repr__` for `PyVectorInternal`
* Implement `__repr__` for `PyPayload`
* Implement `__repr__` for `PyValue`
* Implement `__repr__` for `PyPoint`
* Implement `__repr__` for `PyPointVectors`
* Implement `__repr__` for `PyRecord`
* Move `PyScoredPoint` into a separate file
* Implement `__repr__` for `PyScoredPoint`
* Cleanup examples
* fixup! Implement `__repr__` for `PyScoredPoint`
* Move `PyOrderValue` into separate file
* Add `PyScoredPoint::order_value`
* Implement `pyclass_repr` proc-macro attribute
* Implement `__repr__` for config types using `pyclass_repr` attribute
* Implement `__repr__` for `PySparseVector` using `pyclass_repr` attribute
* Implement `__repr__` for `PyPoint` using `pyclass_repr` attribute
* Implement `__repr__` for `PyPointVectors` using `pyclass_repr` attribute
* Implement `__repr__` for `PyRecord` using `pyclass_repr` attribute
* Implement `__repr__` for `PyScoredPoint` using `pyclass_repr` attribute
* Minor fixes and cleanups
* fixup! Minor fixes and cleanups
* rollback copy for quantization config
* rollback copy for quantization config
---------
Co-authored-by: generall <andrey@vasnetsov.com>
* Remove faulty mark-as-dead condition when recovering Raft snapshot 😅
* Mark new local replicas as locally-disabled when recovering Raft snapshot
* Add test
* Make `snapshot_manifest` methods `async`, so that they don't block async runtime
* Spawn `restore_shard_snapshot` task on `update` runtime instead of "current"