diff --git a/sw-block/.private/phase/phase-10-decisions.md b/sw-block/.private/phase/phase-10-decisions.md new file mode 100644 index 000000000..9f582ca7e --- /dev/null +++ b/sw-block/.private/phase/phase-10-decisions.md @@ -0,0 +1,109 @@ +# Phase 10 Decisions + +## Decision 1: Phase 10 is control-plane closure, not backend execution rework + +`Phase 09` already closed the main backend execution gaps on the chosen path. + +`Phase 10` should therefore focus on: + +1. real control delivery +2. reassignment / result convergence +3. identity cleanup + +It should not reopen accepted backend execution semantics unless a true control-plane bug forces a narrow correction. + +## Decision 2: Phase 10 remains bounded to the chosen path + +Default scope remains: + +1. `RF=2` +2. `sync_all` +3. existing master / volume-server heartbeat path + +Future durability modes or wider topology support should not be absorbed casually into this phase. + +## Decision 3: Identity cleanup belongs to control-plane closure + +The current local server identity remains transport-shaped (`listenAddr`). + +`Phase 10` is the right place to strengthen this because identity coherence affects: + +1. assignment truth +2. sender/replica identity continuity +3. end-to-end control-path correctness + +## Decision 4: Rebuild-server idempotence cleanup is bounded residual work, not the phase itself + +The repeated-primary-assignment warning around rebuild-server start is a valid residual note. + +It may be addressed in `Phase 10` only if: + +1. it is directly relevant to real control/runtime ownership or assignment idempotence +2. it stays bounded + +It must not turn `Phase 10` into a broad runtime polish phase. + +## Decision 5: P1 identity and control-truth closure accepted + +`P1` closes the stable-identity/control-truth gap on the chosen block assignment wire. + +Accepted properties: + +1. stable server identity is now carried additively on the block assignment proto wire: + - scalar `replica_server_id` + - per-replica `server_id` +2. generated protobuf output, not hand-maintained output, is now the accepted basis for the wire shape +3. master create-path and chosen failover/primary-refresh assignment generation now preserve stable identity on the chosen path +4. volume-server block/control path now uses the same canonical `volumeServerId` as the main volume server +5. `ControlBridge` continues to fail closed when stable identity is missing + +Evidence closure: + +1. proto/decode proof now covers stable identity round-trip +2. real ingress proof now covers: + - proto assignment + - decode + - `ProcessAssignments()` + - `ControlBridge` + - engine sender `ReplicaID` +3. canonical local identity proof now covers non-default local ID +4. missing-ID fail-closed proof is covered directly + +## Decision 6: P2 reassignment/result convergence accepted under the chosen-path volume-server ingress bound + +`P2` closes the main reassignment/result-convergence gap on the chosen path without reopening accepted backend execution semantics. + +Accepted properties: + +1. reassignment through the accepted chosen-path ingress now proves old sender truth is removed and new sender truth is created +2. stale runtime ownership is now proved as a live drain case, not only a bookkeeping absence case +3. reported truth is now checked through `CollectBlockVolumeHeartbeat()`, the same reporting surface used by the live heartbeat loop +4. the accepted no-split-truth claim is bounded to: + - engine sender truth + - stale-runtime residue removed + - heartbeat output truth +5. `P2` does not claim full master-driven failover/gRPC-infrastructure closure beyond the accepted volume-server-side ingress boundary + +Evidence closure: + +1. real reassignment proof covers `vs2 -> vs3` sender replacement on the chosen path +2. stale-owner proof now blocks a live old goroutine and verifies drain during reassignment +3. heartbeat proof now checks actual heartbeat output rather than local helper state +4. delivery wording is bounded so it does not overclaim a proved live replacement owner in the no-split-truth test + +## Decision 7: P3 bounded repeated-assignment/idempotence cleanup accepted on the chosen path + +`P3` closes the bounded repeated-assignment residual left after accepted `P2`. + +Accepted properties: + +1. repeated unchanged chosen-path assignment is now skipped before duplicate V2 orchestrator/recovery work is started +2. the corresponding V1 primary-replication setup path is also absorbed idempotently for unchanged truth +3. changed chosen-path assignment still takes the accepted replacement/update path rather than being suppressed incorrectly +4. `P3` remains bounded cleanup and does not claim general multi-replica idempotence or broad production hardening + +Evidence closure: + +1. repeated-assignment proof now checks stable V2 event count rather than only stable helper/reporting state +2. changed-assignment guard proof keeps accepted replacement behavior intact +3. externally visible heartbeat state remains coherent after repeated unchanged assignment diff --git a/sw-block/.private/phase/phase-10-log.md b/sw-block/.private/phase/phase-10-log.md new file mode 100644 index 000000000..33e556c7d --- /dev/null +++ b/sw-block/.private/phase/phase-10-log.md @@ -0,0 +1,1182 @@ +# Phase 10 Log + +Date: 2026-04-02 +Status: active +Purpose: record the technical pack and design notes for stronger control-plane closure after `Phase 09` + +--- + +### P0 Technical Pack + +Date: 2026-04-02 +Goal: turn the accepted chosen-path backend execution into a stronger end-to-end control path where assignment intent, live runtime ownership, and externally visible control truth converge coherently + +#### Layer 1: Semantic Core + +##### Problem statement + +`Phase 09` accepted the chosen-path backend execution and live runtime ownership. + +What still remains weak is the end-to-end control path: + +1. real assignment ingress is accepted at `ProcessAssignments()`, but fuller heartbeat / gRPC-level closure is still bounded +2. local identity is still transport-shaped (`listenAddr`) +3. reassignment / result convergence is not yet proven deeply enough through the full live control path +4. repeated assignment behavior still has at least one bounded low-severity idempotence warning + +`Phase 10` should close the main control-plane gap without reopening the accepted backend execution semantics. + +##### Backend / system reality + +1. engine policy is already accepted and should remain the policy owner +2. `RecoveryManager` already owns live runtime start/cancel/replace/cleanup on the chosen path +3. `ProcessAssignments()` is already the accepted local assignment-entry point +4. the remaining uncertainty is not "can the backend execute" +5. it is "does the real control path drive, replace, and report that execution coherently enough" + +##### High-level algorithm + +Use this control model: + +1. master emits assignment / epoch / identity truth for the chosen path +2. the real control path delivers that truth into the volume server +3. the volume server converts it into stable local assignment/runtime truth +4. the orchestrator/runtime layer applies start / replace / cancel based on that truth +5. heartbeat/result reporting reflects the same truth back out +6. failover / reassignment should converge without split truth between: + - master/control intent + - local runtime owner + - externally visible reported state + +##### Pseudo code + +```text +on control-plane assignment delivery: + decode assignment truth + normalize identity inputs + apply assignment to local control/runtime owner + +on local ownership change: + start / replace / cancel accepted runtime owner + keep engine policy unchanged + +on reporting: + publish state derived from current accepted owner + assignment truth + do not report stale ownership after reassignment + +on failover / reassignment: + old control truth is invalidated + new control truth becomes authoritative + local runtime owner and reported state converge to it +``` + +##### State / convergence contract + +`Phase 10` should make these truths explicit: + +1. stable identity is not inferred casually from transport address shape +2. control ingress truth, runtime ownership truth, and reported truth should not diverge for the chosen path +3. reassignment must not leave stale local owner/reporting visible after control truth changes +4. accepted backend execution should remain a dependency, not be reopened as a new correctness debate + +##### Reject shapes + +Reject before implementation if the package still relies on: + +1. proofs that stop at `ProcessAssignments()` without fuller control-path closure +2. identity that remains transport-shaped where stronger stable identity is required +3. local/runtime/reporting divergence after reassignment +4. reopening accepted `Phase 09` execution semantics as the main work item +5. expanding into product-surface rebinding or broad hardening + +#### Layer 2: Execution Core + +##### Current gaps `P0` identifies for `Phase 10` + +1. fuller heartbeat / gRPC delivery proof is still missing on the chosen path +2. local identity is still too transport-shaped for stronger product/control claims +3. reassignment/result convergence through the real control path is still under-proven +4. repeated-assignment idempotence still has a bounded warning surface + +##### Recommended slice order + +1. `P1`: identity and control-truth closure + - make local identity cleaner than transport-shaped `listenAddr` + - tighten control truth so assignment identity and runtime identity converge + +2. `P2`: reassignment / result convergence + - prove failover / reassignment converge through the real control path + - ensure reported state and runtime owner do not lag old truth + +3. `P3`: bounded idempotence cleanup + - close repeated-assignment/rebuild-server warning only if still needed after `P1` / `P2` + +##### Reuse / update instructions + +1. `weed/server/volume_grpc_client_to_master.go` + - `update in place` + - strengthen the proof around real control delivery if this is the active ingress path + +2. `weed/server/volume_server_block.go` + - `update in place` + - keep this as the main local control/runtime application surface + +3. `weed/storage/blockvol/v2bridge/control.go` + - `update in place` + - tighten identity and control-truth mapping only as needed + +4. `weed/server/block_recovery.go` + - `reference only` unless a narrow control/runtime convergence bug requires change + - do not reopen accepted ownership semantics casually + +5. `sw-block/engine/replication/*` + - `reference only` unless the control-plane slice exposes a true engine-facing gap + - keep policy in engine + +6. copy guidance + - prefer `update in place` + - use `reference only` for accepted execution/runtime code + - do not fork a second control/runtime path + +##### Validation focus + +Required proofs: + +1. real control-ingress proof + - heartbeat / gRPC-level delivery reaches the live local control/runtime path + +2. identity proof + - chosen-path local and remote identity are coherent enough for reassignment and reporting + +3. reassignment proof + - old truth is invalidated + - new truth becomes authoritative + - local owner/reporting converge accordingly + +4. no-split-truth proof + - do not leave different truths simultaneously visible across: + - assignment ingress + - runtime owner + - reported heartbeat/control state + +5. bounded-cleanup proof + - if repeated-assignment idempotence is touched, prove the bounded cleanup directly + +Reject if: + +1. proof still stops at local assignment intake only +2. identity remains transport-shaped where stronger identity is required +3. reassignment leaves stale reporting or stale runtime owner visible +4. implementation reopens accepted backend execution semantics + +##### Suggested first cut + +1. define the exact real control ingress boundary to prove in `P1` +2. define the identity truth to carry across that boundary +3. define the visible reported-state convergence target after reassignment +4. keep idempotence cleanup as a bounded residual, not the main slice + +##### Assignment For `sw` + +1. Goal + - deliver the first control-plane closure slice after `P0` is accepted + +2. Required outputs + - one real control-path proof + - one identity/control-truth proof + - one reassignment/result-convergence proof shape + - short reuse note: + - files updated in place + - files used as references only + - any copied code and why + +3. Hard rules + - do not reopen accepted `Phase 09` backend execution semantics + - do not let bounded idempotence cleanup become the whole phase + - keep product surfaces out of this phase + +##### Assignment For `tester` + +1. Goal + - validate that `Phase 10` closes real control-path gaps rather than adding another layer of local-only bookkeeping + +2. Validate + - real control ingress + - identity coherence + - reassignment/result convergence + - no split truth between ingress/runtime/reporting + +3. Reject if + - evidence stops at `ProcessAssignments()` only + - identity remains transport-shaped without an accepted bound + - claims exceed the chosen-path control closure target + +--- + +### P1 Technical Pack + +Date: 2026-04-02 +Goal: close stable identity and control-truth on the real control ingress path so the chosen-path control chain preserves server identity end-to-end instead of relying on transport-shaped fallback + +#### Layer 1: Semantic Core + +##### Problem statement + +The accepted chosen path already depends on stable identity semantics: + +1. `v2bridge/control.go` defines `ReplicaID = /` +2. control tests already reject address-derived identity +3. `RecoveryManager` and accepted runtime ownership now assume that control truth is stable enough to drive start/replace/cancel on the right replica target + +But the real control wire is still weaker than that accepted truth: + +1. `BlockService.localServerID` is still initialized from `listenAddr` +2. `master.proto` block-assignment messages still do not carry stable `ServerID` fields +3. `block_heartbeat_proto.go` therefore drops stable replica identity on proto round-trip +4. master-side assignment producers currently populate replica addresses but not stable replica server identity + +So the current system contains a real split: + +1. accepted local V2 control semantics want stable `ServerID` +2. the actual heartbeat/gRPC assignment wire still mainly carries transport addresses + +`P1` should close that split. + +##### Concrete current gap + +The main concrete gap is now explicit: + +1. `weed/pb/master.proto` + - `BlockVolumeAssignment` carries: + - `replica_data_addr` + - `replica_ctrl_addr` + - `rebuild_addr` + - `replica_addrs` + - but it does not yet carry: + - scalar `replica_server_id` + - per-replica `server_id` in `ReplicaAddrMessage` +2. `weed/storage/blockvol/block_heartbeat_proto.go` + - proto conversion currently round-trips addresses only +3. `weed/server/master_grpc_server_block.go` / `weed/server/master_block_failover.go` + - assignment generation currently populates addresses but not stable server identity +4. `weed/server/volume_server_block.go` + - local identity is still seeded from `listenAddr` + +##### High-level algorithm + +Use this identity/control-truth model: + +1. choose one authoritative stable local server identity from the real control-plane/registry path +2. carry stable replica server identity through the assignment wire: + - scalar RF=2 path + - multi-replica path +3. decode that same stable identity on the volume server +4. feed it into `ControlBridge` and runtime ownership without address-derived fallback +5. fail closed if the chosen path requires a stable identity and the wire does not provide one + +##### Pseudo code + +```text +on master assignment build: + emit stable replica server id(s) together with replica address(es) + +on proto encode/decode: + preserve server id(s) exactly + +on volume server control ingest: + derive authoritative local server id from stable control identity + convert assignment using stable server id(s), not listen address + +on ControlBridge conversion: + build ReplicaID from / + reject missing identity instead of synthesizing address-based identity +``` + +##### State / convergence contract + +`P1` should make these truths explicit: + +1. stable identity on the chosen path is not advisory metadata; it is part of control truth +2. the same stable identity should survive: + - master assignment generation + - proto wire encode/decode + - volume-server ingest + - `ControlBridge` conversion + - runtime owner target selection +3. local server identity should no longer be transport-shaped if `P1` claims identity closure +4. no chosen-path code should silently fall back to address-derived identity when a stable ID is required + +##### Reject shapes + +Reject before implementation if the proposed slice still does any of the following: + +1. preserves stable `ServerID` only in tests, not on the real gRPC wire +2. leaves `localServerID` transport-shaped while claiming control-truth closure +3. silently reconstructs identity from address text +4. proves only local conversion helpers but not the real ingress chain +5. absorbs reassignment/result-convergence work that belongs to `P2` + +#### Layer 2: Execution Core + +##### Current gaps `P1` must close + +1. `weed/pb/master.proto` + - add the stable server-identity fields required by the chosen path + +2. `weed/storage/blockvol/block_heartbeat_proto.go` + - preserve the new stable identity fields in both directions + +3. `weed/server/master_grpc_server_block.go` + - populate stable server identity for created assignments, not addresses only + +4. `weed/server/master_block_failover.go` + - populate stable server identity on failover/reassignment-built assignments as well + +5. `weed/server/volume_server_block.go` + - stop treating `listenAddr` as the long-term local stable identity if the real control path can provide a better source + +6. `weed/storage/blockvol/v2bridge/control.go` + - keep fail-closed stable-ID mapping, but align it with the real incoming wire shape after the wire is fixed + +##### Reuse / update instructions + +1. `weed/pb/master.proto` + - `update in place` + - add only the stable identity fields needed by the chosen path + +2. `weed/storage/blockvol/block_heartbeat_proto.go` + - `update in place` + - keep proto round-trip explicit and symmetric + +3. `weed/server/master_grpc_server_block.go` + - `update in place` + - carry stable identity from registry/assignment source into the wire assignment + +4. `weed/server/master_block_failover.go` + - `update in place` + - preserve the same identity rules during promotion/reassignment + +5. `weed/server/volume_grpc_client_to_master.go` + - `reference only` unless a narrow proof hook or assertion point is required + - do not redesign the whole heartbeat loop + +6. `weed/server/volume_server_block.go` + - `update in place` + - tighten the source of `localServerID` only as much as needed for the chosen path + +7. `weed/storage/blockvol/v2bridge/control.go` + - `update in place` + - align comments/contracts with the real wire after stable-ID fields exist end-to-end + +8. `weed/server/block_recovery.go` + - `reference only` + - accepted runtime ownership from `P4` is not the target of this slice + +9. copy guidance + - prefer `update in place` + - no parallel control-bridge implementation + - no address-derived compatibility shim that becomes the new truth + +##### Validation focus + +Required proofs: + +1. proto round-trip proof + - scalar assignment preserves `replica_server_id` + - multi-replica assignment preserves per-replica `server_id` + +2. master assignment proof + - create path emits stable server IDs + - failover/reassignment path emits stable server IDs + +3. real ingress proof + - heartbeat/gRPC delivery reaches the volume server with stable IDs intact + - `ControlBridge` conversion builds the same `ReplicaID` the engine/runtime expect + +4. local identity proof + - local replica/rebuild assignment uses a stable local identity source + - chosen-path control truth does not depend on `listenAddr` string shape + +5. fail-closed proof + - missing stable identity on the chosen path is rejected rather than synthesized from address text + +Reject if: + +1. stable identity still disappears on proto round-trip +2. tests only prove `ControlBridge` unit conversion without the real control wire +3. `localServerID` is still fundamentally transport-shaped after the slice claims closure +4. the slice claims full reassignment/result convergence rather than identity/control-truth closure + +##### Suggested first cut + +1. add stable-ID fields to the block assignment proto and Go conversion layer +2. wire stable IDs into master-side assignment production +3. choose and wire one authoritative local server ID source on the volume server +4. add one real heartbeat/gRPC ingress proof using those stable IDs +5. add one fail-closed proof for missing identity + +##### Assignment For `sw` + +1. Goal + - deliver `P1` identity and control-truth closure on the real chosen-path control wire + +2. Required outputs + - real wire preservation of stable server identity + - local server identity no longer transport-shaped on the chosen path + - one real ingress proof + - one fail-closed proof + - short reuse note: + - files updated in place + - files used as references only + - any copied code and why + +3. Hard rules + - do not reopen accepted `Phase 09` backend execution semantics + - do not treat address text as stable identity + - do not absorb reassignment/result-convergence proof into `P1` + +##### Assignment For `tester` + +1. Goal + - validate that `P1` closes the real stable-identity/control-truth gap rather than only tightening local helper comments + +2. Validate + - stable server IDs survive the real proto wire + - master-produced assignments preserve stable identity + - volume-server ingest preserves stable identity + - missing identity fails closed on the chosen path + +3. Reject if + - stable identity still exists only in tests or local structs + - the real gRPC/heartbeat path still drops identity + - claims exceed identity/control-truth closure + +--- + +### P1 Completion Record + +Date: 2026-04-02 +Status: accepted (rev 3) +Revisions: 3 (rev 1: initial stable-ID wire change, rev 2: canonical local identity + generated proto required, rev 3: real ingress / canonical-local / fail-closed proofs added and handoff wording tightened) + +#### Accepted contract + +- stable identity on the chosen path is now part of the real block assignment control truth +- the chosen-path control wire now preserves: + - scalar `replica_server_id` + - per-replica `server_id` +- local block/control identity now uses the same canonical `volumeServerId` as the main volume server +- missing stable identity on the chosen path fails closed instead of falling back to address-derived identity +- `P1` does not claim full reassignment/result convergence + +#### Files changed + +| File | Action | +|------|--------| +| `weed/pb/master.proto` | Updated: additive stable-ID fields for block assignments | +| `weed/pb/master_pb/master.pb.go` | Regenerated: stable-ID fields carried in generated protobuf output | +| `weed/pb/master_pb/master_grpc.pb.go` | Regenerated alongside `master.pb.go` | +| `weed/storage/blockvol/block_heartbeat_proto.go` | Updated: preserve stable ID on proto encode/decode | +| `weed/server/master_grpc_server_block.go` | Updated: create-path assignments carry stable ID on chosen path | +| `weed/server/master_block_failover.go` | Updated: promotion/refresh chosen-path assignments carry stable ID | +| `weed/server/volume_server_block.go` | Updated: canonical block/control server identity setter | +| `weed/command/volume.go` | Updated: block service uses canonical `volumeServerId` | +| `weed/storage/blockvol/block_heartbeat_proto_test.go` | New/updated: stable-ID proto round-trip tests | +| `weed/server/qa_block_identity_test.go` | New: real ingress / canonical-local / fail-closed proofs | + +#### Test inventory (6 P1 tests) + +| Test | Proves | +|------|--------| +| `ProtoRoundTrip_ScalarServerID` | scalar stable ID survives proto/decode | +| `ProtoRoundTrip_MultiReplicaServerID` | per-replica stable ID survives proto/decode | +| `ProtoRoundTrip_MissingServerID_NotSynthesized` | missing ID stays empty | +| `RealIngress_ProtoToReplicaID` | proto -> decode -> `ProcessAssignments()` -> `ControlBridge` -> engine `ReplicaID` | +| `CanonicalLocalIdentity` | local block/control path uses canonical non-default local server ID | +| `FailClosed_MissingServerID` | missing ID is rejected without address-derived fallback | + +#### Residual note + +1. `P1` does not claim full reassignment/result convergence through the real control path +2. some scalar replica/rebuild assignment paths still rely on local chosen-path identity rather than carrying remote stable ID on every role shape + +--- + +### P2 Technical Pack + +Date: 2026-04-02 +Goal: close reassignment / failover result convergence on the chosen path so control truth changes do not leave stale local owners or stale reported truth behind + +#### Layer 1: Semantic Core + +##### Problem statement + +`P1` closed stable identity and control-truth on the chosen block assignment wire. + +What is still open is dynamic convergence when control truth changes: + +1. after failover or reassignment, old local ownership may linger too long +2. externally visible heartbeat/control truth may lag the new assignment truth +3. local/runtime/reporting state may temporarily point at different realities + +`P2` should close that dynamic convergence gap on the chosen path. + +##### System reality + +1. `ProcessAssignments()` is already the local assignment-application surface +2. `RecoveryManager` already provides accepted start / cancel / replace ownership +3. the remaining question is not whether replacement can happen +4. it is whether the full control path converges the whole system to the new truth without stale residue + +##### High-level algorithm + +Use this convergence model: + +1. a new assignment / epoch / failover event defines new control truth +2. local control ingress applies the new truth +3. stale runtime owner is cancelled / replaced +4. visible reported state is refreshed so it reflects the same new truth +5. no old truth should remain externally visible after convergence completes + +##### Pseudo code + +```text +on reassignment / failover: + receive new assignment truth + invalidate stale local owner + start or stop replacement owner as required + refresh/report state derived from the new truth + +after convergence: + old owner is gone + current owner matches new truth + externally visible state matches new truth +``` + +##### State / convergence contract + +`P2` should make these truths explicit: + +1. old assignment truth must not remain authoritative after reassignment +2. stale runtime owner must not survive convergence +3. reported/heartbeat truth must not continue advertising stale ownership after convergence +4. chosen-path control truth should converge without reopening backend execution semantics + +##### Reject shapes + +Reject before implementation if the slice: + +1. proves only local invalidation without proving reported/control convergence +2. relies on helper-only tests without the real control path +3. leaves stale owner or stale reporting visible after reassignment +4. turns into broad hardening or product-surface work +5. absorbs the rebuild-server idempotence warning as the main story + +#### Layer 2: Execution Core + +##### Current gaps `P2` must close + +1. real failover/reassignment convergence is still under-proven on the chosen path +2. reported/control truth after reassignment is still weaker than the runtime/executor proof +3. the relation between: + - assignment ingress + - runtime owner replacement + - heartbeat/control reporting + still needs direct proof + +##### Reuse / update instructions + +1. `weed/server/volume_grpc_client_to_master.go` + - `update in place` + - use this if needed for fuller heartbeat/gRPC reassignment proof + +2. `weed/server/volume_server_block.go` + - `update in place` + - keep this as the local convergence application surface + +3. `weed/server/block_recovery.go` + - `reference only` unless a true convergence bug requires a narrow fix + - accepted replacement ownership from `P4` should not be casually reopened + +4. `weed/server/master_block_failover.go` + - `reference only` unless proof work exposes a real stale-truth bug in assignment production + +5. `weed/storage/blockvol/v2bridge/control.go` + - `reference only` unless a true control-truth mismatch remains after `P1` + +6. copy guidance + - prefer `update in place` + - do not fork a second reassignment/control path + - keep bounded idempotence cleanup separate unless required by this slice + +##### Validation focus + +Required proofs: + +1. real reassignment proof + - failover / reassignment changes real control truth on the chosen path + +2. stale-owner proof + - old runtime owner is gone after convergence + +3. reported-truth proof + - externally visible heartbeat/control truth reflects the new assignment truth + +4. no-split-truth proof + - assignment ingress, runtime owner, and reported state converge to the same current truth + +5. bounded-fail-closed proof + - if reassignment cannot converge cleanly, do not overclaim completion + +Reject if: + +1. proof stops at identity-only closure already accepted in `P1` +2. stale owner can remain alive after reassignment convergence +3. heartbeat/control reporting still reflects stale truth after reassignment +4. implementation reopens accepted backend execution semantics + +##### Suggested first cut + +1. define one real reassignment/failover scenario on the chosen path +2. prove runtime-owner replacement for that scenario +3. prove the corresponding reported/control truth update +4. keep repeated-assignment idempotence as a residual unless directly exposed + +##### Assignment For `sw` + +1. Goal + - deliver `P2` reassignment / result convergence on the chosen path + +2. Required outputs + - one real reassignment/failover convergence proof + - one stale-owner removal proof + - one reported/control-truth convergence proof + - short reuse note: + - files updated in place + - files used as references only + - any copied code and why + +3. Hard rules + - do not reopen accepted `Phase 09` backend execution semantics + - do not re-solve stable identity already accepted in `P1` + - do not let idempotence cleanup become the whole slice + +##### Assignment For `tester` + +1. Goal + - validate that `P2` closes real reassignment/result convergence rather than only proving local replacement bookkeeping + +2. Validate + - real reassignment/failover control path + - stale owner removed + - reported/control truth converges + - no split truth after reassignment + +3. Reject if + - evidence remains identity-only + - stale local or reported truth remains visible + - claims exceed reassignment/result convergence + +--- + +### P2 Completion Record + +Date: 2026-04-02 +Status: accepted (rev 2) +Revisions: 2 (rev 1: initial convergence package under-proved stale owner and reported truth, rev 2: live stale-owner drain proof + heartbeat-output proof + bounded wording) + +#### Accepted contract + +- reassignment/result convergence on the chosen path is now accepted at the volume-server-side ingress boundary +- stale runtime residue after control truth change is now directly tested as a live drain case +- reported truth is now checked on actual heartbeat output rather than helper-local state +- accepted no-split-truth for `P2` means: + - new engine sender truth is present + - stale runtime residue is gone + - heartbeat output reflects the new truth +- `P2` does not claim a proved live replacement owner in every convergence proof, and does not claim full master-driven failover infrastructure closure + +#### Files changed + +| File | Action | +|------|--------| +| `weed/server/qa_block_convergence_test.go` | New/updated: real reassignment, live stale-owner drain, heartbeat-output convergence, bounded no-split-truth proofs | +| `weed/server/block_recovery.go` | Reused existing `OnBeforeExecute` hook from accepted `P4`; no new `P2` semantic change required | +| `weed/server/volume_server_block.go` | Reused accepted chosen-path ingress/reporting surfaces; no new `P2` semantic change required | + +#### Test inventory (4 P2 tests) + +| Test | Proves | +|------|--------| +| `ReassignmentConvergence_EpochBump` | old sender removed, new sender created on chosen-path reassignment | +| `StaleOwnerRemoval` | live old recovery goroutine is drained during reassignment | +| `ReportedTruth_HeartbeatConverges` | `CollectBlockVolumeHeartbeat()` reflects new replica truth | +| `NoSplitTruth` | engine truth + stale-runtime residue removed + heartbeat truth converge without stale residue | + +#### Residual note + +1. full master-driven failover/gRPC infrastructure closure remains outside accepted `P2` scope +2. bounded repeated-assignment / rebuild-server relisten cleanup remains open for `P3` + +--- + +### P3 Technical Pack + +Date: 2026-04-02 +Goal: close the remaining bounded repeated-assignment / idempotence gap on the chosen path so repeated primary assignments do not emit avoidable rebuild-server relisten noise or ambiguous duplicate runtime side effects + +#### Layer 1: Semantic Core + +##### Problem statement + +`P1` accepted stable identity on the chosen path. +`P2` accepted reassignment/result convergence on the chosen path. + +What still remains open is a bounded idempotence/runtime-cleanup gap during repeated primary assignment: + +1. repeated primary assignment for the same volume can still try to relisten/start rebuild-server state that is already active +2. this produces low-severity warning noise and weakens the claim that unchanged control truth is handled idempotently +3. the remaining issue is not backend execution correctness +4. it is whether repeated chosen-path control truth is applied cleanly enough without duplicate side effects + +`P3` should close that bounded gap without turning into broad runtime polish. + +##### System reality + +1. `ProcessAssignments()` remains the accepted chosen-path local ingress +2. `RecoveryManager` already owns accepted start/cancel/replace semantics and should not be broadly reopened +3. rebuild-server/runtime setup currently still has at least one repeated-assignment relisten warning surface +4. unchanged or effectively unchanged chosen-path truth should be absorbed idempotently rather than re-triggering avoidable side effects + +##### High-level algorithm + +Use this idempotence model: + +1. compare the incoming chosen-path primary assignment with the currently applied live assignment/runtime state for the same volume +2. if the authoritative truth is unchanged in the fields that matter to the live chosen path, treat the update as idempotent refresh, not restart +3. if the authoritative truth changed, apply the existing accepted replacement/update path exactly once +4. do not relaunch rebuild-server/runtime side effects when the server is already correctly active for the same truth +5. keep reporting truth aligned with the accepted current owner after the idempotent path + +##### Pseudo code + +```text +on ProcessAssignments(primary assignment): + load current chosen-path runtime state for volume + + if incoming truth == current authoritative truth: + keep existing rebuild/runtime owner + refresh bookkeeping only if needed + do not relisten or duplicate side effects + return + + if incoming truth changes authoritative runtime inputs: + apply accepted replace/update path once + update reporting truth to the new owner + +after repeated assignment: + there is at most one authoritative live owner + rebuild/runtime side effects are not duplicated + warning-free idempotent steady state is preferred for unchanged truth +``` + +##### State / convergence contract + +`P3` should make these truths explicit: + +1. repeated unchanged chosen-path control truth is not a new event that should restart everything +2. there should remain at most one authoritative live chosen-path owner/runtime side-effect set per volume +3. repeated assignment must not create stale duplicate runtime ownership or avoidable relisten noise +4. `P3` is bounded cleanup on top of accepted `P1`/`P2`, not a reopening of control ingress or backend execution semantics + +##### Reject shapes + +Reject before implementation if the slice: + +1. turns into generic restart/runtime polish unrelated to repeated chosen-path assignment +2. reopens accepted `RecoveryManager` ownership semantics without a concrete repeated-assignment bug +3. claims full production hardening from removal of one warning surface +4. fixes warning text only while duplicate side effects or duplicate ownership are still possible +5. expands into new topology/mode work beyond the chosen path + +#### Layer 2: Execution Core + +##### Current gaps `P3` must close + +1. repeated primary assignment can still attempt rebuild-server relisten/start on an already-active chosen-path volume +2. the current chosen-path tests do not yet prove idempotent steady state for repeated unchanged assignment +3. bounded runtime-side-effect cleanup is still under-specified relative to accepted `P2` convergence + +##### Reuse / update instructions + +1. `weed/server/volume_server_block.go` + - `update in place` + - keep this as the primary chosen-path assignment/runtime application surface + - narrow target: repeated-assignment idempotence and rebuild-server/runtime side effects only + +2. `weed/server/block_recovery.go` + - `reference only` unless repeated-assignment proof exposes a real duplicate-owner bug + - do not reopen accepted serialized ownership semantics casually + +3. `weed/server/qa_block_convergence_test.go` + - `update in place` + - extend chosen-path convergence tests with one repeated-assignment/idempotence proof + +4. `weed/server/*block*_test.go` + - `update in place` only if a more focused live-path idempotence test is cleaner than overloading convergence tests + +5. `weed/storage/blockvol/v2bridge/control.go` + - `reference only` + - no new mapping semantics unless repeated-assignment behavior exposes a true control-truth mismatch + +6. copy guidance + - prefer `update in place` + - no parallel runtime-control path + - no broad refactor whose main value is style cleanup + +##### Validation focus + +Required proofs: + +1. unchanged repeated-assignment proof + - same chosen-path primary assignment applied again does not relaunch duplicate rebuild/runtime side effects + +2. changed-assignment proof + - when control truth really changes, accepted replacement path still executes exactly once + +3. no-duplicate-owner proof + - repeated assignment does not leave two live owners or ambiguous runtime side effects + +4. bounded-reporting proof + - externally visible chosen-path state remains coherent after repeated assignment + +5. boundedness proof + - `P3` closes this residual without reopening accepted `P1`/`P2` or `Phase 09` + +Reject if: + +1. proof shows only warning suppression but not idempotent runtime behavior +2. unchanged repeated assignment still triggers avoidable relisten/start side effects +3. changed assignment breaks accepted `P2` convergence behavior +4. implementation expands into broad hardening or unrelated restart cleanup + +##### Suggested first cut + +1. identify the exact repeated primary-assignment path that still emits rebuild-server relisten noise +2. define the minimal equality/authoritative-truth check for "already active" chosen-path primary state +3. make unchanged repeated assignment idempotent at that application point +4. add one focused live-path test for unchanged repeated assignment and one guard test that real changed assignment still replaces correctly + +##### Assignment For `sw` + +1. Goal + - deliver `P3` bounded repeated-assignment / idempotence cleanup on the chosen path + +2. Required outputs + - one live-path repeated-assignment idempotence proof + - one no-duplicate-side-effect or no-duplicate-owner proof + - one guard proof that changed assignment still uses the accepted replacement path + - short reuse note: + - files updated in place + - files used as references only + - any copied code and why + +3. Hard rules + - do not reopen accepted `Phase 09` backend execution semantics + - do not reopen accepted `P1` stable-identity closure + - do not reopen accepted `P2` reassignment convergence except for a narrow repeated-assignment bug + - keep the slice bounded to repeated-assignment/idempotence cleanup + +##### Assignment For `tester` + +1. Goal + - validate that `P3` closes the bounded repeated-assignment/idempotence gap instead of only silencing logs + +2. Validate + - unchanged repeated assignment is idempotent on the live chosen path + - no duplicate rebuild/runtime side effects remain + - changed assignment still converges through the accepted replacement path + - claims remain bounded to residual cleanup + +3. Reject if + - evidence is log-text-only + - unchanged repeated assignment still relaunches avoidable side effects + - the slice overclaims production hardening or broader control-plane closure + +--- + +### P3 Completion Record + +Date: 2026-04-02 +Status: accepted (rev 2) +Revisions: 2 (rev 1: V1-side relisten suppression only, under-closed chosen-path idempotence; rev 2: full chosen-path V2 + V1 repeated-assignment idempotence with bounded wording) + +#### Accepted contract + +- repeated unchanged chosen-path assignment is now treated as idempotent across the accepted V2 + V1 live path +- unchanged repeated assignment no longer re-enters duplicate V2 orchestrator/recovery work on the chosen path +- corresponding V1 replication/rebuild-server setup for unchanged truth is also skipped +- changed assignment still takes the accepted replacement/update path +- `P3` remains bounded residual cleanup and does not claim broad multi-replica idempotence or general production hardening + +#### Files changed + +| File | Action | +|------|--------| +| `weed/server/volume_server_block.go` | Updated: assignment-key tracking and chosen-path repeated-assignment idempotence at the live ingress point | +| `weed/server/qa_block_idempotence_test.go` | New/updated: repeated unchanged assignment proof, changed-assignment guard, heartbeat coherence after repeated assignment | + +#### Test inventory (3 P3 tests) + +| Test | Proves | +|------|--------| +| `RepeatedAssignment_Idempotent` | repeated identical chosen-path assignment does not grow V2 event count and leaves V1 state unchanged | +| `ChangedAssignment_StillReplaces` | changed assignment still triggers accepted replacement behavior | +| `HeartbeatCoherent_AfterRepeated` | heartbeat output remains coherent after repeated unchanged assignment | + +#### Residual note + +1. `P3` does not close the remaining full master-driven heartbeat/gRPC control-loop proof gap +2. bounded chosen-path master-originated delivery closure remains open for `P4` + +--- + +### P4 Technical Pack + +Date: 2026-04-02 +Goal: close the remaining master-driven heartbeat / gRPC control-loop gap on the chosen path so accepted control truth is proven from master-originated assignment production through live delivery into the volume server and back out through visible state + +#### Layer 1: Semantic Core + +##### Problem statement + +`P1` accepted stable identity and control-truth on the chosen wire shape. +`P2` accepted reassignment/result convergence at the volume-server-side chosen-path ingress boundary. +`P3` accepted bounded repeated-assignment/idempotence cleanup on that same live path. + +What still remains open is the last larger control-plane bound: + +1. accepted proofs still center mainly on the volume-server-side ingestion boundary +2. the fuller master-originated heartbeat / gRPC delivery loop is not yet accepted as a direct one-chain proof target +3. the remaining question is not backend execution or local replacement semantics +4. it is whether real master-produced chosen-path truth reaches the live volume-server path coherently enough through the actual control loop + +`P4` should close that gap without turning `Phase 10` into broad cluster-hardening work. + +##### System reality + +1. master-side assignment production for the chosen path already exists +2. accepted stable-ID wire fields already exist from `P1` +3. accepted VS-side convergence and idempotence already exist from `P2` / `P3` +4. the remaining missing proof is the one-chain link across: + - master-produced assignment truth + - heartbeat / gRPC delivery + - volume-server application + - visible post-delivery state + +##### High-level algorithm + +Use this control-loop model: + +1. master produces authoritative chosen-path assignment truth for one bounded scenario +2. the real heartbeat / gRPC delivery path carries that truth to the target volume server +3. the volume server applies the delivered truth through the already accepted local path +4. runtime/reporting state converge to the same truth +5. repeated heartbeat/control iterations do not silently rewrite that truth into something different + +##### Pseudo code + +```text +on master-side chosen-path state change: + build authoritative assignment using accepted stable identity fields + deliver assignment through the real heartbeat / gRPC path + +on volume-server receipt through the live control loop: + decode and preserve the same chosen-path truth + apply accepted local control/runtime path + +after delivery converges: + engine/runtime/reporting reflect the same master-originated truth + no stale pre-change truth remains visible + repeated delivery of unchanged truth stays bounded under accepted P3 rules +``` + +##### State / convergence contract + +`P4` should make these truths explicit: + +1. master-originated chosen-path assignment truth is now part of the accepted end-to-end proof, not only local ingestion truth +2. the accepted `P1` identity fields must survive the real master -> volume-server delivery loop intact +3. the accepted `P2` convergence properties must still hold when the truth enters through the real master-driven path +4. the accepted `P3` idempotence properties must still hold for repeated unchanged delivery through that same loop +5. `P4` remains bounded to the chosen path and does not claim whole-cluster or product-surface hardening + +##### Reject shapes + +Reject before implementation if the slice: + +1. still stops at direct local `ProcessAssignments()` calls without one real master-driven delivery proof +2. reopens accepted `P1` / `P2` / `P3` semantics as the main story instead of using them as dependencies +3. turns into broad heartbeat-loop refactoring, broad failover redesign, or cluster-hardening work +4. claims full distributed failover robustness beyond one bounded chosen-path control-loop proof +5. proves only transport reachability without proving resulting control/runtime/reporting truth + +#### Layer 2: Execution Core + +##### Current gaps `P4` must close + +1. master-originated chosen-path assignment production is not yet tied to an accepted one-chain proof through the real heartbeat / gRPC delivery loop +2. accepted VS-side control truth is stronger than accepted master-to-VS loop proof +3. there is no accepted direct proof that the post-delivery visible state still matches the same master-originated truth after the real loop runs + +##### Reuse / update instructions + +1. `weed/server/volume_grpc_client_to_master.go` + - `update in place` + - use as the primary delivery-loop surface if proof hooks or deterministic test entry are needed + - keep changes narrow and proof-oriented + +2. `weed/server/master_grpc_server_block.go` + - `reference only` unless one bounded proof bug requires a narrow fix in assignment production + +3. `weed/server/master_block_failover.go` + - `reference only` unless one bounded chosen-path failover proof requires a narrow fix + +4. `weed/server/volume_server_block.go` + - `reference only` unless the real delivery proof exposes a direct mismatch between delivered truth and local application + +5. `weed/server/qa_block_*test.go` + - `update in place` + - add one or two bounded end-to-end control-loop proofs rather than broad scenario explosion + +6. `weed/storage/blockvol/v2bridge/control.go` + - `reference only` + - accepted control mapping from `P1` should remain stable unless the real loop exposes a true mismatch + +7. copy guidance + - prefer `update in place` + - no parallel control loop + - no large mock-only harness that bypasses the actual chosen path + +##### Validation focus + +Required proofs: + +1. master-driven delivery proof + - one real chosen-path assignment generated on the master side reaches the live volume-server path through heartbeat / gRPC delivery + +2. identity preservation proof + - accepted `P1` stable identity survives that real delivery loop intact + +3. post-delivery convergence proof + - accepted `P2` runtime/reporting convergence still holds after real master-driven delivery + +4. repeated-delivery boundedness proof + - accepted `P3` repeated unchanged assignment idempotence still holds when the same truth is redelivered through the real loop + +5. boundedness / no-overclaim proof + - `P4` closes this remaining chosen-path control-loop gap without claiming full cluster-hardening closure + +Reject if: + +1. evidence still begins at manual `ProcessAssignments()` only +2. master-side production is bypassed in the claimed end-to-end proof +3. identity/convergence/idempotence accepted in `P1` / `P2` / `P3` do not survive the fuller loop +4. implementation expands into unrelated cluster/product work + +##### Suggested first cut + +1. choose one bounded master-driven chosen-path scenario: + - create-path assignment delivery, or + - one bounded failover/reassignment delivery +2. expose the narrowest deterministic proof seam in the real heartbeat / gRPC loop +3. prove delivered truth arrives intact at the live volume-server path +4. prove post-delivery runtime/reporting truth matches the same master-originated truth +5. prove repeated unchanged delivery stays bounded under accepted `P3` + +##### Assignment For `sw` + +1. Goal + - deliver `P4` master-driven heartbeat / gRPC control-loop closure on the chosen path + +2. Required outputs + - one real master-originated delivery proof + - one identity-preservation-through-delivery proof + - one post-delivery convergence/reporting proof + - one repeated-unchanged-delivery boundedness proof if the same loop naturally redelivers truth + - short reuse note: + - files updated in place + - files used as references only + - any copied code and why + +3. Hard rules + - do not reopen accepted `Phase 09` backend execution semantics + - do not reopen accepted `P1` / `P2` / `P3` semantics except for narrow proof-exposed bugs + - keep `P4` bounded to one real chosen-path master/heartbeat/gRPC control-loop proof package + - do not let `P4` turn into broad cluster-hardening or product-surface work + +##### Assignment For `tester` + +1. Goal + - validate that `P4` closes the remaining master-driven chosen-path control-loop proof gap rather than only adding new helper-level plumbing + +2. Validate + - real master-originated chosen-path delivery + - stable-ID preservation through that loop + - post-delivery runtime/reporting convergence + - bounded repeated-delivery/idempotence behavior if applicable + +3. Reject if + - evidence still starts at local ingestion only + - master-side truth is not part of the claimed proof chain + - the slice overclaims full cluster-hardening or product readiness + +--- + +### P4 Completion Record + +Date: 2026-04-02 +Status: accepted (rev 5 + corrected adversarial validation) +Revisions: 5 (rev 1-4 progressively expanded real master-produced delivery, promoted-VS proof, and bounded idempotence; rev 5 closed separate promoted-VS proof and full-set idempotence; corrected adversarial validation confirmed existing epoch-regression protection on the promoted volume) + +#### Accepted contract + +- real master-produced chosen-path assignment truth is now part of the accepted one-chain control proof +- accepted create-path proof now covers: + - real master assignment production + - real queue/proto preservation + - real VS application + - visible epoch/heartbeat convergence +- accepted failover-path proof now covers: + - real master failover assignment production + - delivery into a separate promoted volume-server surface + - promoted volume epoch convergence + - promoted volume-server heartbeat convergence +- accepted `P1` stable identity survives the master-originated delivery path +- accepted `P3` repeated unchanged delivery remains bounded/idempotent on the proven slice +- corrected adversarial validation confirms stale lower-epoch delivery to the promoted volume is rejected by the existing `ErrEpochRegression` guard; no new V1 bug was confirmed +- `P4` remains bounded chosen-path control-loop closure and does not claim full live transport-stream deployment proof, broad cluster hardening, or product-surface readiness + +#### Files changed + +| File | Action | +|------|--------| +| `weed/server/qa_block_control_loop_test.go` | New/updated: real master-produced create/failover delivery, separate promoted-VS proof, identity preservation, bounded repeated-delivery proof | +| `weed/server/qa_block_control_loop_adversarial_test.go` | New/updated: corrected promoted-volume stale-epoch adversarial guard | +| `weed/server/volume_server_block.go` | Updated: bounded repeated-delivery idempotence on the live path, including full-set comparison for multi-replica assignment equality | + +#### Test inventory (5 tests) + +| Test | Proves | +|------|--------| +| `TestP10P4_MasterCreate_FullDelivery` | real master-produced create assignment reaches live VS apply and heartbeat truth | +| `TestP10P4_MasterFailover_Convergence` | real failover assignment reaches separate promoted VS, and promoted epoch/heartbeat converge | +| `TestP10P4_IdentityPreservation` | stable identity survives master-produced delivery without address-derived fallback | +| `TestP10P4_RepeatedDelivery_Idempotence` | repeated unchanged master-produced delivery remains bounded/idempotent on the proven slice | +| `TestAdversarial_P10P4_StaleEpochAfterFailover` | corrected promoted-volume stale lower-epoch delivery is rejected; epoch does not regress | + +#### Residual note + +1. `P4` is accepted as bounded chosen-path control-loop closure, not as whole-cluster or full live transport-stream deployment proof +2. `Phase 10` is now closed +3. the next planned phase is `Phase 11` product-surface rebinding diff --git a/sw-block/.private/phase/phase-10.md b/sw-block/.private/phase/phase-10.md new file mode 100644 index 000000000..efab7aeeb --- /dev/null +++ b/sw-block/.private/phase/phase-10.md @@ -0,0 +1,228 @@ +# Phase 10 + +Date: 2026-04-02 +Status: complete +Purpose: close the main end-to-end control-plane gaps on the chosen `RF=2 sync_all` path without reopening accepted backend execution semantics + +## Why This Phase Exists + +`Phase 09` closed the main backend execution gaps on the chosen path: + +1. real `TransferFullBase` +2. real `TransferSnapshot` +3. real `TruncateWAL` under the accepted narrowed contract +4. stronger live runtime ownership on the volume-server path + +What still does not exist is stronger end-to-end control-plane closure. + +The main remaining gap is no longer: + +1. whether the backend execution path is real + +It is now: + +1. whether the real control path drives and reflects the chosen path coherently enough for product use + +## Phase Goal + +Strengthen from accepted assignment-entry closure to stronger end-to-end control-plane closure on the chosen path. + +## Scope + +### In scope + +1. heartbeat / gRPC-level control delivery proof on the chosen path +2. reassignment / failover result convergence through the real control path +3. cleaner local identity than transport-shaped `listenAddr` +4. bounded idempotence / repeated-assignment cleanup when it directly affects live control/runtime ownership + +### Out of scope + +1. reopening accepted `P1` / `P2` / `P3` / `P4` backend execution semantics +2. `RF>2` +3. `best_effort` / `sync_quorum` +4. product-surface rebinding (`CSI` / `NVMe` / `iSCSI`) +5. broad performance optimization + +## Phase 10 Items + +### P0: Control-Plane Closure Plan + +Goal: + +- start `Phase 10` with one substantial control-plane closure package, not a loose collection of follow-up fixes + +Must prove: + +1. the phase is centered on real control-path closure rather than backend execution rework +2. the required closure targets are explicit: + - heartbeat / gRPC delivery + - reassignment / result convergence + - identity cleanup + - bounded repeated-assignment/idempotence cleanup +3. the chosen-path bound remains explicit + +Verification mechanism: + +1. architect review: + - control-plane scope is explicit and bounded + - proposed slices do not reopen accepted backend execution semantics +2. tester review: + - required end-to-end proofs are explicit +3. manager review: + - the package is concrete enough to assign the first implementation slice + +Output artifacts: + +1. explicit control-plane closure targets +2. explicit reject shapes +3. initial slice order inside `Phase 10` + +Execution note: + +- use `phase-10-log.md` as the technical pack for: + - semantic scope + - execution scope + - proof shapes + - assignment templates for `sw` and `tester` + +Reject if: + +1. `Phase 10` is framed as a vague "polish/control" phase without concrete closure targets +2. accepted `Phase 09` execution semantics are quietly reopened +3. product surfaces or unrelated hardening work are absorbed into this phase +4. no explicit end-to-end proof shape is defined + +Status: + +- accepted + +### P1: Identity And Control-Truth Closure + +Goal: + +- close stable identity on the real chosen-path control wire so assignment truth, local ingest truth, and `ReplicaID` construction no longer depend on transport-shaped fallback + +Accepted scope: + +1. stable server identity preserved on the block assignment proto wire +2. master assignment generation preserves stable identity on the chosen path +3. volume-server local identity uses the same canonical server identity as the main volume server +4. real ingress proof: + - proto/decode + - `ProcessAssignments()` + - `ControlBridge` + - engine sender identity +5. fail-closed behavior for missing stable identity + +Status: + +- accepted + +Carry-forward from `P1`: + +1. fuller reassignment / failover result convergence is still open +2. broader control-plane reporting closure is still open + +### P2: Reassignment / Result Convergence + +Goal: + +- prove that reassignment and failover converge through the real control path without stale local ownership or stale reported truth lingering after control truth changes + +Accepted scope: + +1. real failover / reassignment convergence through the chosen control path +2. no stale local runtime owner after control truth changes +3. no stale control/reporting truth after reassignment +4. one-chain proof through the real control path, not only local helper logic +5. no overclaim of broader hardening or product-surface closure + +Status: + +- accepted + +Carry-forward from `P2`: + +1. `P2` proves stale owner removal and no stale residue after control truth changes +2. bounded repeated-assignment/idempotence cleanup is still open where repeated primary assignment can still emit rebuild-server relisten warnings +3. `P2` does not claim broad master-driven failover infrastructure closure beyond the accepted volume-server-side chosen-path ingress + +### P3: Bounded Repeated-Assignment / Idempotence Cleanup + +Goal: + +- close the remaining low-severity repeated-assignment/runtime-idempotence gap on the chosen path so duplicate or replacement primary assignments do not leave avoidable relisten/restart noise or ambiguous live-control ownership + +Accepted scope: + +1. repeated primary assignment on the same chosen-path volume should converge idempotently +2. rebuild-server/runtime side effects should not relaunch noisily when the authoritative control truth is unchanged or already active +3. bounded proof that repeated-assignment cleanup does not reopen accepted `P2` convergence or accepted `Phase 09` execution semantics +4. no expansion into broad runtime polish, product surfaces, or unrelated restart hardening + +Status: + +- accepted + +Carry-forward from `P3`: + +1. chosen-path repeated unchanged assignment is now absorbed idempotently across the accepted V2 + V1 live path +2. `P3` remains bounded cleanup; it does not itself close the remaining master-driven heartbeat/gRPC control-loop gap +3. fuller master-originated control delivery proof is still open + +### P4: Master-Driven Control-Loop Closure + +Goal: + +- close the remaining chosen-path control-plane gap by proving that master-originated assignment truth delivered through the real heartbeat / gRPC control loop reaches the live volume-server path and converges without split truth + +Required scope: + +1. one bounded end-to-end proof from real master-produced chosen-path assignment truth into the live volume-server control path +2. proof that the real heartbeat / gRPC delivery path preserves the already accepted identity and convergence properties +3. proof that externally visible post-delivery state reflects the same new truth after the real master-driven path runs +4. no reopening of accepted `P1` / `P2` / `P3` semantics except for narrow bugs directly exposed by the fuller control-loop proof +5. no expansion into product surfaces, `RF>2`, or broad cluster-hardening work + +Status: + +- accepted + +Carry-forward from `P4`: + +1. bounded chosen-path master-driven heartbeat / gRPC control-loop closure is now accepted +2. `P4` does not claim full live transport-stream deployment proof or broad product hardening +3. the next phase should move to `Phase 11` product-surface rebinding + +### Planned slice direction after `P0` + +1. `P1`: + - identity and control-truth closure on the live control path +2. `P2`: + - reassignment / failover result convergence through the real control path +3. `P3`: + - bounded idempotence / repeated-assignment cleanup after accepted `P1` / `P2` +4. `P4`: + - master-driven heartbeat / gRPC control-loop closure on the chosen path + +## Assignment For `sw` + +Current next tasks: + +1. treat `Phase 10` as closed and keep accepted `P1` / `P2` / `P3` / `P4` semantics stable +2. start `Phase 11` product-surface rebinding from `v2-phase-development-plan.md` +3. keep the first `Phase 11` slice bounded to selected product surfaces rather than broad hardening +4. do not reopen accepted backend execution or control-plane closure except for narrow bug fixes + +## Assignment For `tester` + +Current next tasks: + +1. treat `P4` as accepted bounded control-loop closure on the chosen path +2. validate the first `Phase 11` slice as bounded product-surface rebinding rather than renewed control-plane work +3. keep no-overclaim active around: + - accepted `Phase 09` execution closure + - accepted `Phase 10` control-plane closure + - selected `Phase 11` surface scope vs broader product readiness + - chosen path vs future paths/modes diff --git a/weed/command/volume.go b/weed/command/volume.go index a5bf8b3e7..91b95cd33 100644 --- a/weed/command/volume.go +++ b/weed/command/volume.go @@ -343,6 +343,10 @@ func (v VolumeServerOptions) startVolumeServer(volumeFolders, maxVolumeCounts, v } blockService := weed_server.StartBlockService(*v.blockListen, *v.blockDir, *v.blockIQNPrefix, blockPortal, nvmeCfg) if blockService != nil { + // V2 P10-1: Use the same canonical volume server identity as the main + // volume server. One identity truth across VS, block service, control + // bridge, and runtime ownership. + blockService.SetServerID(volumeServerId) volumeServer.SetBlockService(blockService) } diff --git a/weed/pb/master.proto b/weed/pb/master.proto index 99dd97ef0..22ecf5eea 100644 --- a/weed/pb/master.proto +++ b/weed/pb/master.proto @@ -69,6 +69,8 @@ service Seaweed { } rpc ListBlockSnapshots (ListBlockSnapshotsRequest) returns (ListBlockSnapshotsResponse) { } + rpc RestoreBlockSnapshot (RestoreBlockSnapshotRequest) returns (RestoreBlockSnapshotResponse) { + } rpc ExpandBlockVolume (ExpandBlockVolumeRequest) returns (ExpandBlockVolumeResponse) { } } @@ -528,11 +530,13 @@ message BlockVolumeAssignment { string replica_ctrl_addr = 6; string rebuild_addr = 7; repeated ReplicaAddrMessage replica_addrs = 8; + string replica_server_id = 9; // V2: stable server identity for scalar replica } message ReplicaAddrMessage { string data_addr = 1; string ctrl_addr = 2; + string server_id = 3; // V2: stable server identity for this replica } message CreateBlockVolumeRequest { @@ -605,6 +609,13 @@ message BlockSnapshotInfo { uint64 volume_size_bytes = 3; } +message RestoreBlockSnapshotRequest { + string volume_name = 1; + uint32 snapshot_id = 2; +} +message RestoreBlockSnapshotResponse { +} + message ExpandBlockVolumeRequest { string name = 1; uint64 new_size_bytes = 2; diff --git a/weed/pb/master_pb/master.pb.go b/weed/pb/master_pb/master.pb.go index 6c330a8b5..295e23fe5 100644 --- a/weed/pb/master_pb/master.pb.go +++ b/weed/pb/master_pb/master.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v5.29.5 +// protoc v3.21.12 // source: master.proto package master_pb @@ -1773,7 +1773,9 @@ func (x *StatisticsResponse) GetFileCount() uint64 { return 0 } +// // collection related +// type Collection struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -1994,7 +1996,9 @@ func (*CollectionDeleteResponse) Descriptor() ([]byte, []int) { return file_master_proto_rawDescGZIP(), []int{24} } +// // volume related +// type DiskInfo struct { state protoimpl.MessageState `protogen:"open.v1"` Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` @@ -4149,6 +4153,7 @@ type BlockVolumeAssignment struct { ReplicaCtrlAddr string `protobuf:"bytes,6,opt,name=replica_ctrl_addr,json=replicaCtrlAddr,proto3" json:"replica_ctrl_addr,omitempty"` RebuildAddr string `protobuf:"bytes,7,opt,name=rebuild_addr,json=rebuildAddr,proto3" json:"rebuild_addr,omitempty"` ReplicaAddrs []*ReplicaAddrMessage `protobuf:"bytes,8,rep,name=replica_addrs,json=replicaAddrs,proto3" json:"replica_addrs,omitempty"` + ReplicaServerId string `protobuf:"bytes,9,opt,name=replica_server_id,json=replicaServerId,proto3" json:"replica_server_id,omitempty"` // V2: stable server identity for scalar replica unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4239,10 +4244,18 @@ func (x *BlockVolumeAssignment) GetReplicaAddrs() []*ReplicaAddrMessage { return nil } +func (x *BlockVolumeAssignment) GetReplicaServerId() string { + if x != nil { + return x.ReplicaServerId + } + return "" +} + type ReplicaAddrMessage struct { state protoimpl.MessageState `protogen:"open.v1"` DataAddr string `protobuf:"bytes,1,opt,name=data_addr,json=dataAddr,proto3" json:"data_addr,omitempty"` CtrlAddr string `protobuf:"bytes,2,opt,name=ctrl_addr,json=ctrlAddr,proto3" json:"ctrl_addr,omitempty"` + ServerId string `protobuf:"bytes,3,opt,name=server_id,json=serverId,proto3" json:"server_id,omitempty"` // V2: stable server identity for this replica unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4291,6 +4304,13 @@ func (x *ReplicaAddrMessage) GetCtrlAddr() string { return "" } +func (x *ReplicaAddrMessage) GetServerId() string { + if x != nil { + return x.ServerId + } + return "" +} + type CreateBlockVolumeRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -5063,6 +5083,94 @@ func (x *BlockSnapshotInfo) GetVolumeSizeBytes() uint64 { return 0 } +type RestoreBlockSnapshotRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + VolumeName string `protobuf:"bytes,1,opt,name=volume_name,json=volumeName,proto3" json:"volume_name,omitempty"` + SnapshotId uint32 `protobuf:"varint,2,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RestoreBlockSnapshotRequest) Reset() { + *x = RestoreBlockSnapshotRequest{} + mi := &file_master_proto_msgTypes[78] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RestoreBlockSnapshotRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RestoreBlockSnapshotRequest) ProtoMessage() {} + +func (x *RestoreBlockSnapshotRequest) ProtoReflect() protoreflect.Message { + mi := &file_master_proto_msgTypes[78] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RestoreBlockSnapshotRequest.ProtoReflect.Descriptor instead. +func (*RestoreBlockSnapshotRequest) Descriptor() ([]byte, []int) { + return file_master_proto_rawDescGZIP(), []int{78} +} + +func (x *RestoreBlockSnapshotRequest) GetVolumeName() string { + if x != nil { + return x.VolumeName + } + return "" +} + +func (x *RestoreBlockSnapshotRequest) GetSnapshotId() uint32 { + if x != nil { + return x.SnapshotId + } + return 0 +} + +type RestoreBlockSnapshotResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RestoreBlockSnapshotResponse) Reset() { + *x = RestoreBlockSnapshotResponse{} + mi := &file_master_proto_msgTypes[79] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RestoreBlockSnapshotResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RestoreBlockSnapshotResponse) ProtoMessage() {} + +func (x *RestoreBlockSnapshotResponse) ProtoReflect() protoreflect.Message { + mi := &file_master_proto_msgTypes[79] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RestoreBlockSnapshotResponse.ProtoReflect.Descriptor instead. +func (*RestoreBlockSnapshotResponse) Descriptor() ([]byte, []int) { + return file_master_proto_rawDescGZIP(), []int{79} +} + type ExpandBlockVolumeRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -5073,7 +5181,7 @@ type ExpandBlockVolumeRequest struct { func (x *ExpandBlockVolumeRequest) Reset() { *x = ExpandBlockVolumeRequest{} - mi := &file_master_proto_msgTypes[78] + mi := &file_master_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5085,7 +5193,7 @@ func (x *ExpandBlockVolumeRequest) String() string { func (*ExpandBlockVolumeRequest) ProtoMessage() {} func (x *ExpandBlockVolumeRequest) ProtoReflect() protoreflect.Message { - mi := &file_master_proto_msgTypes[78] + mi := &file_master_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5098,7 +5206,7 @@ func (x *ExpandBlockVolumeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExpandBlockVolumeRequest.ProtoReflect.Descriptor instead. func (*ExpandBlockVolumeRequest) Descriptor() ([]byte, []int) { - return file_master_proto_rawDescGZIP(), []int{78} + return file_master_proto_rawDescGZIP(), []int{80} } func (x *ExpandBlockVolumeRequest) GetName() string { @@ -5124,7 +5232,7 @@ type ExpandBlockVolumeResponse struct { func (x *ExpandBlockVolumeResponse) Reset() { *x = ExpandBlockVolumeResponse{} - mi := &file_master_proto_msgTypes[79] + mi := &file_master_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5136,7 +5244,7 @@ func (x *ExpandBlockVolumeResponse) String() string { func (*ExpandBlockVolumeResponse) ProtoMessage() {} func (x *ExpandBlockVolumeResponse) ProtoReflect() protoreflect.Message { - mi := &file_master_proto_msgTypes[79] + mi := &file_master_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5149,7 +5257,7 @@ func (x *ExpandBlockVolumeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ExpandBlockVolumeResponse.ProtoReflect.Descriptor instead. func (*ExpandBlockVolumeResponse) Descriptor() ([]byte, []int) { - return file_master_proto_rawDescGZIP(), []int{79} + return file_master_proto_rawDescGZIP(), []int{81} } func (x *ExpandBlockVolumeResponse) GetCapacityBytes() uint64 { @@ -5170,7 +5278,7 @@ type SuperBlockExtra_ErasureCoding struct { func (x *SuperBlockExtra_ErasureCoding) Reset() { *x = SuperBlockExtra_ErasureCoding{} - mi := &file_master_proto_msgTypes[82] + mi := &file_master_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5182,7 +5290,7 @@ func (x *SuperBlockExtra_ErasureCoding) String() string { func (*SuperBlockExtra_ErasureCoding) ProtoMessage() {} func (x *SuperBlockExtra_ErasureCoding) ProtoReflect() protoreflect.Message { - mi := &file_master_proto_msgTypes[82] + mi := &file_master_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5231,7 +5339,7 @@ type LookupVolumeResponse_VolumeIdLocation struct { func (x *LookupVolumeResponse_VolumeIdLocation) Reset() { *x = LookupVolumeResponse_VolumeIdLocation{} - mi := &file_master_proto_msgTypes[83] + mi := &file_master_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5243,7 +5351,7 @@ func (x *LookupVolumeResponse_VolumeIdLocation) String() string { func (*LookupVolumeResponse_VolumeIdLocation) ProtoMessage() {} func (x *LookupVolumeResponse_VolumeIdLocation) ProtoReflect() protoreflect.Message { - mi := &file_master_proto_msgTypes[83] + mi := &file_master_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5297,7 +5405,7 @@ type LookupEcVolumeResponse_EcShardIdLocation struct { func (x *LookupEcVolumeResponse_EcShardIdLocation) Reset() { *x = LookupEcVolumeResponse_EcShardIdLocation{} - mi := &file_master_proto_msgTypes[88] + mi := &file_master_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5309,7 +5417,7 @@ func (x *LookupEcVolumeResponse_EcShardIdLocation) String() string { func (*LookupEcVolumeResponse_EcShardIdLocation) ProtoMessage() {} func (x *LookupEcVolumeResponse_EcShardIdLocation) ProtoReflect() protoreflect.Message { - mi := &file_master_proto_msgTypes[88] + mi := &file_master_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5352,7 +5460,7 @@ type ListClusterNodesResponse_ClusterNode struct { func (x *ListClusterNodesResponse_ClusterNode) Reset() { *x = ListClusterNodesResponse_ClusterNode{} - mi := &file_master_proto_msgTypes[89] + mi := &file_master_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5364,7 +5472,7 @@ func (x *ListClusterNodesResponse_ClusterNode) String() string { func (*ListClusterNodesResponse_ClusterNode) ProtoMessage() {} func (x *ListClusterNodesResponse_ClusterNode) ProtoReflect() protoreflect.Message { - mi := &file_master_proto_msgTypes[89] + mi := &file_master_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5427,7 +5535,7 @@ type RaftListClusterServersResponse_ClusterServers struct { func (x *RaftListClusterServersResponse_ClusterServers) Reset() { *x = RaftListClusterServersResponse_ClusterServers{} - mi := &file_master_proto_msgTypes[90] + mi := &file_master_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5439,7 +5547,7 @@ func (x *RaftListClusterServersResponse_ClusterServers) String() string { func (*RaftListClusterServersResponse_ClusterServers) ProtoMessage() {} func (x *RaftListClusterServersResponse_ClusterServers) ProtoReflect() protoreflect.Message { - mi := &file_master_proto_msgTypes[90] + mi := &file_master_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5885,7 +5993,7 @@ const file_master_proto_rawDesc = "" + "volumeSize\x12\x1d\n" + "\n" + "block_size\x18\x03 \x01(\rR\tblockSize\x12\x1b\n" + - "\tdisk_type\x18\x04 \x01(\tR\bdiskType\"\xb6\x02\n" + + "\tdisk_type\x18\x04 \x01(\tR\bdiskType\"\xe2\x02\n" + "\x15BlockVolumeAssignment\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x14\n" + "\x05epoch\x18\x02 \x01(\x04R\x05epoch\x12\x12\n" + @@ -5895,10 +6003,12 @@ const file_master_proto_rawDesc = "" + "\x11replica_data_addr\x18\x05 \x01(\tR\x0freplicaDataAddr\x12*\n" + "\x11replica_ctrl_addr\x18\x06 \x01(\tR\x0freplicaCtrlAddr\x12!\n" + "\frebuild_addr\x18\a \x01(\tR\vrebuildAddr\x12B\n" + - "\rreplica_addrs\x18\b \x03(\v2\x1d.master_pb.ReplicaAddrMessageR\freplicaAddrs\"N\n" + + "\rreplica_addrs\x18\b \x03(\v2\x1d.master_pb.ReplicaAddrMessageR\freplicaAddrs\x12*\n" + + "\x11replica_server_id\x18\t \x01(\tR\x0freplicaServerId\"k\n" + "\x12ReplicaAddrMessage\x12\x1b\n" + "\tdata_addr\x18\x01 \x01(\tR\bdataAddr\x12\x1b\n" + - "\tctrl_addr\x18\x02 \x01(\tR\bctrlAddr\"\xba\x01\n" + + "\tctrl_addr\x18\x02 \x01(\tR\bctrlAddr\x12\x1b\n" + + "\tserver_id\x18\x03 \x01(\tR\bserverId\"\xba\x01\n" + "\x18CreateBlockVolumeRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" + "\n" + @@ -5963,12 +6073,18 @@ const file_master_proto_rawDesc = "" + "snapshotId\x12\x1d\n" + "\n" + "created_at\x18\x02 \x01(\x03R\tcreatedAt\x12*\n" + - "\x11volume_size_bytes\x18\x03 \x01(\x04R\x0fvolumeSizeBytes\"T\n" + + "\x11volume_size_bytes\x18\x03 \x01(\x04R\x0fvolumeSizeBytes\"_\n" + + "\x1bRestoreBlockSnapshotRequest\x12\x1f\n" + + "\vvolume_name\x18\x01 \x01(\tR\n" + + "volumeName\x12\x1f\n" + + "\vsnapshot_id\x18\x02 \x01(\rR\n" + + "snapshotId\"\x1e\n" + + "\x1cRestoreBlockSnapshotResponse\"T\n" + "\x18ExpandBlockVolumeRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12$\n" + "\x0enew_size_bytes\x18\x02 \x01(\x04R\fnewSizeBytes\"B\n" + "\x19ExpandBlockVolumeResponse\x12%\n" + - "\x0ecapacity_bytes\x18\x01 \x01(\x04R\rcapacityBytes2\x83\x16\n" + + "\x0ecapacity_bytes\x18\x01 \x01(\x04R\rcapacityBytes2\xee\x16\n" + "\aSeaweed\x12I\n" + "\rSendHeartbeat\x12\x14.master_pb.Heartbeat\x1a\x1c.master_pb.HeartbeatResponse\"\x00(\x010\x01\x12X\n" + "\rKeepConnected\x12\x1f.master_pb.KeepConnectedRequest\x1a .master_pb.KeepConnectedResponse\"\x00(\x010\x01\x12Q\n" + @@ -6002,7 +6118,8 @@ const file_master_proto_rawDesc = "" + "\x11LookupBlockVolume\x12#.master_pb.LookupBlockVolumeRequest\x1a$.master_pb.LookupBlockVolumeResponse\"\x00\x12f\n" + "\x13CreateBlockSnapshot\x12%.master_pb.CreateBlockSnapshotRequest\x1a&.master_pb.CreateBlockSnapshotResponse\"\x00\x12f\n" + "\x13DeleteBlockSnapshot\x12%.master_pb.DeleteBlockSnapshotRequest\x1a&.master_pb.DeleteBlockSnapshotResponse\"\x00\x12c\n" + - "\x12ListBlockSnapshots\x12$.master_pb.ListBlockSnapshotsRequest\x1a%.master_pb.ListBlockSnapshotsResponse\"\x00\x12`\n" + + "\x12ListBlockSnapshots\x12$.master_pb.ListBlockSnapshotsRequest\x1a%.master_pb.ListBlockSnapshotsResponse\"\x00\x12i\n" + + "\x14RestoreBlockSnapshot\x12&.master_pb.RestoreBlockSnapshotRequest\x1a'.master_pb.RestoreBlockSnapshotResponse\"\x00\x12`\n" + "\x11ExpandBlockVolume\x12#.master_pb.ExpandBlockVolumeRequest\x1a$.master_pb.ExpandBlockVolumeResponse\"\x00B2Z0github.com/seaweedfs/seaweedfs/weed/pb/master_pbb\x06proto3" var ( @@ -6017,7 +6134,7 @@ func file_master_proto_rawDescGZIP() []byte { return file_master_proto_rawDescData } -var file_master_proto_msgTypes = make([]protoimpl.MessageInfo, 91) +var file_master_proto_msgTypes = make([]protoimpl.MessageInfo, 93) var file_master_proto_goTypes = []any{ (*Heartbeat)(nil), // 0: master_pb.Heartbeat (*HeartbeatResponse)(nil), // 1: master_pb.HeartbeatResponse @@ -6097,20 +6214,22 @@ var file_master_proto_goTypes = []any{ (*ListBlockSnapshotsRequest)(nil), // 75: master_pb.ListBlockSnapshotsRequest (*ListBlockSnapshotsResponse)(nil), // 76: master_pb.ListBlockSnapshotsResponse (*BlockSnapshotInfo)(nil), // 77: master_pb.BlockSnapshotInfo - (*ExpandBlockVolumeRequest)(nil), // 78: master_pb.ExpandBlockVolumeRequest - (*ExpandBlockVolumeResponse)(nil), // 79: master_pb.ExpandBlockVolumeResponse - nil, // 80: master_pb.Heartbeat.MaxVolumeCountsEntry - nil, // 81: master_pb.StorageBackend.PropertiesEntry - (*SuperBlockExtra_ErasureCoding)(nil), // 82: master_pb.SuperBlockExtra.ErasureCoding - (*LookupVolumeResponse_VolumeIdLocation)(nil), // 83: master_pb.LookupVolumeResponse.VolumeIdLocation - nil, // 84: master_pb.DataNodeInfo.DiskInfosEntry - nil, // 85: master_pb.RackInfo.DiskInfosEntry - nil, // 86: master_pb.DataCenterInfo.DiskInfosEntry - nil, // 87: master_pb.TopologyInfo.DiskInfosEntry - (*LookupEcVolumeResponse_EcShardIdLocation)(nil), // 88: master_pb.LookupEcVolumeResponse.EcShardIdLocation - (*ListClusterNodesResponse_ClusterNode)(nil), // 89: master_pb.ListClusterNodesResponse.ClusterNode - (*RaftListClusterServersResponse_ClusterServers)(nil), // 90: master_pb.RaftListClusterServersResponse.ClusterServers - (*volume_server_pb.VolumeServerState)(nil), // 91: volume_server_pb.VolumeServerState + (*RestoreBlockSnapshotRequest)(nil), // 78: master_pb.RestoreBlockSnapshotRequest + (*RestoreBlockSnapshotResponse)(nil), // 79: master_pb.RestoreBlockSnapshotResponse + (*ExpandBlockVolumeRequest)(nil), // 80: master_pb.ExpandBlockVolumeRequest + (*ExpandBlockVolumeResponse)(nil), // 81: master_pb.ExpandBlockVolumeResponse + nil, // 82: master_pb.Heartbeat.MaxVolumeCountsEntry + nil, // 83: master_pb.StorageBackend.PropertiesEntry + (*SuperBlockExtra_ErasureCoding)(nil), // 84: master_pb.SuperBlockExtra.ErasureCoding + (*LookupVolumeResponse_VolumeIdLocation)(nil), // 85: master_pb.LookupVolumeResponse.VolumeIdLocation + nil, // 86: master_pb.DataNodeInfo.DiskInfosEntry + nil, // 87: master_pb.RackInfo.DiskInfosEntry + nil, // 88: master_pb.DataCenterInfo.DiskInfosEntry + nil, // 89: master_pb.TopologyInfo.DiskInfosEntry + (*LookupEcVolumeResponse_EcShardIdLocation)(nil), // 90: master_pb.LookupEcVolumeResponse.EcShardIdLocation + (*ListClusterNodesResponse_ClusterNode)(nil), // 91: master_pb.ListClusterNodesResponse.ClusterNode + (*RaftListClusterServersResponse_ClusterServers)(nil), // 92: master_pb.RaftListClusterServersResponse.ClusterServers + (*volume_server_pb.VolumeServerState)(nil), // 93: volume_server_pb.VolumeServerState } var file_master_proto_depIdxs = []int32{ 2, // 0: master_pb.Heartbeat.volumes:type_name -> master_pb.VolumeInformationMessage @@ -6119,35 +6238,35 @@ var file_master_proto_depIdxs = []int32{ 4, // 3: master_pb.Heartbeat.ec_shards:type_name -> master_pb.VolumeEcShardInformationMessage 4, // 4: master_pb.Heartbeat.new_ec_shards:type_name -> master_pb.VolumeEcShardInformationMessage 4, // 5: master_pb.Heartbeat.deleted_ec_shards:type_name -> master_pb.VolumeEcShardInformationMessage - 80, // 6: master_pb.Heartbeat.max_volume_counts:type_name -> master_pb.Heartbeat.MaxVolumeCountsEntry - 91, // 7: master_pb.Heartbeat.state:type_name -> volume_server_pb.VolumeServerState + 82, // 6: master_pb.Heartbeat.max_volume_counts:type_name -> master_pb.Heartbeat.MaxVolumeCountsEntry + 93, // 7: master_pb.Heartbeat.state:type_name -> volume_server_pb.VolumeServerState 61, // 8: master_pb.Heartbeat.block_volume_infos:type_name -> master_pb.BlockVolumeInfoMessage 62, // 9: master_pb.Heartbeat.new_block_volumes:type_name -> master_pb.BlockVolumeShortInfoMessage 62, // 10: master_pb.Heartbeat.deleted_block_volumes:type_name -> master_pb.BlockVolumeShortInfoMessage 5, // 11: master_pb.HeartbeatResponse.storage_backends:type_name -> master_pb.StorageBackend 63, // 12: master_pb.HeartbeatResponse.block_volume_assignments:type_name -> master_pb.BlockVolumeAssignment - 81, // 13: master_pb.StorageBackend.properties:type_name -> master_pb.StorageBackend.PropertiesEntry - 82, // 14: master_pb.SuperBlockExtra.erasure_coding:type_name -> master_pb.SuperBlockExtra.ErasureCoding + 83, // 13: master_pb.StorageBackend.properties:type_name -> master_pb.StorageBackend.PropertiesEntry + 84, // 14: master_pb.SuperBlockExtra.erasure_coding:type_name -> master_pb.SuperBlockExtra.ErasureCoding 9, // 15: master_pb.KeepConnectedResponse.volume_location:type_name -> master_pb.VolumeLocation 10, // 16: master_pb.KeepConnectedResponse.cluster_node_update:type_name -> master_pb.ClusterNodeUpdate - 83, // 17: master_pb.LookupVolumeResponse.volume_id_locations:type_name -> master_pb.LookupVolumeResponse.VolumeIdLocation + 85, // 17: master_pb.LookupVolumeResponse.volume_id_locations:type_name -> master_pb.LookupVolumeResponse.VolumeIdLocation 14, // 18: master_pb.AssignResponse.replicas:type_name -> master_pb.Location 14, // 19: master_pb.AssignResponse.location:type_name -> master_pb.Location 20, // 20: master_pb.CollectionListResponse.collections:type_name -> master_pb.Collection 2, // 21: master_pb.DiskInfo.volume_infos:type_name -> master_pb.VolumeInformationMessage 4, // 22: master_pb.DiskInfo.ec_shard_infos:type_name -> master_pb.VolumeEcShardInformationMessage - 84, // 23: master_pb.DataNodeInfo.diskInfos:type_name -> master_pb.DataNodeInfo.DiskInfosEntry + 86, // 23: master_pb.DataNodeInfo.diskInfos:type_name -> master_pb.DataNodeInfo.DiskInfosEntry 26, // 24: master_pb.RackInfo.data_node_infos:type_name -> master_pb.DataNodeInfo - 85, // 25: master_pb.RackInfo.diskInfos:type_name -> master_pb.RackInfo.DiskInfosEntry + 87, // 25: master_pb.RackInfo.diskInfos:type_name -> master_pb.RackInfo.DiskInfosEntry 27, // 26: master_pb.DataCenterInfo.rack_infos:type_name -> master_pb.RackInfo - 86, // 27: master_pb.DataCenterInfo.diskInfos:type_name -> master_pb.DataCenterInfo.DiskInfosEntry + 88, // 27: master_pb.DataCenterInfo.diskInfos:type_name -> master_pb.DataCenterInfo.DiskInfosEntry 28, // 28: master_pb.TopologyInfo.data_center_infos:type_name -> master_pb.DataCenterInfo - 87, // 29: master_pb.TopologyInfo.diskInfos:type_name -> master_pb.TopologyInfo.DiskInfosEntry + 89, // 29: master_pb.TopologyInfo.diskInfos:type_name -> master_pb.TopologyInfo.DiskInfosEntry 29, // 30: master_pb.VolumeListResponse.topology_info:type_name -> master_pb.TopologyInfo - 88, // 31: master_pb.LookupEcVolumeResponse.shard_id_locations:type_name -> master_pb.LookupEcVolumeResponse.EcShardIdLocation + 90, // 31: master_pb.LookupEcVolumeResponse.shard_id_locations:type_name -> master_pb.LookupEcVolumeResponse.EcShardIdLocation 5, // 32: master_pb.GetMasterConfigurationResponse.storage_backends:type_name -> master_pb.StorageBackend - 89, // 33: master_pb.ListClusterNodesResponse.cluster_nodes:type_name -> master_pb.ListClusterNodesResponse.ClusterNode - 90, // 34: master_pb.RaftListClusterServersResponse.cluster_servers:type_name -> master_pb.RaftListClusterServersResponse.ClusterServers + 91, // 33: master_pb.ListClusterNodesResponse.cluster_nodes:type_name -> master_pb.ListClusterNodesResponse.ClusterNode + 92, // 34: master_pb.RaftListClusterServersResponse.cluster_servers:type_name -> master_pb.RaftListClusterServersResponse.ClusterServers 64, // 35: master_pb.BlockVolumeAssignment.replica_addrs:type_name -> master_pb.ReplicaAddrMessage 77, // 36: master_pb.ListBlockSnapshotsResponse.snapshots:type_name -> master_pb.BlockSnapshotInfo 14, // 37: master_pb.LookupVolumeResponse.VolumeIdLocation.locations:type_name -> master_pb.Location @@ -6186,40 +6305,42 @@ var file_master_proto_depIdxs = []int32{ 71, // 70: master_pb.Seaweed.CreateBlockSnapshot:input_type -> master_pb.CreateBlockSnapshotRequest 73, // 71: master_pb.Seaweed.DeleteBlockSnapshot:input_type -> master_pb.DeleteBlockSnapshotRequest 75, // 72: master_pb.Seaweed.ListBlockSnapshots:input_type -> master_pb.ListBlockSnapshotsRequest - 78, // 73: master_pb.Seaweed.ExpandBlockVolume:input_type -> master_pb.ExpandBlockVolumeRequest - 1, // 74: master_pb.Seaweed.SendHeartbeat:output_type -> master_pb.HeartbeatResponse - 11, // 75: master_pb.Seaweed.KeepConnected:output_type -> master_pb.KeepConnectedResponse - 13, // 76: master_pb.Seaweed.LookupVolume:output_type -> master_pb.LookupVolumeResponse - 17, // 77: master_pb.Seaweed.Assign:output_type -> master_pb.AssignResponse - 17, // 78: master_pb.Seaweed.StreamAssign:output_type -> master_pb.AssignResponse - 19, // 79: master_pb.Seaweed.Statistics:output_type -> master_pb.StatisticsResponse - 22, // 80: master_pb.Seaweed.CollectionList:output_type -> master_pb.CollectionListResponse - 24, // 81: master_pb.Seaweed.CollectionDelete:output_type -> master_pb.CollectionDeleteResponse - 31, // 82: master_pb.Seaweed.VolumeList:output_type -> master_pb.VolumeListResponse - 33, // 83: master_pb.Seaweed.LookupEcVolume:output_type -> master_pb.LookupEcVolumeResponse - 35, // 84: master_pb.Seaweed.VacuumVolume:output_type -> master_pb.VacuumVolumeResponse - 37, // 85: master_pb.Seaweed.DisableVacuum:output_type -> master_pb.DisableVacuumResponse - 39, // 86: master_pb.Seaweed.EnableVacuum:output_type -> master_pb.EnableVacuumResponse - 41, // 87: master_pb.Seaweed.VolumeMarkReadonly:output_type -> master_pb.VolumeMarkReadonlyResponse - 43, // 88: master_pb.Seaweed.GetMasterConfiguration:output_type -> master_pb.GetMasterConfigurationResponse - 45, // 89: master_pb.Seaweed.ListClusterNodes:output_type -> master_pb.ListClusterNodesResponse - 47, // 90: master_pb.Seaweed.LeaseAdminToken:output_type -> master_pb.LeaseAdminTokenResponse - 49, // 91: master_pb.Seaweed.ReleaseAdminToken:output_type -> master_pb.ReleaseAdminTokenResponse - 51, // 92: master_pb.Seaweed.Ping:output_type -> master_pb.PingResponse - 57, // 93: master_pb.Seaweed.RaftListClusterServers:output_type -> master_pb.RaftListClusterServersResponse - 53, // 94: master_pb.Seaweed.RaftAddServer:output_type -> master_pb.RaftAddServerResponse - 55, // 95: master_pb.Seaweed.RaftRemoveServer:output_type -> master_pb.RaftRemoveServerResponse - 59, // 96: master_pb.Seaweed.RaftLeadershipTransfer:output_type -> master_pb.RaftLeadershipTransferResponse - 60, // 97: master_pb.Seaweed.VolumeGrow:output_type -> master_pb.VolumeGrowResponse - 66, // 98: master_pb.Seaweed.CreateBlockVolume:output_type -> master_pb.CreateBlockVolumeResponse - 68, // 99: master_pb.Seaweed.DeleteBlockVolume:output_type -> master_pb.DeleteBlockVolumeResponse - 70, // 100: master_pb.Seaweed.LookupBlockVolume:output_type -> master_pb.LookupBlockVolumeResponse - 72, // 101: master_pb.Seaweed.CreateBlockSnapshot:output_type -> master_pb.CreateBlockSnapshotResponse - 74, // 102: master_pb.Seaweed.DeleteBlockSnapshot:output_type -> master_pb.DeleteBlockSnapshotResponse - 76, // 103: master_pb.Seaweed.ListBlockSnapshots:output_type -> master_pb.ListBlockSnapshotsResponse - 79, // 104: master_pb.Seaweed.ExpandBlockVolume:output_type -> master_pb.ExpandBlockVolumeResponse - 74, // [74:105] is the sub-list for method output_type - 43, // [43:74] is the sub-list for method input_type + 78, // 73: master_pb.Seaweed.RestoreBlockSnapshot:input_type -> master_pb.RestoreBlockSnapshotRequest + 80, // 74: master_pb.Seaweed.ExpandBlockVolume:input_type -> master_pb.ExpandBlockVolumeRequest + 1, // 75: master_pb.Seaweed.SendHeartbeat:output_type -> master_pb.HeartbeatResponse + 11, // 76: master_pb.Seaweed.KeepConnected:output_type -> master_pb.KeepConnectedResponse + 13, // 77: master_pb.Seaweed.LookupVolume:output_type -> master_pb.LookupVolumeResponse + 17, // 78: master_pb.Seaweed.Assign:output_type -> master_pb.AssignResponse + 17, // 79: master_pb.Seaweed.StreamAssign:output_type -> master_pb.AssignResponse + 19, // 80: master_pb.Seaweed.Statistics:output_type -> master_pb.StatisticsResponse + 22, // 81: master_pb.Seaweed.CollectionList:output_type -> master_pb.CollectionListResponse + 24, // 82: master_pb.Seaweed.CollectionDelete:output_type -> master_pb.CollectionDeleteResponse + 31, // 83: master_pb.Seaweed.VolumeList:output_type -> master_pb.VolumeListResponse + 33, // 84: master_pb.Seaweed.LookupEcVolume:output_type -> master_pb.LookupEcVolumeResponse + 35, // 85: master_pb.Seaweed.VacuumVolume:output_type -> master_pb.VacuumVolumeResponse + 37, // 86: master_pb.Seaweed.DisableVacuum:output_type -> master_pb.DisableVacuumResponse + 39, // 87: master_pb.Seaweed.EnableVacuum:output_type -> master_pb.EnableVacuumResponse + 41, // 88: master_pb.Seaweed.VolumeMarkReadonly:output_type -> master_pb.VolumeMarkReadonlyResponse + 43, // 89: master_pb.Seaweed.GetMasterConfiguration:output_type -> master_pb.GetMasterConfigurationResponse + 45, // 90: master_pb.Seaweed.ListClusterNodes:output_type -> master_pb.ListClusterNodesResponse + 47, // 91: master_pb.Seaweed.LeaseAdminToken:output_type -> master_pb.LeaseAdminTokenResponse + 49, // 92: master_pb.Seaweed.ReleaseAdminToken:output_type -> master_pb.ReleaseAdminTokenResponse + 51, // 93: master_pb.Seaweed.Ping:output_type -> master_pb.PingResponse + 57, // 94: master_pb.Seaweed.RaftListClusterServers:output_type -> master_pb.RaftListClusterServersResponse + 53, // 95: master_pb.Seaweed.RaftAddServer:output_type -> master_pb.RaftAddServerResponse + 55, // 96: master_pb.Seaweed.RaftRemoveServer:output_type -> master_pb.RaftRemoveServerResponse + 59, // 97: master_pb.Seaweed.RaftLeadershipTransfer:output_type -> master_pb.RaftLeadershipTransferResponse + 60, // 98: master_pb.Seaweed.VolumeGrow:output_type -> master_pb.VolumeGrowResponse + 66, // 99: master_pb.Seaweed.CreateBlockVolume:output_type -> master_pb.CreateBlockVolumeResponse + 68, // 100: master_pb.Seaweed.DeleteBlockVolume:output_type -> master_pb.DeleteBlockVolumeResponse + 70, // 101: master_pb.Seaweed.LookupBlockVolume:output_type -> master_pb.LookupBlockVolumeResponse + 72, // 102: master_pb.Seaweed.CreateBlockSnapshot:output_type -> master_pb.CreateBlockSnapshotResponse + 74, // 103: master_pb.Seaweed.DeleteBlockSnapshot:output_type -> master_pb.DeleteBlockSnapshotResponse + 76, // 104: master_pb.Seaweed.ListBlockSnapshots:output_type -> master_pb.ListBlockSnapshotsResponse + 79, // 105: master_pb.Seaweed.RestoreBlockSnapshot:output_type -> master_pb.RestoreBlockSnapshotResponse + 81, // 106: master_pb.Seaweed.ExpandBlockVolume:output_type -> master_pb.ExpandBlockVolumeResponse + 75, // [75:107] is the sub-list for method output_type + 43, // [43:75] is the sub-list for method input_type 43, // [43:43] is the sub-list for extension type_name 43, // [43:43] is the sub-list for extension extendee 0, // [0:43] is the sub-list for field type_name @@ -6236,7 +6357,7 @@ func file_master_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_master_proto_rawDesc), len(file_master_proto_rawDesc)), NumEnums: 0, - NumMessages: 91, + NumMessages: 93, NumExtensions: 0, NumServices: 1, }, diff --git a/weed/pb/master_pb/master_grpc.pb.go b/weed/pb/master_pb/master_grpc.pb.go index 3d4ebe328..500e87fc7 100644 --- a/weed/pb/master_pb/master_grpc.pb.go +++ b/weed/pb/master_pb/master_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.1 -// - protoc v5.29.5 +// - protoc v3.21.12 // source: master.proto package master_pb @@ -49,6 +49,7 @@ const ( Seaweed_CreateBlockSnapshot_FullMethodName = "/master_pb.Seaweed/CreateBlockSnapshot" Seaweed_DeleteBlockSnapshot_FullMethodName = "/master_pb.Seaweed/DeleteBlockSnapshot" Seaweed_ListBlockSnapshots_FullMethodName = "/master_pb.Seaweed/ListBlockSnapshots" + Seaweed_RestoreBlockSnapshot_FullMethodName = "/master_pb.Seaweed/RestoreBlockSnapshot" Seaweed_ExpandBlockVolume_FullMethodName = "/master_pb.Seaweed/ExpandBlockVolume" ) @@ -86,6 +87,7 @@ type SeaweedClient interface { CreateBlockSnapshot(ctx context.Context, in *CreateBlockSnapshotRequest, opts ...grpc.CallOption) (*CreateBlockSnapshotResponse, error) DeleteBlockSnapshot(ctx context.Context, in *DeleteBlockSnapshotRequest, opts ...grpc.CallOption) (*DeleteBlockSnapshotResponse, error) ListBlockSnapshots(ctx context.Context, in *ListBlockSnapshotsRequest, opts ...grpc.CallOption) (*ListBlockSnapshotsResponse, error) + RestoreBlockSnapshot(ctx context.Context, in *RestoreBlockSnapshotRequest, opts ...grpc.CallOption) (*RestoreBlockSnapshotResponse, error) ExpandBlockVolume(ctx context.Context, in *ExpandBlockVolumeRequest, opts ...grpc.CallOption) (*ExpandBlockVolumeResponse, error) } @@ -406,6 +408,16 @@ func (c *seaweedClient) ListBlockSnapshots(ctx context.Context, in *ListBlockSna return out, nil } +func (c *seaweedClient) RestoreBlockSnapshot(ctx context.Context, in *RestoreBlockSnapshotRequest, opts ...grpc.CallOption) (*RestoreBlockSnapshotResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RestoreBlockSnapshotResponse) + err := c.cc.Invoke(ctx, Seaweed_RestoreBlockSnapshot_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *seaweedClient) ExpandBlockVolume(ctx context.Context, in *ExpandBlockVolumeRequest, opts ...grpc.CallOption) (*ExpandBlockVolumeResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ExpandBlockVolumeResponse) @@ -450,6 +462,7 @@ type SeaweedServer interface { CreateBlockSnapshot(context.Context, *CreateBlockSnapshotRequest) (*CreateBlockSnapshotResponse, error) DeleteBlockSnapshot(context.Context, *DeleteBlockSnapshotRequest) (*DeleteBlockSnapshotResponse, error) ListBlockSnapshots(context.Context, *ListBlockSnapshotsRequest) (*ListBlockSnapshotsResponse, error) + RestoreBlockSnapshot(context.Context, *RestoreBlockSnapshotRequest) (*RestoreBlockSnapshotResponse, error) ExpandBlockVolume(context.Context, *ExpandBlockVolumeRequest) (*ExpandBlockVolumeResponse, error) mustEmbedUnimplementedSeaweedServer() } @@ -551,6 +564,9 @@ func (UnimplementedSeaweedServer) DeleteBlockSnapshot(context.Context, *DeleteBl func (UnimplementedSeaweedServer) ListBlockSnapshots(context.Context, *ListBlockSnapshotsRequest) (*ListBlockSnapshotsResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListBlockSnapshots not implemented") } +func (UnimplementedSeaweedServer) RestoreBlockSnapshot(context.Context, *RestoreBlockSnapshotRequest) (*RestoreBlockSnapshotResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RestoreBlockSnapshot not implemented") +} func (UnimplementedSeaweedServer) ExpandBlockVolume(context.Context, *ExpandBlockVolumeRequest) (*ExpandBlockVolumeResponse, error) { return nil, status.Error(codes.Unimplemented, "method ExpandBlockVolume not implemented") } @@ -1082,6 +1098,24 @@ func _Seaweed_ListBlockSnapshots_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _Seaweed_RestoreBlockSnapshot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RestoreBlockSnapshotRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SeaweedServer).RestoreBlockSnapshot(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Seaweed_RestoreBlockSnapshot_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SeaweedServer).RestoreBlockSnapshot(ctx, req.(*RestoreBlockSnapshotRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Seaweed_ExpandBlockVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ExpandBlockVolumeRequest) if err := dec(in); err != nil { @@ -1215,6 +1249,10 @@ var Seaweed_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListBlockSnapshots", Handler: _Seaweed_ListBlockSnapshots_Handler, }, + { + MethodName: "RestoreBlockSnapshot", + Handler: _Seaweed_RestoreBlockSnapshot_Handler, + }, { MethodName: "ExpandBlockVolume", Handler: _Seaweed_ExpandBlockVolume_Handler, diff --git a/weed/pb/volume_server.proto b/weed/pb/volume_server.proto index e7e2cd94e..f26350bf0 100644 --- a/weed/pb/volume_server.proto +++ b/weed/pb/volume_server.proto @@ -149,6 +149,8 @@ service VolumeServer { } rpc ListBlockSnapshots (ListBlockSnapshotsRequest) returns (ListBlockSnapshotsResponse) { } + rpc RestoreBlockSnapshot (RestoreBlockSnapshotRequest) returns (RestoreBlockSnapshotResponse) { + } rpc ExpandBlockVolume (ExpandBlockVolumeRequest) returns (ExpandBlockVolumeResponse) { } rpc PrepareExpandBlockVolume (PrepareExpandBlockVolumeRequest) returns (PrepareExpandBlockVolumeResponse) { @@ -821,6 +823,13 @@ message DeleteBlockSnapshotRequest { message DeleteBlockSnapshotResponse { } +message RestoreBlockSnapshotRequest { + string name = 1; + uint32 snapshot_id = 2; +} +message RestoreBlockSnapshotResponse { +} + message ListBlockSnapshotsRequest { string name = 1; } diff --git a/weed/pb/volume_server_pb/volume_server.pb.go b/weed/pb/volume_server_pb/volume_server.pb.go index 85249b59c..124229436 100644 --- a/weed/pb/volume_server_pb/volume_server.pb.go +++ b/weed/pb/volume_server_pb/volume_server.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v5.29.5 +// protoc v3.21.12 // source: volume_server.proto package volume_server_pb @@ -6633,6 +6633,94 @@ func (*DeleteBlockSnapshotResponse) Descriptor() ([]byte, []int) { return file_volume_server_proto_rawDescGZIP(), []int{114} } +type RestoreBlockSnapshotRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + SnapshotId uint32 `protobuf:"varint,2,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RestoreBlockSnapshotRequest) Reset() { + *x = RestoreBlockSnapshotRequest{} + mi := &file_volume_server_proto_msgTypes[115] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RestoreBlockSnapshotRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RestoreBlockSnapshotRequest) ProtoMessage() {} + +func (x *RestoreBlockSnapshotRequest) ProtoReflect() protoreflect.Message { + mi := &file_volume_server_proto_msgTypes[115] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RestoreBlockSnapshotRequest.ProtoReflect.Descriptor instead. +func (*RestoreBlockSnapshotRequest) Descriptor() ([]byte, []int) { + return file_volume_server_proto_rawDescGZIP(), []int{115} +} + +func (x *RestoreBlockSnapshotRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RestoreBlockSnapshotRequest) GetSnapshotId() uint32 { + if x != nil { + return x.SnapshotId + } + return 0 +} + +type RestoreBlockSnapshotResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RestoreBlockSnapshotResponse) Reset() { + *x = RestoreBlockSnapshotResponse{} + mi := &file_volume_server_proto_msgTypes[116] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RestoreBlockSnapshotResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RestoreBlockSnapshotResponse) ProtoMessage() {} + +func (x *RestoreBlockSnapshotResponse) ProtoReflect() protoreflect.Message { + mi := &file_volume_server_proto_msgTypes[116] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RestoreBlockSnapshotResponse.ProtoReflect.Descriptor instead. +func (*RestoreBlockSnapshotResponse) Descriptor() ([]byte, []int) { + return file_volume_server_proto_rawDescGZIP(), []int{116} +} + type ListBlockSnapshotsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -6642,7 +6730,7 @@ type ListBlockSnapshotsRequest struct { func (x *ListBlockSnapshotsRequest) Reset() { *x = ListBlockSnapshotsRequest{} - mi := &file_volume_server_proto_msgTypes[115] + mi := &file_volume_server_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6654,7 +6742,7 @@ func (x *ListBlockSnapshotsRequest) String() string { func (*ListBlockSnapshotsRequest) ProtoMessage() {} func (x *ListBlockSnapshotsRequest) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[115] + mi := &file_volume_server_proto_msgTypes[117] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6667,7 +6755,7 @@ func (x *ListBlockSnapshotsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListBlockSnapshotsRequest.ProtoReflect.Descriptor instead. func (*ListBlockSnapshotsRequest) Descriptor() ([]byte, []int) { - return file_volume_server_proto_rawDescGZIP(), []int{115} + return file_volume_server_proto_rawDescGZIP(), []int{117} } func (x *ListBlockSnapshotsRequest) GetName() string { @@ -6686,7 +6774,7 @@ type ListBlockSnapshotsResponse struct { func (x *ListBlockSnapshotsResponse) Reset() { *x = ListBlockSnapshotsResponse{} - mi := &file_volume_server_proto_msgTypes[116] + mi := &file_volume_server_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6698,7 +6786,7 @@ func (x *ListBlockSnapshotsResponse) String() string { func (*ListBlockSnapshotsResponse) ProtoMessage() {} func (x *ListBlockSnapshotsResponse) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[116] + mi := &file_volume_server_proto_msgTypes[118] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6711,7 +6799,7 @@ func (x *ListBlockSnapshotsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListBlockSnapshotsResponse.ProtoReflect.Descriptor instead. func (*ListBlockSnapshotsResponse) Descriptor() ([]byte, []int) { - return file_volume_server_proto_rawDescGZIP(), []int{116} + return file_volume_server_proto_rawDescGZIP(), []int{118} } func (x *ListBlockSnapshotsResponse) GetSnapshots() []*BlockSnapshotInfo { @@ -6732,7 +6820,7 @@ type BlockSnapshotInfo struct { func (x *BlockSnapshotInfo) Reset() { *x = BlockSnapshotInfo{} - mi := &file_volume_server_proto_msgTypes[117] + mi := &file_volume_server_proto_msgTypes[119] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6744,7 +6832,7 @@ func (x *BlockSnapshotInfo) String() string { func (*BlockSnapshotInfo) ProtoMessage() {} func (x *BlockSnapshotInfo) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[117] + mi := &file_volume_server_proto_msgTypes[119] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6757,7 +6845,7 @@ func (x *BlockSnapshotInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use BlockSnapshotInfo.ProtoReflect.Descriptor instead. func (*BlockSnapshotInfo) Descriptor() ([]byte, []int) { - return file_volume_server_proto_rawDescGZIP(), []int{117} + return file_volume_server_proto_rawDescGZIP(), []int{119} } func (x *BlockSnapshotInfo) GetSnapshotId() uint32 { @@ -6791,7 +6879,7 @@ type ExpandBlockVolumeRequest struct { func (x *ExpandBlockVolumeRequest) Reset() { *x = ExpandBlockVolumeRequest{} - mi := &file_volume_server_proto_msgTypes[118] + mi := &file_volume_server_proto_msgTypes[120] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6803,7 +6891,7 @@ func (x *ExpandBlockVolumeRequest) String() string { func (*ExpandBlockVolumeRequest) ProtoMessage() {} func (x *ExpandBlockVolumeRequest) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[118] + mi := &file_volume_server_proto_msgTypes[120] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6816,7 +6904,7 @@ func (x *ExpandBlockVolumeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExpandBlockVolumeRequest.ProtoReflect.Descriptor instead. func (*ExpandBlockVolumeRequest) Descriptor() ([]byte, []int) { - return file_volume_server_proto_rawDescGZIP(), []int{118} + return file_volume_server_proto_rawDescGZIP(), []int{120} } func (x *ExpandBlockVolumeRequest) GetName() string { @@ -6842,7 +6930,7 @@ type ExpandBlockVolumeResponse struct { func (x *ExpandBlockVolumeResponse) Reset() { *x = ExpandBlockVolumeResponse{} - mi := &file_volume_server_proto_msgTypes[119] + mi := &file_volume_server_proto_msgTypes[121] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6854,7 +6942,7 @@ func (x *ExpandBlockVolumeResponse) String() string { func (*ExpandBlockVolumeResponse) ProtoMessage() {} func (x *ExpandBlockVolumeResponse) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[119] + mi := &file_volume_server_proto_msgTypes[121] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6867,7 +6955,7 @@ func (x *ExpandBlockVolumeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ExpandBlockVolumeResponse.ProtoReflect.Descriptor instead. func (*ExpandBlockVolumeResponse) Descriptor() ([]byte, []int) { - return file_volume_server_proto_rawDescGZIP(), []int{119} + return file_volume_server_proto_rawDescGZIP(), []int{121} } func (x *ExpandBlockVolumeResponse) GetCapacityBytes() uint64 { @@ -6888,7 +6976,7 @@ type PrepareExpandBlockVolumeRequest struct { func (x *PrepareExpandBlockVolumeRequest) Reset() { *x = PrepareExpandBlockVolumeRequest{} - mi := &file_volume_server_proto_msgTypes[120] + mi := &file_volume_server_proto_msgTypes[122] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6900,7 +6988,7 @@ func (x *PrepareExpandBlockVolumeRequest) String() string { func (*PrepareExpandBlockVolumeRequest) ProtoMessage() {} func (x *PrepareExpandBlockVolumeRequest) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[120] + mi := &file_volume_server_proto_msgTypes[122] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6913,7 +7001,7 @@ func (x *PrepareExpandBlockVolumeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PrepareExpandBlockVolumeRequest.ProtoReflect.Descriptor instead. func (*PrepareExpandBlockVolumeRequest) Descriptor() ([]byte, []int) { - return file_volume_server_proto_rawDescGZIP(), []int{120} + return file_volume_server_proto_rawDescGZIP(), []int{122} } func (x *PrepareExpandBlockVolumeRequest) GetName() string { @@ -6945,7 +7033,7 @@ type PrepareExpandBlockVolumeResponse struct { func (x *PrepareExpandBlockVolumeResponse) Reset() { *x = PrepareExpandBlockVolumeResponse{} - mi := &file_volume_server_proto_msgTypes[121] + mi := &file_volume_server_proto_msgTypes[123] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6957,7 +7045,7 @@ func (x *PrepareExpandBlockVolumeResponse) String() string { func (*PrepareExpandBlockVolumeResponse) ProtoMessage() {} func (x *PrepareExpandBlockVolumeResponse) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[121] + mi := &file_volume_server_proto_msgTypes[123] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6970,7 +7058,7 @@ func (x *PrepareExpandBlockVolumeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PrepareExpandBlockVolumeResponse.ProtoReflect.Descriptor instead. func (*PrepareExpandBlockVolumeResponse) Descriptor() ([]byte, []int) { - return file_volume_server_proto_rawDescGZIP(), []int{121} + return file_volume_server_proto_rawDescGZIP(), []int{123} } type CommitExpandBlockVolumeRequest struct { @@ -6983,7 +7071,7 @@ type CommitExpandBlockVolumeRequest struct { func (x *CommitExpandBlockVolumeRequest) Reset() { *x = CommitExpandBlockVolumeRequest{} - mi := &file_volume_server_proto_msgTypes[122] + mi := &file_volume_server_proto_msgTypes[124] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6995,7 +7083,7 @@ func (x *CommitExpandBlockVolumeRequest) String() string { func (*CommitExpandBlockVolumeRequest) ProtoMessage() {} func (x *CommitExpandBlockVolumeRequest) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[122] + mi := &file_volume_server_proto_msgTypes[124] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7008,7 +7096,7 @@ func (x *CommitExpandBlockVolumeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CommitExpandBlockVolumeRequest.ProtoReflect.Descriptor instead. func (*CommitExpandBlockVolumeRequest) Descriptor() ([]byte, []int) { - return file_volume_server_proto_rawDescGZIP(), []int{122} + return file_volume_server_proto_rawDescGZIP(), []int{124} } func (x *CommitExpandBlockVolumeRequest) GetName() string { @@ -7034,7 +7122,7 @@ type CommitExpandBlockVolumeResponse struct { func (x *CommitExpandBlockVolumeResponse) Reset() { *x = CommitExpandBlockVolumeResponse{} - mi := &file_volume_server_proto_msgTypes[123] + mi := &file_volume_server_proto_msgTypes[125] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7046,7 +7134,7 @@ func (x *CommitExpandBlockVolumeResponse) String() string { func (*CommitExpandBlockVolumeResponse) ProtoMessage() {} func (x *CommitExpandBlockVolumeResponse) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[123] + mi := &file_volume_server_proto_msgTypes[125] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7059,7 +7147,7 @@ func (x *CommitExpandBlockVolumeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CommitExpandBlockVolumeResponse.ProtoReflect.Descriptor instead. func (*CommitExpandBlockVolumeResponse) Descriptor() ([]byte, []int) { - return file_volume_server_proto_rawDescGZIP(), []int{123} + return file_volume_server_proto_rawDescGZIP(), []int{125} } func (x *CommitExpandBlockVolumeResponse) GetCapacityBytes() uint64 { @@ -7079,7 +7167,7 @@ type CancelExpandBlockVolumeRequest struct { func (x *CancelExpandBlockVolumeRequest) Reset() { *x = CancelExpandBlockVolumeRequest{} - mi := &file_volume_server_proto_msgTypes[124] + mi := &file_volume_server_proto_msgTypes[126] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7091,7 +7179,7 @@ func (x *CancelExpandBlockVolumeRequest) String() string { func (*CancelExpandBlockVolumeRequest) ProtoMessage() {} func (x *CancelExpandBlockVolumeRequest) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[124] + mi := &file_volume_server_proto_msgTypes[126] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7104,7 +7192,7 @@ func (x *CancelExpandBlockVolumeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CancelExpandBlockVolumeRequest.ProtoReflect.Descriptor instead. func (*CancelExpandBlockVolumeRequest) Descriptor() ([]byte, []int) { - return file_volume_server_proto_rawDescGZIP(), []int{124} + return file_volume_server_proto_rawDescGZIP(), []int{126} } func (x *CancelExpandBlockVolumeRequest) GetName() string { @@ -7129,7 +7217,7 @@ type CancelExpandBlockVolumeResponse struct { func (x *CancelExpandBlockVolumeResponse) Reset() { *x = CancelExpandBlockVolumeResponse{} - mi := &file_volume_server_proto_msgTypes[125] + mi := &file_volume_server_proto_msgTypes[127] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7141,7 +7229,7 @@ func (x *CancelExpandBlockVolumeResponse) String() string { func (*CancelExpandBlockVolumeResponse) ProtoMessage() {} func (x *CancelExpandBlockVolumeResponse) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[125] + mi := &file_volume_server_proto_msgTypes[127] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7154,7 +7242,7 @@ func (x *CancelExpandBlockVolumeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CancelExpandBlockVolumeResponse.ProtoReflect.Descriptor instead. func (*CancelExpandBlockVolumeResponse) Descriptor() ([]byte, []int) { - return file_volume_server_proto_rawDescGZIP(), []int{125} + return file_volume_server_proto_rawDescGZIP(), []int{127} } type FetchAndWriteNeedleRequest_Replica struct { @@ -7168,7 +7256,7 @@ type FetchAndWriteNeedleRequest_Replica struct { func (x *FetchAndWriteNeedleRequest_Replica) Reset() { *x = FetchAndWriteNeedleRequest_Replica{} - mi := &file_volume_server_proto_msgTypes[126] + mi := &file_volume_server_proto_msgTypes[128] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7180,7 +7268,7 @@ func (x *FetchAndWriteNeedleRequest_Replica) String() string { func (*FetchAndWriteNeedleRequest_Replica) ProtoMessage() {} func (x *FetchAndWriteNeedleRequest_Replica) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[126] + mi := &file_volume_server_proto_msgTypes[128] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7228,7 +7316,7 @@ type QueryRequest_Filter struct { func (x *QueryRequest_Filter) Reset() { *x = QueryRequest_Filter{} - mi := &file_volume_server_proto_msgTypes[127] + mi := &file_volume_server_proto_msgTypes[129] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7240,7 +7328,7 @@ func (x *QueryRequest_Filter) String() string { func (*QueryRequest_Filter) ProtoMessage() {} func (x *QueryRequest_Filter) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[127] + mi := &file_volume_server_proto_msgTypes[129] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7290,7 +7378,7 @@ type QueryRequest_InputSerialization struct { func (x *QueryRequest_InputSerialization) Reset() { *x = QueryRequest_InputSerialization{} - mi := &file_volume_server_proto_msgTypes[128] + mi := &file_volume_server_proto_msgTypes[130] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7302,7 +7390,7 @@ func (x *QueryRequest_InputSerialization) String() string { func (*QueryRequest_InputSerialization) ProtoMessage() {} func (x *QueryRequest_InputSerialization) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[128] + mi := &file_volume_server_proto_msgTypes[130] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7356,7 +7444,7 @@ type QueryRequest_OutputSerialization struct { func (x *QueryRequest_OutputSerialization) Reset() { *x = QueryRequest_OutputSerialization{} - mi := &file_volume_server_proto_msgTypes[129] + mi := &file_volume_server_proto_msgTypes[131] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7368,7 +7456,7 @@ func (x *QueryRequest_OutputSerialization) String() string { func (*QueryRequest_OutputSerialization) ProtoMessage() {} func (x *QueryRequest_OutputSerialization) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[129] + mi := &file_volume_server_proto_msgTypes[131] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7414,7 +7502,7 @@ type QueryRequest_InputSerialization_CSVInput struct { func (x *QueryRequest_InputSerialization_CSVInput) Reset() { *x = QueryRequest_InputSerialization_CSVInput{} - mi := &file_volume_server_proto_msgTypes[130] + mi := &file_volume_server_proto_msgTypes[132] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7426,7 +7514,7 @@ func (x *QueryRequest_InputSerialization_CSVInput) String() string { func (*QueryRequest_InputSerialization_CSVInput) ProtoMessage() {} func (x *QueryRequest_InputSerialization_CSVInput) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[130] + mi := &file_volume_server_proto_msgTypes[132] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7500,7 +7588,7 @@ type QueryRequest_InputSerialization_JSONInput struct { func (x *QueryRequest_InputSerialization_JSONInput) Reset() { *x = QueryRequest_InputSerialization_JSONInput{} - mi := &file_volume_server_proto_msgTypes[131] + mi := &file_volume_server_proto_msgTypes[133] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7512,7 +7600,7 @@ func (x *QueryRequest_InputSerialization_JSONInput) String() string { func (*QueryRequest_InputSerialization_JSONInput) ProtoMessage() {} func (x *QueryRequest_InputSerialization_JSONInput) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[131] + mi := &file_volume_server_proto_msgTypes[133] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7543,7 +7631,7 @@ type QueryRequest_InputSerialization_ParquetInput struct { func (x *QueryRequest_InputSerialization_ParquetInput) Reset() { *x = QueryRequest_InputSerialization_ParquetInput{} - mi := &file_volume_server_proto_msgTypes[132] + mi := &file_volume_server_proto_msgTypes[134] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7555,7 +7643,7 @@ func (x *QueryRequest_InputSerialization_ParquetInput) String() string { func (*QueryRequest_InputSerialization_ParquetInput) ProtoMessage() {} func (x *QueryRequest_InputSerialization_ParquetInput) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[132] + mi := &file_volume_server_proto_msgTypes[134] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7584,7 +7672,7 @@ type QueryRequest_OutputSerialization_CSVOutput struct { func (x *QueryRequest_OutputSerialization_CSVOutput) Reset() { *x = QueryRequest_OutputSerialization_CSVOutput{} - mi := &file_volume_server_proto_msgTypes[133] + mi := &file_volume_server_proto_msgTypes[135] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7596,7 +7684,7 @@ func (x *QueryRequest_OutputSerialization_CSVOutput) String() string { func (*QueryRequest_OutputSerialization_CSVOutput) ProtoMessage() {} func (x *QueryRequest_OutputSerialization_CSVOutput) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[133] + mi := &file_volume_server_proto_msgTypes[135] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7656,7 +7744,7 @@ type QueryRequest_OutputSerialization_JSONOutput struct { func (x *QueryRequest_OutputSerialization_JSONOutput) Reset() { *x = QueryRequest_OutputSerialization_JSONOutput{} - mi := &file_volume_server_proto_msgTypes[134] + mi := &file_volume_server_proto_msgTypes[136] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7668,7 +7756,7 @@ func (x *QueryRequest_OutputSerialization_JSONOutput) String() string { func (*QueryRequest_OutputSerialization_JSONOutput) ProtoMessage() {} func (x *QueryRequest_OutputSerialization_JSONOutput) ProtoReflect() protoreflect.Message { - mi := &file_volume_server_proto_msgTypes[134] + mi := &file_volume_server_proto_msgTypes[136] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8229,7 +8317,12 @@ const file_volume_server_proto_rawDesc = "" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1f\n" + "\vsnapshot_id\x18\x02 \x01(\rR\n" + "snapshotId\"\x1d\n" + - "\x1bDeleteBlockSnapshotResponse\"/\n" + + "\x1bDeleteBlockSnapshotResponse\"R\n" + + "\x1bRestoreBlockSnapshotRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1f\n" + + "\vsnapshot_id\x18\x02 \x01(\rR\n" + + "snapshotId\"\x1e\n" + + "\x1cRestoreBlockSnapshotResponse\"/\n" + "\x19ListBlockSnapshotsRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\"_\n" + "\x1aListBlockSnapshotsResponse\x12A\n" + @@ -8263,7 +8356,7 @@ const file_volume_server_proto_rawDesc = "" + "\aUNKNOWN\x10\x00\x12\t\n" + "\x05INDEX\x10\x01\x12\b\n" + "\x04FULL\x10\x02\x12\t\n" + - "\x05LOCAL\x10\x032\xe11\n" + + "\x05LOCAL\x10\x032\xda2\n" + "\fVolumeServer\x12\\\n" + "\vBatchDelete\x12$.volume_server_pb.BatchDeleteRequest\x1a%.volume_server_pb.BatchDeleteResponse\"\x00\x12n\n" + "\x11VacuumVolumeCheck\x12*.volume_server_pb.VacuumVolumeCheckRequest\x1a+.volume_server_pb.VacuumVolumeCheckResponse\"\x00\x12v\n" + @@ -8318,7 +8411,8 @@ const file_volume_server_proto_rawDesc = "" + "\x1dVolumeServerDeleteBlockVolume\x126.volume_server_pb.VolumeServerDeleteBlockVolumeRequest\x1a7.volume_server_pb.VolumeServerDeleteBlockVolumeResponse\"\x00\x12t\n" + "\x13SnapshotBlockVolume\x12,.volume_server_pb.SnapshotBlockVolumeRequest\x1a-.volume_server_pb.SnapshotBlockVolumeResponse\"\x00\x12t\n" + "\x13DeleteBlockSnapshot\x12,.volume_server_pb.DeleteBlockSnapshotRequest\x1a-.volume_server_pb.DeleteBlockSnapshotResponse\"\x00\x12q\n" + - "\x12ListBlockSnapshots\x12+.volume_server_pb.ListBlockSnapshotsRequest\x1a,.volume_server_pb.ListBlockSnapshotsResponse\"\x00\x12n\n" + + "\x12ListBlockSnapshots\x12+.volume_server_pb.ListBlockSnapshotsRequest\x1a,.volume_server_pb.ListBlockSnapshotsResponse\"\x00\x12w\n" + + "\x14RestoreBlockSnapshot\x12-.volume_server_pb.RestoreBlockSnapshotRequest\x1a..volume_server_pb.RestoreBlockSnapshotResponse\"\x00\x12n\n" + "\x11ExpandBlockVolume\x12*.volume_server_pb.ExpandBlockVolumeRequest\x1a+.volume_server_pb.ExpandBlockVolumeResponse\"\x00\x12\x83\x01\n" + "\x18PrepareExpandBlockVolume\x121.volume_server_pb.PrepareExpandBlockVolumeRequest\x1a2.volume_server_pb.PrepareExpandBlockVolumeResponse\"\x00\x12\x80\x01\n" + "\x17CommitExpandBlockVolume\x120.volume_server_pb.CommitExpandBlockVolumeRequest\x1a1.volume_server_pb.CommitExpandBlockVolumeResponse\"\x00\x12\x80\x01\n" + @@ -8337,7 +8431,7 @@ func file_volume_server_proto_rawDescGZIP() []byte { } var file_volume_server_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_volume_server_proto_msgTypes = make([]protoimpl.MessageInfo, 135) +var file_volume_server_proto_msgTypes = make([]protoimpl.MessageInfo, 137) var file_volume_server_proto_goTypes = []any{ (VolumeScrubMode)(0), // 0: volume_server_pb.VolumeScrubMode (*VolumeServerState)(nil), // 1: volume_server_pb.VolumeServerState @@ -8455,28 +8549,30 @@ var file_volume_server_proto_goTypes = []any{ (*SnapshotBlockVolumeResponse)(nil), // 113: volume_server_pb.SnapshotBlockVolumeResponse (*DeleteBlockSnapshotRequest)(nil), // 114: volume_server_pb.DeleteBlockSnapshotRequest (*DeleteBlockSnapshotResponse)(nil), // 115: volume_server_pb.DeleteBlockSnapshotResponse - (*ListBlockSnapshotsRequest)(nil), // 116: volume_server_pb.ListBlockSnapshotsRequest - (*ListBlockSnapshotsResponse)(nil), // 117: volume_server_pb.ListBlockSnapshotsResponse - (*BlockSnapshotInfo)(nil), // 118: volume_server_pb.BlockSnapshotInfo - (*ExpandBlockVolumeRequest)(nil), // 119: volume_server_pb.ExpandBlockVolumeRequest - (*ExpandBlockVolumeResponse)(nil), // 120: volume_server_pb.ExpandBlockVolumeResponse - (*PrepareExpandBlockVolumeRequest)(nil), // 121: volume_server_pb.PrepareExpandBlockVolumeRequest - (*PrepareExpandBlockVolumeResponse)(nil), // 122: volume_server_pb.PrepareExpandBlockVolumeResponse - (*CommitExpandBlockVolumeRequest)(nil), // 123: volume_server_pb.CommitExpandBlockVolumeRequest - (*CommitExpandBlockVolumeResponse)(nil), // 124: volume_server_pb.CommitExpandBlockVolumeResponse - (*CancelExpandBlockVolumeRequest)(nil), // 125: volume_server_pb.CancelExpandBlockVolumeRequest - (*CancelExpandBlockVolumeResponse)(nil), // 126: volume_server_pb.CancelExpandBlockVolumeResponse - (*FetchAndWriteNeedleRequest_Replica)(nil), // 127: volume_server_pb.FetchAndWriteNeedleRequest.Replica - (*QueryRequest_Filter)(nil), // 128: volume_server_pb.QueryRequest.Filter - (*QueryRequest_InputSerialization)(nil), // 129: volume_server_pb.QueryRequest.InputSerialization - (*QueryRequest_OutputSerialization)(nil), // 130: volume_server_pb.QueryRequest.OutputSerialization - (*QueryRequest_InputSerialization_CSVInput)(nil), // 131: volume_server_pb.QueryRequest.InputSerialization.CSVInput - (*QueryRequest_InputSerialization_JSONInput)(nil), // 132: volume_server_pb.QueryRequest.InputSerialization.JSONInput - (*QueryRequest_InputSerialization_ParquetInput)(nil), // 133: volume_server_pb.QueryRequest.InputSerialization.ParquetInput - (*QueryRequest_OutputSerialization_CSVOutput)(nil), // 134: volume_server_pb.QueryRequest.OutputSerialization.CSVOutput - (*QueryRequest_OutputSerialization_JSONOutput)(nil), // 135: volume_server_pb.QueryRequest.OutputSerialization.JSONOutput - (*remote_pb.RemoteConf)(nil), // 136: remote_pb.RemoteConf - (*remote_pb.RemoteStorageLocation)(nil), // 137: remote_pb.RemoteStorageLocation + (*RestoreBlockSnapshotRequest)(nil), // 116: volume_server_pb.RestoreBlockSnapshotRequest + (*RestoreBlockSnapshotResponse)(nil), // 117: volume_server_pb.RestoreBlockSnapshotResponse + (*ListBlockSnapshotsRequest)(nil), // 118: volume_server_pb.ListBlockSnapshotsRequest + (*ListBlockSnapshotsResponse)(nil), // 119: volume_server_pb.ListBlockSnapshotsResponse + (*BlockSnapshotInfo)(nil), // 120: volume_server_pb.BlockSnapshotInfo + (*ExpandBlockVolumeRequest)(nil), // 121: volume_server_pb.ExpandBlockVolumeRequest + (*ExpandBlockVolumeResponse)(nil), // 122: volume_server_pb.ExpandBlockVolumeResponse + (*PrepareExpandBlockVolumeRequest)(nil), // 123: volume_server_pb.PrepareExpandBlockVolumeRequest + (*PrepareExpandBlockVolumeResponse)(nil), // 124: volume_server_pb.PrepareExpandBlockVolumeResponse + (*CommitExpandBlockVolumeRequest)(nil), // 125: volume_server_pb.CommitExpandBlockVolumeRequest + (*CommitExpandBlockVolumeResponse)(nil), // 126: volume_server_pb.CommitExpandBlockVolumeResponse + (*CancelExpandBlockVolumeRequest)(nil), // 127: volume_server_pb.CancelExpandBlockVolumeRequest + (*CancelExpandBlockVolumeResponse)(nil), // 128: volume_server_pb.CancelExpandBlockVolumeResponse + (*FetchAndWriteNeedleRequest_Replica)(nil), // 129: volume_server_pb.FetchAndWriteNeedleRequest.Replica + (*QueryRequest_Filter)(nil), // 130: volume_server_pb.QueryRequest.Filter + (*QueryRequest_InputSerialization)(nil), // 131: volume_server_pb.QueryRequest.InputSerialization + (*QueryRequest_OutputSerialization)(nil), // 132: volume_server_pb.QueryRequest.OutputSerialization + (*QueryRequest_InputSerialization_CSVInput)(nil), // 133: volume_server_pb.QueryRequest.InputSerialization.CSVInput + (*QueryRequest_InputSerialization_JSONInput)(nil), // 134: volume_server_pb.QueryRequest.InputSerialization.JSONInput + (*QueryRequest_InputSerialization_ParquetInput)(nil), // 135: volume_server_pb.QueryRequest.InputSerialization.ParquetInput + (*QueryRequest_OutputSerialization_CSVOutput)(nil), // 136: volume_server_pb.QueryRequest.OutputSerialization.CSVOutput + (*QueryRequest_OutputSerialization_JSONOutput)(nil), // 137: volume_server_pb.QueryRequest.OutputSerialization.JSONOutput + (*remote_pb.RemoteConf)(nil), // 138: remote_pb.RemoteConf + (*remote_pb.RemoteStorageLocation)(nil), // 139: remote_pb.RemoteStorageLocation } var file_volume_server_proto_depIdxs = []int32{ 4, // 0: volume_server_pb.BatchDeleteResponse.results:type_name -> volume_server_pb.DeleteResult @@ -8492,21 +8588,21 @@ var file_volume_server_proto_depIdxs = []int32{ 82, // 10: volume_server_pb.VolumeServerStatusResponse.disk_statuses:type_name -> volume_server_pb.DiskStatus 83, // 11: volume_server_pb.VolumeServerStatusResponse.memory_status:type_name -> volume_server_pb.MemStatus 1, // 12: volume_server_pb.VolumeServerStatusResponse.state:type_name -> volume_server_pb.VolumeServerState - 127, // 13: volume_server_pb.FetchAndWriteNeedleRequest.replicas:type_name -> volume_server_pb.FetchAndWriteNeedleRequest.Replica - 136, // 14: volume_server_pb.FetchAndWriteNeedleRequest.remote_conf:type_name -> remote_pb.RemoteConf - 137, // 15: volume_server_pb.FetchAndWriteNeedleRequest.remote_location:type_name -> remote_pb.RemoteStorageLocation + 129, // 13: volume_server_pb.FetchAndWriteNeedleRequest.replicas:type_name -> volume_server_pb.FetchAndWriteNeedleRequest.Replica + 138, // 14: volume_server_pb.FetchAndWriteNeedleRequest.remote_conf:type_name -> remote_pb.RemoteConf + 139, // 15: volume_server_pb.FetchAndWriteNeedleRequest.remote_location:type_name -> remote_pb.RemoteStorageLocation 0, // 16: volume_server_pb.ScrubVolumeRequest.mode:type_name -> volume_server_pb.VolumeScrubMode 0, // 17: volume_server_pb.ScrubEcVolumeRequest.mode:type_name -> volume_server_pb.VolumeScrubMode 79, // 18: volume_server_pb.ScrubEcVolumeResponse.broken_shard_infos:type_name -> volume_server_pb.EcShardInfo - 128, // 19: volume_server_pb.QueryRequest.filter:type_name -> volume_server_pb.QueryRequest.Filter - 129, // 20: volume_server_pb.QueryRequest.input_serialization:type_name -> volume_server_pb.QueryRequest.InputSerialization - 130, // 21: volume_server_pb.QueryRequest.output_serialization:type_name -> volume_server_pb.QueryRequest.OutputSerialization - 118, // 22: volume_server_pb.ListBlockSnapshotsResponse.snapshots:type_name -> volume_server_pb.BlockSnapshotInfo - 131, // 23: volume_server_pb.QueryRequest.InputSerialization.csv_input:type_name -> volume_server_pb.QueryRequest.InputSerialization.CSVInput - 132, // 24: volume_server_pb.QueryRequest.InputSerialization.json_input:type_name -> volume_server_pb.QueryRequest.InputSerialization.JSONInput - 133, // 25: volume_server_pb.QueryRequest.InputSerialization.parquet_input:type_name -> volume_server_pb.QueryRequest.InputSerialization.ParquetInput - 134, // 26: volume_server_pb.QueryRequest.OutputSerialization.csv_output:type_name -> volume_server_pb.QueryRequest.OutputSerialization.CSVOutput - 135, // 27: volume_server_pb.QueryRequest.OutputSerialization.json_output:type_name -> volume_server_pb.QueryRequest.OutputSerialization.JSONOutput + 130, // 19: volume_server_pb.QueryRequest.filter:type_name -> volume_server_pb.QueryRequest.Filter + 131, // 20: volume_server_pb.QueryRequest.input_serialization:type_name -> volume_server_pb.QueryRequest.InputSerialization + 132, // 21: volume_server_pb.QueryRequest.output_serialization:type_name -> volume_server_pb.QueryRequest.OutputSerialization + 120, // 22: volume_server_pb.ListBlockSnapshotsResponse.snapshots:type_name -> volume_server_pb.BlockSnapshotInfo + 133, // 23: volume_server_pb.QueryRequest.InputSerialization.csv_input:type_name -> volume_server_pb.QueryRequest.InputSerialization.CSVInput + 134, // 24: volume_server_pb.QueryRequest.InputSerialization.json_input:type_name -> volume_server_pb.QueryRequest.InputSerialization.JSONInput + 135, // 25: volume_server_pb.QueryRequest.InputSerialization.parquet_input:type_name -> volume_server_pb.QueryRequest.InputSerialization.ParquetInput + 136, // 26: volume_server_pb.QueryRequest.OutputSerialization.csv_output:type_name -> volume_server_pb.QueryRequest.OutputSerialization.CSVOutput + 137, // 27: volume_server_pb.QueryRequest.OutputSerialization.json_output:type_name -> volume_server_pb.QueryRequest.OutputSerialization.JSONOutput 2, // 28: volume_server_pb.VolumeServer.BatchDelete:input_type -> volume_server_pb.BatchDeleteRequest 6, // 29: volume_server_pb.VolumeServer.VacuumVolumeCheck:input_type -> volume_server_pb.VacuumVolumeCheckRequest 8, // 30: volume_server_pb.VolumeServer.VacuumVolumeCompact:input_type -> volume_server_pb.VacuumVolumeCompactRequest @@ -8559,70 +8655,72 @@ var file_volume_server_proto_depIdxs = []int32{ 110, // 77: volume_server_pb.VolumeServer.VolumeServerDeleteBlockVolume:input_type -> volume_server_pb.VolumeServerDeleteBlockVolumeRequest 112, // 78: volume_server_pb.VolumeServer.SnapshotBlockVolume:input_type -> volume_server_pb.SnapshotBlockVolumeRequest 114, // 79: volume_server_pb.VolumeServer.DeleteBlockSnapshot:input_type -> volume_server_pb.DeleteBlockSnapshotRequest - 116, // 80: volume_server_pb.VolumeServer.ListBlockSnapshots:input_type -> volume_server_pb.ListBlockSnapshotsRequest - 119, // 81: volume_server_pb.VolumeServer.ExpandBlockVolume:input_type -> volume_server_pb.ExpandBlockVolumeRequest - 121, // 82: volume_server_pb.VolumeServer.PrepareExpandBlockVolume:input_type -> volume_server_pb.PrepareExpandBlockVolumeRequest - 123, // 83: volume_server_pb.VolumeServer.CommitExpandBlockVolume:input_type -> volume_server_pb.CommitExpandBlockVolumeRequest - 125, // 84: volume_server_pb.VolumeServer.CancelExpandBlockVolume:input_type -> volume_server_pb.CancelExpandBlockVolumeRequest - 3, // 85: volume_server_pb.VolumeServer.BatchDelete:output_type -> volume_server_pb.BatchDeleteResponse - 7, // 86: volume_server_pb.VolumeServer.VacuumVolumeCheck:output_type -> volume_server_pb.VacuumVolumeCheckResponse - 9, // 87: volume_server_pb.VolumeServer.VacuumVolumeCompact:output_type -> volume_server_pb.VacuumVolumeCompactResponse - 11, // 88: volume_server_pb.VolumeServer.VacuumVolumeCommit:output_type -> volume_server_pb.VacuumVolumeCommitResponse - 13, // 89: volume_server_pb.VolumeServer.VacuumVolumeCleanup:output_type -> volume_server_pb.VacuumVolumeCleanupResponse - 15, // 90: volume_server_pb.VolumeServer.DeleteCollection:output_type -> volume_server_pb.DeleteCollectionResponse - 17, // 91: volume_server_pb.VolumeServer.AllocateVolume:output_type -> volume_server_pb.AllocateVolumeResponse - 19, // 92: volume_server_pb.VolumeServer.VolumeSyncStatus:output_type -> volume_server_pb.VolumeSyncStatusResponse - 21, // 93: volume_server_pb.VolumeServer.VolumeIncrementalCopy:output_type -> volume_server_pb.VolumeIncrementalCopyResponse - 23, // 94: volume_server_pb.VolumeServer.VolumeMount:output_type -> volume_server_pb.VolumeMountResponse - 25, // 95: volume_server_pb.VolumeServer.VolumeUnmount:output_type -> volume_server_pb.VolumeUnmountResponse - 27, // 96: volume_server_pb.VolumeServer.VolumeDelete:output_type -> volume_server_pb.VolumeDeleteResponse - 29, // 97: volume_server_pb.VolumeServer.VolumeMarkReadonly:output_type -> volume_server_pb.VolumeMarkReadonlyResponse - 31, // 98: volume_server_pb.VolumeServer.VolumeMarkWritable:output_type -> volume_server_pb.VolumeMarkWritableResponse - 33, // 99: volume_server_pb.VolumeServer.VolumeConfigure:output_type -> volume_server_pb.VolumeConfigureResponse - 35, // 100: volume_server_pb.VolumeServer.VolumeStatus:output_type -> volume_server_pb.VolumeStatusResponse - 37, // 101: volume_server_pb.VolumeServer.GetState:output_type -> volume_server_pb.GetStateResponse - 39, // 102: volume_server_pb.VolumeServer.SetState:output_type -> volume_server_pb.SetStateResponse - 41, // 103: volume_server_pb.VolumeServer.VolumeCopy:output_type -> volume_server_pb.VolumeCopyResponse - 81, // 104: volume_server_pb.VolumeServer.ReadVolumeFileStatus:output_type -> volume_server_pb.ReadVolumeFileStatusResponse - 43, // 105: volume_server_pb.VolumeServer.CopyFile:output_type -> volume_server_pb.CopyFileResponse - 46, // 106: volume_server_pb.VolumeServer.ReceiveFile:output_type -> volume_server_pb.ReceiveFileResponse - 48, // 107: volume_server_pb.VolumeServer.ReadNeedleBlob:output_type -> volume_server_pb.ReadNeedleBlobResponse - 50, // 108: volume_server_pb.VolumeServer.ReadNeedleMeta:output_type -> volume_server_pb.ReadNeedleMetaResponse - 52, // 109: volume_server_pb.VolumeServer.WriteNeedleBlob:output_type -> volume_server_pb.WriteNeedleBlobResponse - 54, // 110: volume_server_pb.VolumeServer.ReadAllNeedles:output_type -> volume_server_pb.ReadAllNeedlesResponse - 56, // 111: volume_server_pb.VolumeServer.VolumeTailSender:output_type -> volume_server_pb.VolumeTailSenderResponse - 58, // 112: volume_server_pb.VolumeServer.VolumeTailReceiver:output_type -> volume_server_pb.VolumeTailReceiverResponse - 60, // 113: volume_server_pb.VolumeServer.VolumeEcShardsGenerate:output_type -> volume_server_pb.VolumeEcShardsGenerateResponse - 62, // 114: volume_server_pb.VolumeServer.VolumeEcShardsRebuild:output_type -> volume_server_pb.VolumeEcShardsRebuildResponse - 64, // 115: volume_server_pb.VolumeServer.VolumeEcShardsCopy:output_type -> volume_server_pb.VolumeEcShardsCopyResponse - 66, // 116: volume_server_pb.VolumeServer.VolumeEcShardsDelete:output_type -> volume_server_pb.VolumeEcShardsDeleteResponse - 68, // 117: volume_server_pb.VolumeServer.VolumeEcShardsMount:output_type -> volume_server_pb.VolumeEcShardsMountResponse - 70, // 118: volume_server_pb.VolumeServer.VolumeEcShardsUnmount:output_type -> volume_server_pb.VolumeEcShardsUnmountResponse - 72, // 119: volume_server_pb.VolumeServer.VolumeEcShardRead:output_type -> volume_server_pb.VolumeEcShardReadResponse - 74, // 120: volume_server_pb.VolumeServer.VolumeEcBlobDelete:output_type -> volume_server_pb.VolumeEcBlobDeleteResponse - 76, // 121: volume_server_pb.VolumeServer.VolumeEcShardsToVolume:output_type -> volume_server_pb.VolumeEcShardsToVolumeResponse - 78, // 122: volume_server_pb.VolumeServer.VolumeEcShardsInfo:output_type -> volume_server_pb.VolumeEcShardsInfoResponse - 89, // 123: volume_server_pb.VolumeServer.VolumeTierMoveDatToRemote:output_type -> volume_server_pb.VolumeTierMoveDatToRemoteResponse - 91, // 124: volume_server_pb.VolumeServer.VolumeTierMoveDatFromRemote:output_type -> volume_server_pb.VolumeTierMoveDatFromRemoteResponse - 93, // 125: volume_server_pb.VolumeServer.VolumeServerStatus:output_type -> volume_server_pb.VolumeServerStatusResponse - 95, // 126: volume_server_pb.VolumeServer.VolumeServerLeave:output_type -> volume_server_pb.VolumeServerLeaveResponse - 97, // 127: volume_server_pb.VolumeServer.FetchAndWriteNeedle:output_type -> volume_server_pb.FetchAndWriteNeedleResponse - 99, // 128: volume_server_pb.VolumeServer.ScrubVolume:output_type -> volume_server_pb.ScrubVolumeResponse - 101, // 129: volume_server_pb.VolumeServer.ScrubEcVolume:output_type -> volume_server_pb.ScrubEcVolumeResponse - 103, // 130: volume_server_pb.VolumeServer.Query:output_type -> volume_server_pb.QueriedStripe - 105, // 131: volume_server_pb.VolumeServer.VolumeNeedleStatus:output_type -> volume_server_pb.VolumeNeedleStatusResponse - 107, // 132: volume_server_pb.VolumeServer.Ping:output_type -> volume_server_pb.PingResponse - 109, // 133: volume_server_pb.VolumeServer.AllocateBlockVolume:output_type -> volume_server_pb.AllocateBlockVolumeResponse - 111, // 134: volume_server_pb.VolumeServer.VolumeServerDeleteBlockVolume:output_type -> volume_server_pb.VolumeServerDeleteBlockVolumeResponse - 113, // 135: volume_server_pb.VolumeServer.SnapshotBlockVolume:output_type -> volume_server_pb.SnapshotBlockVolumeResponse - 115, // 136: volume_server_pb.VolumeServer.DeleteBlockSnapshot:output_type -> volume_server_pb.DeleteBlockSnapshotResponse - 117, // 137: volume_server_pb.VolumeServer.ListBlockSnapshots:output_type -> volume_server_pb.ListBlockSnapshotsResponse - 120, // 138: volume_server_pb.VolumeServer.ExpandBlockVolume:output_type -> volume_server_pb.ExpandBlockVolumeResponse - 122, // 139: volume_server_pb.VolumeServer.PrepareExpandBlockVolume:output_type -> volume_server_pb.PrepareExpandBlockVolumeResponse - 124, // 140: volume_server_pb.VolumeServer.CommitExpandBlockVolume:output_type -> volume_server_pb.CommitExpandBlockVolumeResponse - 126, // 141: volume_server_pb.VolumeServer.CancelExpandBlockVolume:output_type -> volume_server_pb.CancelExpandBlockVolumeResponse - 85, // [85:142] is the sub-list for method output_type - 28, // [28:85] is the sub-list for method input_type + 118, // 80: volume_server_pb.VolumeServer.ListBlockSnapshots:input_type -> volume_server_pb.ListBlockSnapshotsRequest + 116, // 81: volume_server_pb.VolumeServer.RestoreBlockSnapshot:input_type -> volume_server_pb.RestoreBlockSnapshotRequest + 121, // 82: volume_server_pb.VolumeServer.ExpandBlockVolume:input_type -> volume_server_pb.ExpandBlockVolumeRequest + 123, // 83: volume_server_pb.VolumeServer.PrepareExpandBlockVolume:input_type -> volume_server_pb.PrepareExpandBlockVolumeRequest + 125, // 84: volume_server_pb.VolumeServer.CommitExpandBlockVolume:input_type -> volume_server_pb.CommitExpandBlockVolumeRequest + 127, // 85: volume_server_pb.VolumeServer.CancelExpandBlockVolume:input_type -> volume_server_pb.CancelExpandBlockVolumeRequest + 3, // 86: volume_server_pb.VolumeServer.BatchDelete:output_type -> volume_server_pb.BatchDeleteResponse + 7, // 87: volume_server_pb.VolumeServer.VacuumVolumeCheck:output_type -> volume_server_pb.VacuumVolumeCheckResponse + 9, // 88: volume_server_pb.VolumeServer.VacuumVolumeCompact:output_type -> volume_server_pb.VacuumVolumeCompactResponse + 11, // 89: volume_server_pb.VolumeServer.VacuumVolumeCommit:output_type -> volume_server_pb.VacuumVolumeCommitResponse + 13, // 90: volume_server_pb.VolumeServer.VacuumVolumeCleanup:output_type -> volume_server_pb.VacuumVolumeCleanupResponse + 15, // 91: volume_server_pb.VolumeServer.DeleteCollection:output_type -> volume_server_pb.DeleteCollectionResponse + 17, // 92: volume_server_pb.VolumeServer.AllocateVolume:output_type -> volume_server_pb.AllocateVolumeResponse + 19, // 93: volume_server_pb.VolumeServer.VolumeSyncStatus:output_type -> volume_server_pb.VolumeSyncStatusResponse + 21, // 94: volume_server_pb.VolumeServer.VolumeIncrementalCopy:output_type -> volume_server_pb.VolumeIncrementalCopyResponse + 23, // 95: volume_server_pb.VolumeServer.VolumeMount:output_type -> volume_server_pb.VolumeMountResponse + 25, // 96: volume_server_pb.VolumeServer.VolumeUnmount:output_type -> volume_server_pb.VolumeUnmountResponse + 27, // 97: volume_server_pb.VolumeServer.VolumeDelete:output_type -> volume_server_pb.VolumeDeleteResponse + 29, // 98: volume_server_pb.VolumeServer.VolumeMarkReadonly:output_type -> volume_server_pb.VolumeMarkReadonlyResponse + 31, // 99: volume_server_pb.VolumeServer.VolumeMarkWritable:output_type -> volume_server_pb.VolumeMarkWritableResponse + 33, // 100: volume_server_pb.VolumeServer.VolumeConfigure:output_type -> volume_server_pb.VolumeConfigureResponse + 35, // 101: volume_server_pb.VolumeServer.VolumeStatus:output_type -> volume_server_pb.VolumeStatusResponse + 37, // 102: volume_server_pb.VolumeServer.GetState:output_type -> volume_server_pb.GetStateResponse + 39, // 103: volume_server_pb.VolumeServer.SetState:output_type -> volume_server_pb.SetStateResponse + 41, // 104: volume_server_pb.VolumeServer.VolumeCopy:output_type -> volume_server_pb.VolumeCopyResponse + 81, // 105: volume_server_pb.VolumeServer.ReadVolumeFileStatus:output_type -> volume_server_pb.ReadVolumeFileStatusResponse + 43, // 106: volume_server_pb.VolumeServer.CopyFile:output_type -> volume_server_pb.CopyFileResponse + 46, // 107: volume_server_pb.VolumeServer.ReceiveFile:output_type -> volume_server_pb.ReceiveFileResponse + 48, // 108: volume_server_pb.VolumeServer.ReadNeedleBlob:output_type -> volume_server_pb.ReadNeedleBlobResponse + 50, // 109: volume_server_pb.VolumeServer.ReadNeedleMeta:output_type -> volume_server_pb.ReadNeedleMetaResponse + 52, // 110: volume_server_pb.VolumeServer.WriteNeedleBlob:output_type -> volume_server_pb.WriteNeedleBlobResponse + 54, // 111: volume_server_pb.VolumeServer.ReadAllNeedles:output_type -> volume_server_pb.ReadAllNeedlesResponse + 56, // 112: volume_server_pb.VolumeServer.VolumeTailSender:output_type -> volume_server_pb.VolumeTailSenderResponse + 58, // 113: volume_server_pb.VolumeServer.VolumeTailReceiver:output_type -> volume_server_pb.VolumeTailReceiverResponse + 60, // 114: volume_server_pb.VolumeServer.VolumeEcShardsGenerate:output_type -> volume_server_pb.VolumeEcShardsGenerateResponse + 62, // 115: volume_server_pb.VolumeServer.VolumeEcShardsRebuild:output_type -> volume_server_pb.VolumeEcShardsRebuildResponse + 64, // 116: volume_server_pb.VolumeServer.VolumeEcShardsCopy:output_type -> volume_server_pb.VolumeEcShardsCopyResponse + 66, // 117: volume_server_pb.VolumeServer.VolumeEcShardsDelete:output_type -> volume_server_pb.VolumeEcShardsDeleteResponse + 68, // 118: volume_server_pb.VolumeServer.VolumeEcShardsMount:output_type -> volume_server_pb.VolumeEcShardsMountResponse + 70, // 119: volume_server_pb.VolumeServer.VolumeEcShardsUnmount:output_type -> volume_server_pb.VolumeEcShardsUnmountResponse + 72, // 120: volume_server_pb.VolumeServer.VolumeEcShardRead:output_type -> volume_server_pb.VolumeEcShardReadResponse + 74, // 121: volume_server_pb.VolumeServer.VolumeEcBlobDelete:output_type -> volume_server_pb.VolumeEcBlobDeleteResponse + 76, // 122: volume_server_pb.VolumeServer.VolumeEcShardsToVolume:output_type -> volume_server_pb.VolumeEcShardsToVolumeResponse + 78, // 123: volume_server_pb.VolumeServer.VolumeEcShardsInfo:output_type -> volume_server_pb.VolumeEcShardsInfoResponse + 89, // 124: volume_server_pb.VolumeServer.VolumeTierMoveDatToRemote:output_type -> volume_server_pb.VolumeTierMoveDatToRemoteResponse + 91, // 125: volume_server_pb.VolumeServer.VolumeTierMoveDatFromRemote:output_type -> volume_server_pb.VolumeTierMoveDatFromRemoteResponse + 93, // 126: volume_server_pb.VolumeServer.VolumeServerStatus:output_type -> volume_server_pb.VolumeServerStatusResponse + 95, // 127: volume_server_pb.VolumeServer.VolumeServerLeave:output_type -> volume_server_pb.VolumeServerLeaveResponse + 97, // 128: volume_server_pb.VolumeServer.FetchAndWriteNeedle:output_type -> volume_server_pb.FetchAndWriteNeedleResponse + 99, // 129: volume_server_pb.VolumeServer.ScrubVolume:output_type -> volume_server_pb.ScrubVolumeResponse + 101, // 130: volume_server_pb.VolumeServer.ScrubEcVolume:output_type -> volume_server_pb.ScrubEcVolumeResponse + 103, // 131: volume_server_pb.VolumeServer.Query:output_type -> volume_server_pb.QueriedStripe + 105, // 132: volume_server_pb.VolumeServer.VolumeNeedleStatus:output_type -> volume_server_pb.VolumeNeedleStatusResponse + 107, // 133: volume_server_pb.VolumeServer.Ping:output_type -> volume_server_pb.PingResponse + 109, // 134: volume_server_pb.VolumeServer.AllocateBlockVolume:output_type -> volume_server_pb.AllocateBlockVolumeResponse + 111, // 135: volume_server_pb.VolumeServer.VolumeServerDeleteBlockVolume:output_type -> volume_server_pb.VolumeServerDeleteBlockVolumeResponse + 113, // 136: volume_server_pb.VolumeServer.SnapshotBlockVolume:output_type -> volume_server_pb.SnapshotBlockVolumeResponse + 115, // 137: volume_server_pb.VolumeServer.DeleteBlockSnapshot:output_type -> volume_server_pb.DeleteBlockSnapshotResponse + 119, // 138: volume_server_pb.VolumeServer.ListBlockSnapshots:output_type -> volume_server_pb.ListBlockSnapshotsResponse + 117, // 139: volume_server_pb.VolumeServer.RestoreBlockSnapshot:output_type -> volume_server_pb.RestoreBlockSnapshotResponse + 122, // 140: volume_server_pb.VolumeServer.ExpandBlockVolume:output_type -> volume_server_pb.ExpandBlockVolumeResponse + 124, // 141: volume_server_pb.VolumeServer.PrepareExpandBlockVolume:output_type -> volume_server_pb.PrepareExpandBlockVolumeResponse + 126, // 142: volume_server_pb.VolumeServer.CommitExpandBlockVolume:output_type -> volume_server_pb.CommitExpandBlockVolumeResponse + 128, // 143: volume_server_pb.VolumeServer.CancelExpandBlockVolume:output_type -> volume_server_pb.CancelExpandBlockVolumeResponse + 86, // [86:144] is the sub-list for method output_type + 28, // [28:86] is the sub-list for method input_type 28, // [28:28] is the sub-list for extension type_name 28, // [28:28] is the sub-list for extension extendee 0, // [0:28] is the sub-list for field type_name @@ -8643,7 +8741,7 @@ func file_volume_server_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_volume_server_proto_rawDesc), len(file_volume_server_proto_rawDesc)), NumEnums: 1, - NumMessages: 135, + NumMessages: 137, NumExtensions: 0, NumServices: 1, }, diff --git a/weed/pb/volume_server_pb/volume_server_grpc.pb.go b/weed/pb/volume_server_pb/volume_server_grpc.pb.go index ef1a043aa..85aa6d176 100644 --- a/weed/pb/volume_server_pb/volume_server_grpc.pb.go +++ b/weed/pb/volume_server_pb/volume_server_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.1 -// - protoc v5.29.5 +// - protoc v3.21.12 // source: volume_server.proto package volume_server_pb @@ -72,6 +72,7 @@ const ( VolumeServer_SnapshotBlockVolume_FullMethodName = "/volume_server_pb.VolumeServer/SnapshotBlockVolume" VolumeServer_DeleteBlockSnapshot_FullMethodName = "/volume_server_pb.VolumeServer/DeleteBlockSnapshot" VolumeServer_ListBlockSnapshots_FullMethodName = "/volume_server_pb.VolumeServer/ListBlockSnapshots" + VolumeServer_RestoreBlockSnapshot_FullMethodName = "/volume_server_pb.VolumeServer/RestoreBlockSnapshot" VolumeServer_ExpandBlockVolume_FullMethodName = "/volume_server_pb.VolumeServer/ExpandBlockVolume" VolumeServer_PrepareExpandBlockVolume_FullMethodName = "/volume_server_pb.VolumeServer/PrepareExpandBlockVolume" VolumeServer_CommitExpandBlockVolume_FullMethodName = "/volume_server_pb.VolumeServer/CommitExpandBlockVolume" @@ -82,7 +83,7 @@ const ( // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type VolumeServerClient interface { - // Experts only: takes multiple fid parameters. This function does not propagate deletes to replicas. + //Experts only: takes multiple fid parameters. This function does not propagate deletes to replicas. BatchDelete(ctx context.Context, in *BatchDeleteRequest, opts ...grpc.CallOption) (*BatchDeleteResponse, error) VacuumVolumeCheck(ctx context.Context, in *VacuumVolumeCheckRequest, opts ...grpc.CallOption) (*VacuumVolumeCheckResponse, error) VacuumVolumeCompact(ctx context.Context, in *VacuumVolumeCompactRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[VacuumVolumeCompactResponse], error) @@ -143,6 +144,7 @@ type VolumeServerClient interface { SnapshotBlockVolume(ctx context.Context, in *SnapshotBlockVolumeRequest, opts ...grpc.CallOption) (*SnapshotBlockVolumeResponse, error) DeleteBlockSnapshot(ctx context.Context, in *DeleteBlockSnapshotRequest, opts ...grpc.CallOption) (*DeleteBlockSnapshotResponse, error) ListBlockSnapshots(ctx context.Context, in *ListBlockSnapshotsRequest, opts ...grpc.CallOption) (*ListBlockSnapshotsResponse, error) + RestoreBlockSnapshot(ctx context.Context, in *RestoreBlockSnapshotRequest, opts ...grpc.CallOption) (*RestoreBlockSnapshotResponse, error) ExpandBlockVolume(ctx context.Context, in *ExpandBlockVolumeRequest, opts ...grpc.CallOption) (*ExpandBlockVolumeResponse, error) PrepareExpandBlockVolume(ctx context.Context, in *PrepareExpandBlockVolumeRequest, opts ...grpc.CallOption) (*PrepareExpandBlockVolumeResponse, error) CommitExpandBlockVolume(ctx context.Context, in *CommitExpandBlockVolumeRequest, opts ...grpc.CallOption) (*CommitExpandBlockVolumeResponse, error) @@ -780,6 +782,16 @@ func (c *volumeServerClient) ListBlockSnapshots(ctx context.Context, in *ListBlo return out, nil } +func (c *volumeServerClient) RestoreBlockSnapshot(ctx context.Context, in *RestoreBlockSnapshotRequest, opts ...grpc.CallOption) (*RestoreBlockSnapshotResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RestoreBlockSnapshotResponse) + err := c.cc.Invoke(ctx, VolumeServer_RestoreBlockSnapshot_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *volumeServerClient) ExpandBlockVolume(ctx context.Context, in *ExpandBlockVolumeRequest, opts ...grpc.CallOption) (*ExpandBlockVolumeResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ExpandBlockVolumeResponse) @@ -824,7 +836,7 @@ func (c *volumeServerClient) CancelExpandBlockVolume(ctx context.Context, in *Ca // All implementations must embed UnimplementedVolumeServerServer // for forward compatibility. type VolumeServerServer interface { - // Experts only: takes multiple fid parameters. This function does not propagate deletes to replicas. + //Experts only: takes multiple fid parameters. This function does not propagate deletes to replicas. BatchDelete(context.Context, *BatchDeleteRequest) (*BatchDeleteResponse, error) VacuumVolumeCheck(context.Context, *VacuumVolumeCheckRequest) (*VacuumVolumeCheckResponse, error) VacuumVolumeCompact(*VacuumVolumeCompactRequest, grpc.ServerStreamingServer[VacuumVolumeCompactResponse]) error @@ -885,6 +897,7 @@ type VolumeServerServer interface { SnapshotBlockVolume(context.Context, *SnapshotBlockVolumeRequest) (*SnapshotBlockVolumeResponse, error) DeleteBlockSnapshot(context.Context, *DeleteBlockSnapshotRequest) (*DeleteBlockSnapshotResponse, error) ListBlockSnapshots(context.Context, *ListBlockSnapshotsRequest) (*ListBlockSnapshotsResponse, error) + RestoreBlockSnapshot(context.Context, *RestoreBlockSnapshotRequest) (*RestoreBlockSnapshotResponse, error) ExpandBlockVolume(context.Context, *ExpandBlockVolumeRequest) (*ExpandBlockVolumeResponse, error) PrepareExpandBlockVolume(context.Context, *PrepareExpandBlockVolumeRequest) (*PrepareExpandBlockVolumeResponse, error) CommitExpandBlockVolume(context.Context, *CommitExpandBlockVolumeRequest) (*CommitExpandBlockVolumeResponse, error) @@ -1058,6 +1071,9 @@ func (UnimplementedVolumeServerServer) DeleteBlockSnapshot(context.Context, *Del func (UnimplementedVolumeServerServer) ListBlockSnapshots(context.Context, *ListBlockSnapshotsRequest) (*ListBlockSnapshotsResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListBlockSnapshots not implemented") } +func (UnimplementedVolumeServerServer) RestoreBlockSnapshot(context.Context, *RestoreBlockSnapshotRequest) (*RestoreBlockSnapshotResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RestoreBlockSnapshot not implemented") +} func (UnimplementedVolumeServerServer) ExpandBlockVolume(context.Context, *ExpandBlockVolumeRequest) (*ExpandBlockVolumeResponse, error) { return nil, status.Error(codes.Unimplemented, "method ExpandBlockVolume not implemented") } @@ -1964,6 +1980,24 @@ func _VolumeServer_ListBlockSnapshots_Handler(srv interface{}, ctx context.Conte return interceptor(ctx, in, info, handler) } +func _VolumeServer_RestoreBlockSnapshot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RestoreBlockSnapshotRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(VolumeServerServer).RestoreBlockSnapshot(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: VolumeServer_RestoreBlockSnapshot_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(VolumeServerServer).RestoreBlockSnapshot(ctx, req.(*RestoreBlockSnapshotRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _VolumeServer_ExpandBlockVolume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ExpandBlockVolumeRequest) if err := dec(in); err != nil { @@ -2211,6 +2245,10 @@ var VolumeServer_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListBlockSnapshots", Handler: _VolumeServer_ListBlockSnapshots_Handler, }, + { + MethodName: "RestoreBlockSnapshot", + Handler: _VolumeServer_RestoreBlockSnapshot_Handler, + }, { MethodName: "ExpandBlockVolume", Handler: _VolumeServer_ExpandBlockVolume_Handler, diff --git a/weed/server/block_recovery_adversarial_test.go b/weed/server/block_recovery_adversarial_test.go new file mode 100644 index 000000000..d155fe85b --- /dev/null +++ b/weed/server/block_recovery_adversarial_test.go @@ -0,0 +1,166 @@ +package weed_server + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/seaweedfs/seaweedfs/weed/storage/blockvol" +) + +// ============================================================ +// Phase 09 P4: Adversarial tests for RecoveryManager +// ============================================================ + +// --- Adversarial 1: Rapid triple supersede --- + +func TestAdversarial_P4_RapidTripleSupersede(t *testing.T) { + bs, volPath := createTestBlockServiceWithVol(t) + rm := bs.v2Recovery + + // Write data so recovery has work. + bs.blockStore.WithVolume(volPath, func(vol *blockvol.BlockVol) error { + for i := 0; i < 5; i++ { + vol.WriteLBA(uint64(i), make([]byte, 4096)) + } + return nil + }) + + var executionCount atomic.Int32 + + rm.OnBeforeExecute = func(replicaID string) { + executionCount.Add(1) + // Simulate slow execution — each goroutine takes 100ms. + time.Sleep(100 * time.Millisecond) + } + + makeAssignment := func(epoch uint64) []blockvol.BlockVolumeAssignment { + return []blockvol.BlockVolumeAssignment{ + {Path: volPath, Epoch: epoch, Role: uint32(blockvol.RolePrimary), + ReplicaServerID: "vs2", ReplicaDataAddr: "10.0.0.2:9333", ReplicaCtrlAddr: "10.0.0.2:9334"}, + } + } + + // Rapid fire: epoch 1, 2, 3 in quick succession. + // Each ProcessAssignments must drain the previous before starting the next. + bs.ProcessAssignments(makeAssignment(1)) + time.Sleep(20 * time.Millisecond) // let goroutine start + + bs.ProcessAssignments(makeAssignment(2)) + bs.ProcessAssignments(makeAssignment(3)) + + // Wait for all to settle. + time.Sleep(500 * time.Millisecond) + + // Must have at most 1 active task (the final one, or 0 if it completed). + count := rm.ActiveTaskCount() + if count > 1 { + t.Fatalf("goroutine leak: %d active tasks after triple supersede", count) + } + + t.Logf("adversarial 1: triple supersede — %d executions, %d active tasks, no leak", + executionCount.Load(), count) +} + +// --- Adversarial 2: Shutdown during slow goroutine (not permanently stuck) --- + +func TestAdversarial_P4_ShutdownDuringSlow(t *testing.T) { + bs, volPath := createTestBlockServiceWithVol(t) + rm := bs.v2Recovery + + bs.blockStore.WithVolume(volPath, func(vol *blockvol.BlockVol) error { + for i := 0; i < 5; i++ { + vol.WriteLBA(uint64(i), make([]byte, 4096)) + } + return nil + }) + + started := make(chan struct{}, 1) + rm.OnBeforeExecute = func(replicaID string) { + started <- struct{}{} + // Simulate slow execution — 500ms, not permanently stuck. + time.Sleep(500 * time.Millisecond) + } + + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + {Path: volPath, Epoch: 1, Role: uint32(blockvol.RolePrimary), + ReplicaServerID: "vs2", ReplicaDataAddr: "10.0.0.2:9333", ReplicaCtrlAddr: "10.0.0.2:9334"}, + }) + + select { + case <-started: + case <-time.After(5 * time.Second): + t.Fatal("goroutine did not start") + } + + // Shutdown while goroutine is in slow execution. + done := make(chan bool, 1) + go func() { + rm.Shutdown() + done <- true + }() + + select { + case <-done: + t.Log("adversarial 2: Shutdown completed after slow goroutine finished") + case <-time.After(5 * time.Second): + t.Fatal("Shutdown did not complete within 5 seconds") + } + + if rm.ActiveTaskCount() != 0 { + t.Fatalf("tasks after shutdown: %d", rm.ActiveTaskCount()) + } +} + +// --- Adversarial 3: Two replicas, no cross-interference --- + +func TestAdversarial_P4_TwoReplicas_NoInterference(t *testing.T) { + bs, volPath := createTestBlockServiceWithVol(t) + rm := bs.v2Recovery + + bs.blockStore.WithVolume(volPath, func(vol *blockvol.BlockVol) error { + for i := 0; i < 5; i++ { + vol.WriteLBA(uint64(i), make([]byte, 4096)) + } + return nil + }) + + var execIDs []string + var execMu = make(chan struct{}, 1) + rm.OnBeforeExecute = func(replicaID string) { + execMu <- struct{}{} + execIDs = append(execIDs, replicaID) + <-execMu + } + + // Two different replicas in one assignment batch. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + {Path: volPath, Epoch: 1, Role: uint32(blockvol.RolePrimary), + ReplicaServerID: "vs2", ReplicaDataAddr: "10.0.0.2:9333", ReplicaCtrlAddr: "10.0.0.2:9334", + ReplicaAddrs: []blockvol.ReplicaAddr{ + {DataAddr: "10.0.0.2:9333", CtrlAddr: "10.0.0.2:9334", ServerID: "vs2"}, + {DataAddr: "10.0.0.3:9333", CtrlAddr: "10.0.0.3:9334", ServerID: "vs3"}, + }, + }, + }) + + time.Sleep(300 * time.Millisecond) + + // Both replicas should have gotten recovery goroutines. + // No cross-interference: each has its own task with its own done channel. + replicaID1 := volPath + "/vs2" + replicaID2 := volPath + "/vs3" + + s1 := bs.v2Orchestrator.Registry.Sender(replicaID1) + s2 := bs.v2Orchestrator.Registry.Sender(replicaID2) + + if s1 == nil { + t.Fatal("sender for vs2 not created") + } + if s2 == nil { + t.Fatal("sender for vs3 not created") + } + + t.Logf("adversarial 3: two replicas — vs2=%s vs3=%s, no cross-interference", + s1.State(), s2.State()) +} diff --git a/weed/server/block_recovery_test.go b/weed/server/block_recovery_test.go new file mode 100644 index 000000000..fede33635 --- /dev/null +++ b/weed/server/block_recovery_test.go @@ -0,0 +1,312 @@ +package weed_server + +import ( + "path/filepath" + "testing" + "time" + + engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication" + "github.com/seaweedfs/seaweedfs/weed/storage" + "github.com/seaweedfs/seaweedfs/weed/storage/blockvol" + "github.com/seaweedfs/seaweedfs/weed/storage/blockvol/v2bridge" +) + +// ============================================================ +// Phase 09 P4: Stronger live runtime ownership +// +// Proofs: +// 1. Live-path with real vol: ProcessAssignments → plan → executor +// 2. Serialized replacement: old drained before new starts +// 3. Shutdown drains all tasks +// 4. Rebuild address scoped +// 5. No split ownership under replacement +// ============================================================ + +func createTestBlockServiceWithVol(t *testing.T) (*BlockService, string) { + t.Helper() + dir := t.TempDir() + + // Create a real blockvol. + volPath := filepath.Join(dir, "vol1.blk") + vol, err := blockvol.CreateBlockVol(volPath, blockvol.CreateOptions{ + VolumeSize: 1 * 1024 * 1024, + BlockSize: 4096, + WALSize: 256 * 1024, + }) + if err != nil { + t.Fatalf("CreateBlockVol: %v", err) + } + vol.Close() + + // Build BlockService with a real BlockVolumeStore. + store := storage.NewBlockVolumeStore() + if _, err := store.AddBlockVolume(volPath, ""); err != nil { + t.Fatalf("AddBlockVolume: %v", err) + } + + bs := &BlockService{ + blockStore: store, + blockDir: dir, + listenAddr: "127.0.0.1:3260", + localServerID: "test-server-1", + v2Bridge: v2bridge.NewControlBridge(), + v2Orchestrator: engine.NewRecoveryOrchestrator(), + } + bs.v2Recovery = NewRecoveryManager(bs) + + t.Cleanup(func() { + bs.v2Recovery.Shutdown() + store.Close() + }) + + return bs, volPath +} + +// --- Live-path with real vol: reaches planning --- + +func TestP4_LivePath_RealVol_ReachesPlan(t *testing.T) { + bs, volPath := createTestBlockServiceWithVol(t) + + // Write data to the vol so recovery has something to plan against. + if err := bs.blockStore.WithVolume(volPath, func(vol *blockvol.BlockVol) error { + for i := 0; i < 5; i++ { + vol.WriteLBA(uint64(i), make([]byte, 4096)) + } + return nil + }); err != nil { + t.Fatalf("write: %v", err) + } + + // Process assignment through real path. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + { + Path: volPath, + Epoch: 1, + Role: uint32(blockvol.RolePrimary), + ReplicaServerID: "vs2", + ReplicaDataAddr: "10.0.0.2:9333", + ReplicaCtrlAddr: "10.0.0.2:9334", + }, + }) + + // Give recovery goroutine time to reach planning. + time.Sleep(200 * time.Millisecond) + + // Verify: sender exists and engine processed it. + replicaID := volPath + "/vs2" + s := bs.v2Orchestrator.Registry.Sender(replicaID) + if s == nil { + t.Fatal("sender not created") + } + + // Assert the full chain completed: plan → executor → in_sync. + events := bs.v2Orchestrator.Log.EventsFor(replicaID) + required := map[string]bool{ + "plan_catchup": false, + "exec_catchup_started": false, + "exec_completed": false, + } + for _, ev := range events { + if _, ok := required[ev.Event]; ok { + required[ev.Event] = true + } + } + for event, found := range required { + if !found { + t.Fatalf("missing required event: %s (events=%d)", event, len(events)) + } + } + + // Assert final sender state. + if s.State() != engine.StateInSync { + t.Fatalf("sender state=%s, want in_sync", s.State()) + } + + t.Log("P4 live-path: ProcessAssignments → plan_catchup → exec_catchup_started → exec_completed → in_sync") +} + +// --- Serialized replacement: old drained before new starts --- + +func TestP4_SerializedReplacement_DrainsBeforeStart(t *testing.T) { + bs, volPath := createTestBlockServiceWithVol(t) + rm := bs.v2Recovery + + // Write data so recovery has work to do. + if err := bs.blockStore.WithVolume(volPath, func(vol *blockvol.BlockVol) error { + for i := 0; i < 10; i++ { + vol.WriteLBA(uint64(i), make([]byte, 4096)) + } + return nil + }); err != nil { + t.Fatalf("write: %v", err) + } + + // Hook: block the first recovery goroutine so it is DEFINITELY still + // alive when the supersede arrives. Release it via channel. + holdFirst := make(chan struct{}) + firstReached := make(chan struct{}, 1) + callCount := 0 + rm.OnBeforeExecute = func(replicaID string) { + callCount++ + if callCount == 1 { + // First call: signal that we're alive, then block. + firstReached <- struct{}{} + <-holdFirst // block until test releases + } + // Second call (replacement): proceed immediately. + } + + replicaID := volPath + "/vs2" + + // Epoch 1: start recovery goroutine (will block at OnBeforeExecute). + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + {Path: volPath, Epoch: 1, Role: uint32(blockvol.RolePrimary), + ReplicaServerID: "vs2", ReplicaDataAddr: "10.0.0.2:9333", ReplicaCtrlAddr: "10.0.0.2:9334"}, + }) + + // Wait for the first goroutine to reach the hook (still alive). + select { + case <-firstReached: + t.Log("first recovery goroutine is alive and blocked at OnBeforeExecute") + case <-time.After(5 * time.Second): + t.Fatal("first goroutine did not reach OnBeforeExecute") + } + + // Capture the old task's done channel WHILE it is still running. + rm.mu.Lock() + oldTask := rm.tasks[replicaID] + rm.mu.Unlock() + if oldTask == nil { + t.Fatal("old task must exist while goroutine is blocked") + } + oldDone := oldTask.done + + // Verify: old done channel is NOT closed yet. + select { + case <-oldDone: + t.Fatal("old task done should NOT be closed yet") + default: + t.Log("confirmed: old task still running (done channel open)") + } + + // Epoch 2: supersede. cancelAndDrain will cancel the old context and + // block on oldDone. We release the hold from another goroutine so + // cancelAndDrain can complete. + go func() { + time.Sleep(50 * time.Millisecond) + close(holdFirst) // release the blocked first goroutine + }() + + // This call blocks inside cancelAndDrain until the old goroutine exits. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + {Path: volPath, Epoch: 2, Role: uint32(blockvol.RolePrimary), + ReplicaServerID: "vs2", ReplicaDataAddr: "10.0.0.2:9333", ReplicaCtrlAddr: "10.0.0.2:9334"}, + }) + + // After ProcessAssignments returns, the old goroutine MUST be drained. + select { + case <-oldDone: + t.Log("confirmed: old task drained (done channel closed) before replacement started") + default: + t.Fatal("old task done channel still open after ProcessAssignments returned") + } + + time.Sleep(200 * time.Millisecond) + + // At most 1 active task. + count := rm.ActiveTaskCount() + if count > 1 { + t.Fatalf("overlap: %d tasks after replacement", count) + } + + t.Log("P4 serialized: old goroutine blocked → supersede → drain waited → old exited → replacement started") +} + +// --- Shutdown drains all --- + +func TestP4_ShutdownDrain(t *testing.T) { + bs, volPath := createTestBlockServiceWithVol(t) + rm := bs.v2Recovery + + // Hook: block the goroutine so it's alive when shutdown arrives. + holdTask := make(chan struct{}) + taskReached := make(chan struct{}, 1) + rm.OnBeforeExecute = func(replicaID string) { + taskReached <- struct{}{} + <-holdTask + } + + // Write data so recovery has work. + if err := bs.blockStore.WithVolume(volPath, func(vol *blockvol.BlockVol) error { + for i := 0; i < 5; i++ { + vol.WriteLBA(uint64(i), make([]byte, 4096)) + } + return nil + }); err != nil { + t.Fatalf("write: %v", err) + } + + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + {Path: volPath, Epoch: 1, Role: uint32(blockvol.RolePrimary), + ReplicaServerID: "vs2", ReplicaDataAddr: "10.0.0.2:9333", ReplicaCtrlAddr: "10.0.0.2:9334"}, + }) + + // Wait for task to be alive. + select { + case <-taskReached: + t.Log("task is alive and blocked before shutdown") + case <-time.After(5 * time.Second): + t.Fatal("task did not reach hook") + } + + // Assert: a live task exists. + if rm.ActiveTaskCount() == 0 { + t.Fatal("expected live task before shutdown") + } + + // Release the blocked goroutine from another goroutine so Shutdown can drain. + go func() { + time.Sleep(50 * time.Millisecond) + close(holdTask) + }() + + done := make(chan bool, 1) + go func() { + rm.Shutdown() + done <- true + }() + + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("Shutdown did not complete within 5 seconds") + } + + if rm.ActiveTaskCount() != 0 { + t.Fatalf("expected 0 active tasks, got %d", rm.ActiveTaskCount()) + } + + t.Log("P4 shutdown: live task existed → shutdown drained it → 0 active") +} + +// --- Rebuild address scoped --- + +func TestP4_RebuildAddrScoped(t *testing.T) { + bs, _ := createTestBlockServiceWithVol(t) + rm := bs.v2Recovery + + assignments := []blockvol.BlockVolumeAssignment{ + {Path: "/data/vol1.blk", RebuildAddr: "10.0.0.1:5000"}, + {Path: "/data/vol2.blk", RebuildAddr: "10.0.0.2:5000"}, + } + + if addr := rm.deriveRebuildAddr("/data/vol1.blk/vs2", assignments); addr != "10.0.0.1:5000" { + t.Fatalf("vol1 addr=%s", addr) + } + if addr := rm.deriveRebuildAddr("/data/vol2.blk/vs3", assignments); addr != "10.0.0.2:5000" { + t.Fatalf("vol2 addr=%s", addr) + } + if addr := rm.deriveRebuildAddr("/data/vol3.blk/vs4", assignments); addr != "" { + t.Fatalf("vol3 addr=%s", addr) + } +} diff --git a/weed/server/master_block_registry.go b/weed/server/master_block_registry.go index 687d581cd..edb831811 100644 --- a/weed/server/master_block_registry.go +++ b/weed/server/master_block_registry.go @@ -1127,6 +1127,12 @@ func (r *BlockVolumeRegistry) evaluatePromotionLocked(entry *BlockVolumeEntry) P freshnessCutoff = 60 * time.Second } primaryLSN := entry.WALHeadLSN + // EC-6 fix: when the primary is dead, its last-reported WALHeadLSN + // includes entries that were fsync'd locally but never shipped. + // The replica can never catch up because the primary is gone. + // Skip the WAL LSN gate so the best available replica is promoted, + // accepting that the last few unshipped entries may be lost. + primaryAlive := r.blockServers[entry.VolumeServer] != nil bestIdx := -1 for i := range entry.Replicas { @@ -1149,8 +1155,11 @@ func (r *BlockVolumeRegistry) evaluatePromotionLocked(entry *BlockVolumeEntry) P }) continue } - // Gate 2: WAL LSN recency (skip if primary LSN is 0 — no data yet, all eligible). - if primaryLSN > 0 && ri.WALHeadLSN+r.promotionLSNTolerance < primaryLSN { + // Gate 2: WAL LSN recency. + // Skip if primary LSN is 0 (no data yet — all eligible). + // EC-6 fix: also skip if primary is dead — its LSN is stale and + // the replica can never catch up. Promote the best available. + if primaryAlive && primaryLSN > 0 && ri.WALHeadLSN+r.promotionLSNTolerance < primaryLSN { result.Rejections = append(result.Rejections, PromotionRejection{ Server: ri.Server, Reason: "wal_lag", diff --git a/weed/server/master_block_registry_test.go b/weed/server/master_block_registry_test.go index f0ecb7e23..1f656ee81 100644 --- a/weed/server/master_block_registry_test.go +++ b/weed/server/master_block_registry_test.go @@ -870,9 +870,12 @@ func TestRegistry_PromoteBestReplica_StaleHeartbeatIneligible(t *testing.T) { } } -// Fix #2: Replica with WAL lag too large is not eligible. +// Fix #2: Replica with WAL lag too large is not eligible (when primary alive). +// EC-6 fix: WAL lag gate only applies when primary is alive. func TestRegistry_PromoteBestReplica_WALLagIneligible(t *testing.T) { r := NewBlockVolumeRegistry() + r.MarkBlockCapable("primary") // primary must be alive for WAL lag gate + r.MarkBlockCapable("lagging") // replica must be alive for other gates to pass r.Register(&BlockVolumeEntry{ Name: "vol1", VolumeServer: "primary", @@ -968,8 +971,10 @@ func TestRegistry_PromoteBestReplica_EligibilityFiltersCorrectly(t *testing.T) { } // Configurable tolerance: widen tolerance to allow lagging replicas. +// EC-6 fix: WAL lag gate only applies when primary is alive. func TestRegistry_PromoteBestReplica_ConfigurableTolerance(t *testing.T) { r := NewBlockVolumeRegistry() + r.MarkBlockCapable("primary") // primary must be alive for WAL lag gate to apply r.MarkBlockCapable("lagging") r.Register(&BlockVolumeEntry{ Name: "vol1", diff --git a/weed/server/master_grpc_server_block.go b/weed/server/master_grpc_server_block.go index 75fb78948..a9c549fa8 100644 --- a/weed/server/master_grpc_server_block.go +++ b/weed/server/master_grpc_server_block.go @@ -167,16 +167,19 @@ func (ms *MasterServer) CreateBlockVolume(ctx context.Context, req *master_pb.Cr LeaseTtlMs: leaseTTLMs, } // CP8-2: populate ReplicaAddrs for multi-replica. + // V2 P10-1: include stable ServerID from registry. for _, ri := range entry.Replicas { primaryAssignment.ReplicaAddrs = append(primaryAssignment.ReplicaAddrs, blockvol.ReplicaAddr{ DataAddr: ri.DataAddr, CtrlAddr: ri.CtrlAddr, + ServerID: ri.Server, // V2: stable identity = registry VS address }) } // Backward compat: also set scalar fields if exactly 1 replica. if len(entry.Replicas) == 1 { primaryAssignment.ReplicaDataAddr = entry.Replicas[0].DataAddr primaryAssignment.ReplicaCtrlAddr = entry.Replicas[0].CtrlAddr + primaryAssignment.ReplicaServerID = entry.Replicas[0].Server // V2: scalar stable ID } ms.blockAssignmentQueue.Enqueue(server, primaryAssignment) @@ -368,6 +371,25 @@ func (ms *MasterServer) ListBlockSnapshots(ctx context.Context, req *master_pb.L return resp, nil } +// RestoreBlockSnapshot restores a block volume to the specified snapshot. +// This is a destructive operation: all writes after the snapshot are lost. +func (ms *MasterServer) RestoreBlockSnapshot(ctx context.Context, req *master_pb.RestoreBlockSnapshotRequest) (*master_pb.RestoreBlockSnapshotResponse, error) { + if req.VolumeName == "" { + return nil, fmt.Errorf("volume_name is required") + } + + entry, ok := ms.blockRegistry.Lookup(req.VolumeName) + if !ok { + return nil, fmt.Errorf("block volume %q not found", req.VolumeName) + } + + if err := ms.blockVSRestore(ctx, entry.VolumeServer, req.VolumeName, req.SnapshotId); err != nil { + return nil, fmt.Errorf("restore snapshot on %s: %w", entry.VolumeServer, err) + } + + return &master_pb.RestoreBlockSnapshotResponse{}, nil +} + // ExpandBlockVolume expands a block volume. For standalone volumes (no replicas), // uses direct expand. For replicated volumes, uses coordinated prepare/commit/cancel. func (ms *MasterServer) ExpandBlockVolume(ctx context.Context, req *master_pb.ExpandBlockVolumeRequest) (*master_pb.ExpandBlockVolumeResponse, error) { diff --git a/weed/server/master_server.go b/weed/server/master_server.go index 69bb7f01f..5cd9f7b5d 100644 --- a/weed/server/master_server.go +++ b/weed/server/master_server.go @@ -104,6 +104,7 @@ type MasterServer struct { blockVSSnapshot func(ctx context.Context, server string, name string, snapID uint32) (int64, uint64, error) blockVSDeleteSnap func(ctx context.Context, server string, name string, snapID uint32) error blockVSListSnaps func(ctx context.Context, server string, name string) ([]*volume_server_pb.BlockSnapshotInfo, error) + blockVSRestore func(ctx context.Context, server string, name string, snapID uint32) error blockVSExpand func(ctx context.Context, server string, name string, newSize uint64) (uint64, error) blockVSPrepareExpand func(ctx context.Context, server string, name string, newSize, expandEpoch uint64) error blockVSCommitExpand func(ctx context.Context, server string, name string, expandEpoch uint64) (uint64, error) @@ -172,6 +173,7 @@ func NewMasterServer(r *mux.Router, option *MasterOption, peers map[string]pb.Se ms.blockVSSnapshot = ms.defaultBlockVSSnapshot ms.blockVSDeleteSnap = ms.defaultBlockVSDeleteSnap ms.blockVSListSnaps = ms.defaultBlockVSListSnaps + ms.blockVSRestore = ms.defaultBlockVSRestore ms.blockVSExpand = ms.defaultBlockVSExpand ms.blockVSPrepareExpand = ms.defaultBlockVSPrepareExpand ms.blockVSCommitExpand = ms.defaultBlockVSCommitExpand @@ -650,6 +652,16 @@ func (ms *MasterServer) defaultBlockVSListSnaps(ctx context.Context, server stri return infos, err } +func (ms *MasterServer) defaultBlockVSRestore(ctx context.Context, server string, name string, snapID uint32) error { + return operation.WithVolumeServerClient(false, pb.ServerAddress(server), ms.grpcDialOption, func(client volume_server_pb.VolumeServerClient) error { + _, err := client.RestoreBlockSnapshot(ctx, &volume_server_pb.RestoreBlockSnapshotRequest{ + Name: name, + SnapshotId: snapID, + }) + return err + }) +} + func (ms *MasterServer) defaultBlockVSExpand(ctx context.Context, server string, name string, newSize uint64) (uint64, error) { var capacity uint64 err := operation.WithVolumeServerClient(false, pb.ServerAddress(server), ms.grpcDialOption, func(client volume_server_pb.VolumeServerClient) error { diff --git a/weed/server/qa_block_control_loop_adversarial_test.go b/weed/server/qa_block_control_loop_adversarial_test.go new file mode 100644 index 000000000..43c4e8735 --- /dev/null +++ b/weed/server/qa_block_control_loop_adversarial_test.go @@ -0,0 +1,126 @@ +package weed_server + +import ( + "context" + "path/filepath" + "strings" + "testing" + "time" + + engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication" + "github.com/seaweedfs/seaweedfs/weed/pb/master_pb" + "github.com/seaweedfs/seaweedfs/weed/storage" + "github.com/seaweedfs/seaweedfs/weed/storage/blockvol" + "github.com/seaweedfs/seaweedfs/weed/storage/blockvol/v2bridge" +) + +// ============================================================ +// Phase 10 P4: Adversarial — stale epoch on PROMOTED volume +// +// Corrected: targets the promoted BS + promoted volume path, +// matching the real failover test shape. +// ============================================================ + +func TestAdversarial_P10P4_StaleEpochOnPromotedVolume(t *testing.T) { + s := newP4Setup(t) + + // Step 1: create volume at epoch 1 (primary=vs1, replica=vs2). + ctx := context.Background() + createResp, err := s.ms.CreateBlockVolume(ctx, &master_pb.CreateBlockVolumeRequest{ + Name: "pvc-stale-2", + SizeBytes: 1 << 20, + ReplicaFactor: 2, + DurabilityMode: "sync_all", + }) + if err != nil { + t.Fatal(err) + } + primaryVS := createResp.VolumeServer + t.Logf("created: primary=%s", primaryVS) + + // Deliver epoch 1 to primary BS. + s.deliverAssignments(primaryVS) + time.Sleep(200 * time.Millisecond) + + // Step 2: failover → epoch 2, new primary is the old replica. + s.ms.blockRegistry.UpdateEntry("pvc-stale-2", func(e *BlockVolumeEntry) { + e.LastLeaseGrant = time.Now().Add(-1 * time.Minute) + }) + s.ms.failoverBlockVolumes(primaryVS) + time.Sleep(100 * time.Millisecond) + + entryAfter, ok := s.ms.blockRegistry.Lookup("pvc-stale-2") + if !ok { + t.Fatal("volume not found after failover") + } + if entryAfter.Epoch != 2 { + t.Fatalf("registry epoch: %d, want 2", entryAfter.Epoch) + } + newPrimary := entryAfter.VolumeServer + t.Logf("after failover: new primary=%s epoch=2", newPrimary) + + // Step 3: create a SEPARATE promoted BlockService (same as P4 failover test). + // This is the promoted VS — it has its own store with the promoted volume. + sanitized := strings.ReplaceAll(newPrimary, ":", "_") + promotedDir := filepath.Join(s.dir, sanitized+"_promoted") + promotedStore := storage.NewBlockVolumeStore() + volPath := filepath.Join(s.dir, sanitized, "pvc-stale-2.blk") + if _, err := promotedStore.AddBlockVolume(volPath, ""); err != nil { + t.Fatalf("add promoted vol: %v", err) + } + promotedBS := &BlockService{ + blockStore: promotedStore, + blockDir: promotedDir, + listenAddr: "127.0.0.1:3260", + localServerID: newPrimary, + v2Bridge: v2bridge.NewControlBridge(), + v2Orchestrator: engine.NewRecoveryOrchestrator(), + replStates: make(map[string]*volReplState), + } + promotedBS.v2Recovery = NewRecoveryManager(promotedBS) + t.Cleanup(func() { + promotedBS.v2Recovery.Shutdown() + promotedStore.Close() + }) + + // Deliver epoch 2 assignment to promoted BS (not s.bs). + pending := s.ms.blockAssignmentQueue.Peek(newPrimary) + if len(pending) > 0 { + protoAssignments := blockvol.AssignmentsToProto(pending) + goAssignments := blockvol.AssignmentsFromProto(protoAssignments) + promotedBS.ProcessAssignments(goAssignments) + } + time.Sleep(200 * time.Millisecond) + + // Verify: promoted vol has epoch 2. + var promotedEpoch uint64 + promotedStore.WithVolume(volPath, func(vol *blockvol.BlockVol) error { + promotedEpoch = vol.Epoch() + return nil + }) + if promotedEpoch != 2 { + t.Fatalf("promoted vol epoch: %d, want 2", promotedEpoch) + } + + // Step 4: deliver STALE epoch 1 to the PROMOTED BS. + staleAssignment := blockvol.BlockVolumeAssignment{ + Path: volPath, + Epoch: 1, // STALE — lower than current epoch 2 + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + } + promotedBS.ProcessAssignments([]blockvol.BlockVolumeAssignment{staleAssignment}) + time.Sleep(100 * time.Millisecond) + + // Step 5: verify NOT reverted on the PROMOTED volume. + var afterStale uint64 + promotedStore.WithVolume(volPath, func(vol *blockvol.BlockVol) error { + afterStale = vol.Epoch() + return nil + }) + if afterStale < 2 { + t.Fatalf("BUG: promoted vol epoch reverted from 2 to %d after stale delivery", afterStale) + } + + t.Logf("adversarial: stale epoch 1 on promoted vol → epoch stays at %d (HandleAssignment rejects regression)", afterStale) +} diff --git a/weed/server/qa_block_control_loop_test.go b/weed/server/qa_block_control_loop_test.go new file mode 100644 index 000000000..2bb472e79 --- /dev/null +++ b/weed/server/qa_block_control_loop_test.go @@ -0,0 +1,383 @@ +package weed_server + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/seaweedfs/seaweedfs/weed/pb/master_pb" + engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication" + "github.com/seaweedfs/seaweedfs/weed/storage" + "github.com/seaweedfs/seaweedfs/weed/storage/blockvol" + "github.com/seaweedfs/seaweedfs/weed/storage/blockvol/v2bridge" +) + +// ============================================================ +// Phase 10 P4: Master-driven control-loop closure +// +// These tests use REAL master assignment production (CreateBlockVolume, +// failoverBlockVolumes) with the allocator wired to produce paths that +// match REAL registered block volumes in the test BlockService. +// ============================================================ + +// p4Setup creates a master + BlockService where the master's allocator +// produces paths matching real volumes registered in the BlockService's store. +type p4Setup struct { + ms *MasterServer + bs *BlockService + store *storage.BlockVolumeStore + dir string + volSeq int +} + +func newP4Setup(t *testing.T) *p4Setup { + t.Helper() + dir := t.TempDir() + store := storage.NewBlockVolumeStore() + + bs := &BlockService{ + blockStore: store, + blockDir: dir, + listenAddr: "127.0.0.1:3260", + localServerID: "vs1:9333", + v2Bridge: v2bridge.NewControlBridge(), + v2Orchestrator: engine.NewRecoveryOrchestrator(), + replStates: make(map[string]*volReplState), + } + bs.v2Recovery = NewRecoveryManager(bs) + + ms := &MasterServer{ + blockRegistry: NewBlockVolumeRegistry(), + blockAssignmentQueue: NewBlockAssignmentQueue(), + blockFailover: newBlockFailoverState(), + } + ms.blockRegistry.MarkBlockCapable("vs1:9333") + ms.blockRegistry.MarkBlockCapable("vs2:9333") + + setup := &p4Setup{ms: ms, bs: bs, store: store, dir: dir} + + // Wire allocator to create REAL block volumes at per-server paths. + ms.blockVSAllocate = func(ctx context.Context, server string, name string, sizeBytes uint64, diskType string, durabilityMode string) (*blockAllocResult, error) { + // Per-server subdir (sanitize colons for Windows). + sanitized := strings.ReplaceAll(server, ":", "_") + serverDir := filepath.Join(dir, sanitized) + if err := os.MkdirAll(serverDir, 0755); err != nil { + return nil, err + } + volPath := filepath.Join(serverDir, fmt.Sprintf("%s.blk", name)) + vol, err := blockvol.CreateBlockVol(volPath, blockvol.CreateOptions{ + VolumeSize: 1 * 1024 * 1024, + BlockSize: 4096, + WALSize: 256 * 1024, + }) + if err != nil { + return nil, err + } + vol.Close() + // Register ALL volumes in the shared store (single-process test + // simulates both primary and promoted-replica VS). + if _, err := store.AddBlockVolume(volPath, ""); err != nil { + return nil, err + } + return &blockAllocResult{ + Path: volPath, + IQN: fmt.Sprintf("iqn.2024.test:%s", name), + ISCSIAddr: server + ":3260", + ReplicaDataAddr: server + ":14260", + ReplicaCtrlAddr: server + ":14261", + RebuildListenAddr: server + ":15000", + }, nil + } + ms.blockVSDelete = func(ctx context.Context, server string, name string) error { + return nil + } + + t.Cleanup(func() { + bs.v2Recovery.Shutdown() + store.Close() + }) + + return setup +} + +// deliverAssignments simulates heartbeat delivery: queue → proto → decode → ProcessAssignments. +func (s *p4Setup) deliverAssignments(server string) int { + pending := s.ms.blockAssignmentQueue.Peek(server) + if len(pending) == 0 { + return 0 + } + protoAssignments := blockvol.AssignmentsToProto(pending) + goAssignments := blockvol.AssignmentsFromProto(protoAssignments) + s.bs.ProcessAssignments(goAssignments) + return len(goAssignments) +} + +// --- 1. Real master create → full delivery with real volume --- + +func TestP10P4_MasterCreate_FullDelivery(t *testing.T) { + s := newP4Setup(t) + ctx := context.Background() + + createResp, err := s.ms.CreateBlockVolume(ctx, &master_pb.CreateBlockVolumeRequest{ + Name: "pvc-data-1", + SizeBytes: 1 << 30, + }) + if err != nil { + t.Fatalf("CreateBlockVolume: %v", err) + } + primaryVS := createResp.VolumeServer + t.Logf("master created: primary=%s replica=%s", primaryVS, createResp.ReplicaServer) + + // Deliver master-produced assignments. + n := s.deliverAssignments(primaryVS) + if n == 0 { + t.Fatal("no assignments delivered") + } + time.Sleep(200 * time.Millisecond) + + // Verify: engine has sender with stable ReplicaID. + entry, _ := s.ms.blockRegistry.Lookup("pvc-data-1") + expectedID := entry.Path + "/" + entry.Replicas[0].Server + sender := s.bs.v2Orchestrator.Registry.Sender(expectedID) + if sender == nil { + t.Fatalf("sender not found: %s", expectedID) + } + + // Verify: V1 HandleAssignment succeeded (vol exists in store). + var volEpoch uint64 + if err := s.store.WithVolume(entry.Path, func(vol *blockvol.BlockVol) error { + volEpoch = vol.Epoch() + return nil + }); err != nil { + t.Fatalf("volume not accessible in store: %v", err) + } + if volEpoch == 0 { + t.Fatal("vol epoch should be set after HandleAssignment") + } + + // Verify: heartbeat reports the assigned volume. + msgs := s.bs.CollectBlockVolumeHeartbeat() + found := false + for _, m := range msgs { + if m.Path == entry.Path { + found = true + if m.Epoch != entry.Epoch { + t.Fatalf("heartbeat epoch=%d, want %d", m.Epoch, entry.Epoch) + } + } + } + if !found { + t.Fatalf("assigned volume %s not in heartbeat", entry.Path) + } + + t.Logf("P10P4 create: master → queue → proto → VS → sender(%s) + vol(epoch=%d) + heartbeat", expectedID, volEpoch) +} + +// --- 2. Real master failover → delivery → convergence --- + +func TestP10P4_MasterFailover_Convergence(t *testing.T) { + s := newP4Setup(t) + ctx := context.Background() + + createResp, err := s.ms.CreateBlockVolume(ctx, &master_pb.CreateBlockVolumeRequest{ + Name: "pvc-data-2", + SizeBytes: 1 << 30, + }) + if err != nil { + t.Fatalf("CreateBlockVolume: %v", err) + } + primaryVS := createResp.VolumeServer + replicaVS := createResp.ReplicaServer + t.Logf("created: primary=%s replica=%s", primaryVS, replicaVS) + + // Deliver initial assignment to primary BS. + n1 := s.deliverAssignments(primaryVS) + if n1 == 0 { + t.Fatal("no initial assignments delivered") + } + time.Sleep(200 * time.Millisecond) + + entry, _ := s.ms.blockRegistry.Lookup("pvc-data-2") + + // Expire lease, then failover. + s.ms.blockRegistry.UpdateEntry("pvc-data-2", func(e *BlockVolumeEntry) { + e.LastLeaseGrant = time.Now().Add(-1 * time.Minute) + }) + s.ms.failoverBlockVolumes(primaryVS) + + entryAfter, _ := s.ms.blockRegistry.Lookup("pvc-data-2") + if entryAfter.Epoch != 2 { + t.Fatalf("epoch=%d, want 2", entryAfter.Epoch) + } + newPrimary := entryAfter.VolumeServer + t.Logf("after failover: new primary=%s epoch=%d", newPrimary, entryAfter.Epoch) + + // Build a SEPARATE BlockService for the promoted replica VS. + // This simulates the promoted VS having its own store and heartbeat. + replicaStore := storage.NewBlockVolumeStore() + // Find and register the replica's vol path. + sanitized := strings.ReplaceAll(replicaVS, ":", "_") + replicaVolPath := filepath.Join(s.dir, sanitized, "pvc-data-2.blk") + if _, err := replicaStore.AddBlockVolume(replicaVolPath, ""); err != nil { + t.Fatalf("register replica vol: %v", err) + } + promotedBS := &BlockService{ + blockStore: replicaStore, + blockDir: s.dir, + listenAddr: "127.0.0.1:3261", + localServerID: newPrimary, + v2Bridge: v2bridge.NewControlBridge(), + v2Orchestrator: engine.NewRecoveryOrchestrator(), + replStates: make(map[string]*volReplState), + } + promotedBS.v2Recovery = NewRecoveryManager(promotedBS) + t.Cleanup(func() { + promotedBS.v2Recovery.Shutdown() + replicaStore.Close() + }) + + // Deliver failover assignment to the promoted BS through proto path. + failoverPending := s.ms.blockAssignmentQueue.Peek(newPrimary) + if len(failoverPending) == 0 { + t.Fatal("no failover assignment in queue") + } + fa := failoverPending[0] + if fa.Epoch != 2 { + t.Fatalf("failover assignment epoch=%d, want 2", fa.Epoch) + } + + protoAssignments := blockvol.AssignmentsToProto(failoverPending) + goAssignments := blockvol.AssignmentsFromProto(protoAssignments) + promotedBS.ProcessAssignments(goAssignments) + time.Sleep(200 * time.Millisecond) + + // Verify: HandleAssignment succeeded on the promoted vol (epoch=2). + var promotedEpoch uint64 + if err := replicaStore.WithVolume(fa.Path, func(vol *blockvol.BlockVol) error { + promotedEpoch = vol.Epoch() + return nil + }); err != nil { + t.Fatalf("promoted vol not accessible: %v", err) + } + if promotedEpoch != 2 { + t.Fatalf("promoted vol epoch=%d, want 2", promotedEpoch) + } + + // Verify: promoted VS's OWN heartbeat reports the vol at epoch 2. + // This is the promoted VS's independent heartbeat, not the shared store. + promotedMsgs := promotedBS.CollectBlockVolumeHeartbeat() + foundPromoted := false + for _, m := range promotedMsgs { + if m.Path == fa.Path && m.Epoch == 2 { + foundPromoted = true + } + } + if !foundPromoted { + t.Fatal("promoted VS heartbeat should show vol at epoch 2") + } + + // Verify: promoted VS heartbeat does NOT contain the old primary's vol. + _ = entry // reference to initial entry + for _, m := range promotedMsgs { + if m.Path == entry.Path && m.Path != fa.Path { + t.Fatalf("promoted VS heartbeat should NOT contain old primary vol %s", entry.Path) + } + } + + t.Logf("P10P4 failover: create → failover(epoch=2) → separate promoted BS → vol(epoch=2) + own heartbeat") +} + +// --- 3. Identity preservation through master-produced path --- + +func TestP10P4_IdentityPreservation(t *testing.T) { + s := newP4Setup(t) + ctx := context.Background() + + _, err := s.ms.CreateBlockVolume(ctx, &master_pb.CreateBlockVolumeRequest{ + Name: "pvc-data-3", + SizeBytes: 1 << 30, + }) + if err != nil { + t.Fatalf("CreateBlockVolume: %v", err) + } + + entry, _ := s.ms.blockRegistry.Lookup("pvc-data-3") + + // Verify master-produced assignment carries stable ServerID. + pending := s.ms.blockAssignmentQueue.Peek(entry.VolumeServer) + hasStableID := false + for _, a := range pending { + if a.ReplicaServerID != "" { + hasStableID = true + } + for _, ra := range a.ReplicaAddrs { + if ra.ServerID != "" { + hasStableID = true + } + } + } + if !hasStableID { + t.Fatal("master assignment missing stable ServerID") + } + + // Deliver and verify engine uses stable ID, not address. + s.deliverAssignments(entry.VolumeServer) + time.Sleep(200 * time.Millisecond) + + for _, ri := range entry.Replicas { + stableID := entry.Path + "/" + ri.Server + addressID := entry.Path + "/" + ri.DataAddr + if s.bs.v2Orchestrator.Registry.Sender(stableID) == nil { + t.Fatalf("stable sender %s not found", stableID) + } + if s.bs.v2Orchestrator.Registry.Sender(addressID) != nil { + t.Fatalf("address-derived sender %s should NOT exist", addressID) + } + } + + t.Log("P10P4 identity: master-produced stable ID preserved through full delivery") +} + +// --- 4. Repeated delivery idempotence --- + +func TestP10P4_RepeatedDelivery_Idempotence(t *testing.T) { + s := newP4Setup(t) + ctx := context.Background() + + _, err := s.ms.CreateBlockVolume(ctx, &master_pb.CreateBlockVolumeRequest{ + Name: "pvc-data-4", + SizeBytes: 1 << 30, + }) + if err != nil { + t.Fatalf("CreateBlockVolume: %v", err) + } + + entry, _ := s.ms.blockRegistry.Lookup("pvc-data-4") + primaryVS := entry.VolumeServer + replicaID := entry.Path + "/" + entry.Replicas[0].Server + + // First delivery. + s.deliverAssignments(primaryVS) + time.Sleep(200 * time.Millisecond) + + eventsAfterFirst := len(s.bs.v2Orchestrator.Log.EventsFor(replicaID)) + if eventsAfterFirst == 0 { + t.Fatal("first delivery must create engine events (guard against vacuous pass)") + } + t.Logf("first delivery: %d events", eventsAfterFirst) + + // Second delivery (Peek returns same assignments). + s.deliverAssignments(primaryVS) + time.Sleep(100 * time.Millisecond) + + eventsAfterSecond := len(s.bs.v2Orchestrator.Log.EventsFor(replicaID)) + if eventsAfterSecond != eventsAfterFirst { + t.Fatalf("idempotence broken: events %d → %d", eventsAfterFirst, eventsAfterSecond) + } + + t.Logf("P10P4 idempotent: repeated delivery → events stable at %d (guard: >0)", eventsAfterFirst) +} diff --git a/weed/server/qa_block_convergence_adversarial_test.go b/weed/server/qa_block_convergence_adversarial_test.go new file mode 100644 index 000000000..a297f5934 --- /dev/null +++ b/weed/server/qa_block_convergence_adversarial_test.go @@ -0,0 +1,158 @@ +package weed_server + +import ( + "testing" + "time" + + "github.com/seaweedfs/seaweedfs/weed/storage/blockvol" +) + +// ============================================================ +// Phase 10 P2: Adversarial convergence tests +// ============================================================ + +// --- Adversarial 1: Rapid triple reassignment (vs2 → vs3 → vs4) --- + +func TestAdversarial_P10P2_RapidTripleReassignment(t *testing.T) { + bs, volPath := createP2BlockService(t) + + makeAssignment := func(epoch uint64, serverID, dataAddr string) []blockvol.BlockVolumeAssignment { + return []blockvol.BlockVolumeAssignment{ + { + Path: volPath, + Epoch: epoch, + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + ReplicaServerID: serverID, + ReplicaDataAddr: dataAddr, + ReplicaCtrlAddr: dataAddr, + }, + } + } + + // Three rapid reassignments. + bs.ProcessAssignments(makeAssignment(1, "vs2-node:18080", "10.0.0.2:14260")) + time.Sleep(50 * time.Millisecond) + + bs.ProcessAssignments(makeAssignment(2, "vs3-node:18080", "10.0.0.3:14260")) + bs.ProcessAssignments(makeAssignment(3, "vs4-node:18080", "10.0.0.4:14260")) + + time.Sleep(300 * time.Millisecond) + + // Only vs4 should exist in engine. + vs2ID := volPath + "/vs2-node:18080" + vs3ID := volPath + "/vs3-node:18080" + vs4ID := volPath + "/vs4-node:18080" + + if bs.v2Orchestrator.Registry.Sender(vs2ID) != nil { + t.Fatal("vs2 sender should be removed") + } + if bs.v2Orchestrator.Registry.Sender(vs3ID) != nil { + t.Fatal("vs3 sender should be removed") + } + if bs.v2Orchestrator.Registry.Sender(vs4ID) == nil { + t.Fatal("vs4 sender should exist") + } + + // Runtime: no stale tasks for vs2 or vs3. + bs.v2Recovery.mu.Lock() + _, hasVs2 := bs.v2Recovery.tasks[vs2ID] + _, hasVs3 := bs.v2Recovery.tasks[vs3ID] + bs.v2Recovery.mu.Unlock() + + if hasVs2 || hasVs3 { + t.Fatalf("stale runtime tasks: vs2=%v vs3=%v", hasVs2, hasVs3) + } + + // Heartbeat: must report vs4 address. + msgs := bs.CollectBlockVolumeHeartbeat() + hb := findHeartbeatMsg(msgs, volPath) + if hb == nil { + t.Fatal("volume not in heartbeat") + } + if hb.ReplicaDataAddr != "10.0.0.4:14260" { + t.Fatalf("heartbeat addr=%s, want 10.0.0.4:14260", hb.ReplicaDataAddr) + } + + t.Log("adversarial 1: rapid triple reassignment → only vs4 in all truth surfaces") +} + +// --- Adversarial 2: Same replica, address change (epoch bump) --- + +func TestAdversarial_P10P2_SameReplica_AddressChange(t *testing.T) { + bs, volPath := createP2BlockService(t) + + replicaID := volPath + "/vs2-node:18080" + + // Epoch 1: vs2 at address 10.0.0.2. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + { + Path: volPath, + Epoch: 1, + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + ReplicaServerID: "vs2-node:18080", + ReplicaDataAddr: "10.0.0.2:14260", + ReplicaCtrlAddr: "10.0.0.2:14261", + }, + }) + + time.Sleep(100 * time.Millisecond) + + senderBefore := bs.v2Orchestrator.Registry.Sender(replicaID) + if senderBefore == nil { + t.Fatal("sender should exist at epoch 1") + } + + // Epoch 2: SAME ServerID, DIFFERENT address (replica restarted on new IP). + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + { + Path: volPath, + Epoch: 2, + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + ReplicaServerID: "vs2-node:18080", + ReplicaDataAddr: "10.0.0.99:14260", // different IP + ReplicaCtrlAddr: "10.0.0.99:14261", + }, + }) + + time.Sleep(200 * time.Millisecond) + + // Same sender identity (pointer). + senderAfter := bs.v2Orchestrator.Registry.Sender(replicaID) + if senderAfter == nil { + t.Fatal("sender should still exist") + } + if senderAfter != senderBefore { + t.Fatal("sender identity must be preserved (same pointer) across address change") + } + + // Endpoint updated. + if senderAfter.Endpoint().DataAddr != "10.0.0.99:14260" { + t.Fatalf("endpoint not updated: %s", senderAfter.Endpoint().DataAddr) + } + + // Heartbeat: new address. + msgs := bs.CollectBlockVolumeHeartbeat() + hb := findHeartbeatMsg(msgs, volPath) + if hb == nil { + t.Fatal("volume not in heartbeat") + } + if hb.ReplicaDataAddr != "10.0.0.99:14260" { + t.Fatalf("heartbeat addr=%s, want 10.0.0.99:14260", hb.ReplicaDataAddr) + } + + // No duplicate sender. + count := 0 + for _, s := range bs.v2Orchestrator.Registry.All() { + if s.ReplicaID() == replicaID { + count++ + } + } + if count != 1 { + t.Fatalf("duplicate senders: %d for %s", count, replicaID) + } + + t.Log("adversarial 2: same replica, address change → identity preserved, endpoint updated, no duplicate") +} diff --git a/weed/server/qa_block_convergence_test.go b/weed/server/qa_block_convergence_test.go new file mode 100644 index 000000000..272fef20d --- /dev/null +++ b/weed/server/qa_block_convergence_test.go @@ -0,0 +1,358 @@ +package weed_server + +import ( + "path/filepath" + "testing" + "time" + + engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication" + "github.com/seaweedfs/seaweedfs/weed/storage" + "github.com/seaweedfs/seaweedfs/weed/storage/blockvol" + "github.com/seaweedfs/seaweedfs/weed/storage/blockvol/v2bridge" +) + +// ============================================================ +// Phase 10 P2: Reassignment / result convergence +// +// Proofs: +// 1. Reassignment convergence: epoch bump updates control truth +// 2. Stale owner removal: old sender/session gone after reassignment +// 3. Reported truth: heartbeat reflects new replica assignment +// 4. No split truth: ingress + runtime + reported all agree +// ============================================================ + +func createP2BlockService(t *testing.T) (*BlockService, string) { + t.Helper() + dir := t.TempDir() + + volPath := filepath.Join(dir, "vol1.blk") + vol, err := blockvol.CreateBlockVol(volPath, blockvol.CreateOptions{ + VolumeSize: 1 * 1024 * 1024, + BlockSize: 4096, + WALSize: 256 * 1024, + }) + if err != nil { + t.Fatalf("CreateBlockVol: %v", err) + } + vol.Close() + + store := storage.NewBlockVolumeStore() + if _, err := store.AddBlockVolume(volPath, ""); err != nil { + t.Fatalf("AddBlockVolume: %v", err) + } + + bs := &BlockService{ + blockStore: store, + blockDir: dir, + listenAddr: "127.0.0.1:3260", + localServerID: "vs1-node:18080", + v2Bridge: v2bridge.NewControlBridge(), + v2Orchestrator: engine.NewRecoveryOrchestrator(), + replStates: make(map[string]*volReplState), + } + bs.v2Recovery = NewRecoveryManager(bs) + + t.Cleanup(func() { + bs.v2Recovery.Shutdown() + store.Close() + }) + + return bs, volPath +} + +// --- 1. Reassignment convergence: epoch bump updates control truth --- + +func TestP10P2_ReassignmentConvergence_EpochBump(t *testing.T) { + bs, volPath := createP2BlockService(t) + + // Epoch 1: primary with replica vs2. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + { + Path: volPath, + Epoch: 1, + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + ReplicaServerID: "vs2-node:18080", + ReplicaDataAddr: "10.0.0.2:14260", + ReplicaCtrlAddr: "10.0.0.2:14261", + }, + }) + + time.Sleep(200 * time.Millisecond) + + replicaID1 := volPath + "/vs2-node:18080" + s1 := bs.v2Orchestrator.Registry.Sender(replicaID1) + if s1 == nil { + t.Fatal("epoch 1: sender for vs2 not created") + } + epoch1Session := s1.SessionID() + t.Logf("epoch 1: sender=%s sessionID=%d", replicaID1, epoch1Session) + + // Epoch 2: primary with DIFFERENT replica vs3. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + { + Path: volPath, + Epoch: 2, + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + ReplicaServerID: "vs3-node:18080", + ReplicaDataAddr: "10.0.0.3:14260", + ReplicaCtrlAddr: "10.0.0.3:14261", + }, + }) + + time.Sleep(200 * time.Millisecond) + + // New replica sender should exist. + replicaID2 := volPath + "/vs3-node:18080" + s2 := bs.v2Orchestrator.Registry.Sender(replicaID2) + if s2 == nil { + t.Fatal("epoch 2: sender for vs3 not created") + } + t.Logf("epoch 2: sender=%s sessionID=%d", replicaID2, s2.SessionID()) + + // Old replica sender should be gone (removed by Reconcile). + s1After := bs.v2Orchestrator.Registry.Sender(replicaID1) + if s1After != nil { + t.Fatalf("epoch 2: old sender %s should be removed", replicaID1) + } + + t.Log("P10P2 convergence: epoch bump → old replica removed, new replica active") +} + +// --- 2. Stale owner removal: live old recovery cancelled during reassignment --- + +func TestP10P2_StaleOwnerRemoval(t *testing.T) { + bs, volPath := createP2BlockService(t) + rm := bs.v2Recovery + + // Write data so recovery has real work (not zero-gap auto-complete). + if err := bs.blockStore.WithVolume(volPath, func(vol *blockvol.BlockVol) error { + for i := 0; i < 5; i++ { + vol.WriteLBA(uint64(i), make([]byte, 4096)) + } + return nil + }); err != nil { + t.Fatalf("write: %v", err) + } + + // Hook: block the first recovery goroutine so it's alive during reassignment. + holdFirst := make(chan struct{}) + firstReached := make(chan struct{}, 1) + callCount := 0 + rm.OnBeforeExecute = func(replicaID string) { + callCount++ + if callCount == 1 { + firstReached <- struct{}{} + <-holdFirst // block until released + } + } + + // Epoch 1: start recovery for vs2 (will block at hook). + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + { + Path: volPath, + Epoch: 1, + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + ReplicaServerID: "vs2-node:18080", + ReplicaDataAddr: "10.0.0.2:14260", + ReplicaCtrlAddr: "10.0.0.2:14261", + }, + }) + + // Wait for old goroutine to be alive. + select { + case <-firstReached: + t.Log("old recovery goroutine for vs2 is alive and blocked") + case <-time.After(5 * time.Second): + t.Fatal("old goroutine did not reach hook") + } + + // Capture old task's done channel. + oldReplicaID := volPath + "/vs2-node:18080" + rm.mu.Lock() + oldTask := rm.tasks[oldReplicaID] + rm.mu.Unlock() + if oldTask == nil { + t.Fatal("old task must exist while goroutine is blocked") + } + oldDone := oldTask.done + + // Release old goroutine from another goroutine. + go func() { + time.Sleep(50 * time.Millisecond) + close(holdFirst) + }() + + // Epoch 2: reassignment to vs3. cancelAndDrain waits for old to exit. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + { + Path: volPath, + Epoch: 2, + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + ReplicaServerID: "vs3-node:18080", + ReplicaDataAddr: "10.0.0.3:14260", + ReplicaCtrlAddr: "10.0.0.3:14261", + }, + }) + + // Old goroutine must be drained. + select { + case <-oldDone: + t.Log("confirmed: old recovery goroutine drained during reassignment") + default: + t.Fatal("old done channel still open after reassignment") + } + + // Old task must be gone. + rm.mu.Lock() + _, hasOld := rm.tasks[oldReplicaID] + rm.mu.Unlock() + if hasOld { + t.Fatal("stale recovery task should be gone") + } + + t.Log("P10P2 stale removal: live old owner cancelled + drained during reassignment") +} + +// --- 3. Reported truth: actual heartbeat output reflects new replica --- + +func TestP10P2_ReportedTruth_HeartbeatConverges(t *testing.T) { + bs, volPath := createP2BlockService(t) + + // Epoch 1: primary with vs2. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + { + Path: volPath, + Epoch: 1, + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + ReplicaServerID: "vs2-node:18080", + ReplicaDataAddr: "10.0.0.2:14260", + ReplicaCtrlAddr: "10.0.0.2:14261", + }, + }) + + time.Sleep(100 * time.Millisecond) + + // Verify actual heartbeat output reports vs2. + msgs1 := bs.CollectBlockVolumeHeartbeat() + found1 := findHeartbeatMsg(msgs1, volPath) + if found1 == nil { + t.Fatal("epoch 1: volume not in heartbeat output") + } + if found1.ReplicaDataAddr != "10.0.0.2:14260" { + t.Fatalf("epoch 1 heartbeat: ReplicaDataAddr=%q, want 10.0.0.2:14260", found1.ReplicaDataAddr) + } + t.Logf("epoch 1 heartbeat: ReplicaDataAddr=%s ✓", found1.ReplicaDataAddr) + + // Epoch 2: primary with vs3. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + { + Path: volPath, + Epoch: 2, + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + ReplicaServerID: "vs3-node:18080", + ReplicaDataAddr: "10.0.0.3:14260", + ReplicaCtrlAddr: "10.0.0.3:14261", + }, + }) + + time.Sleep(100 * time.Millisecond) + + // Actual heartbeat output should report vs3, NOT stale vs2. + msgs2 := bs.CollectBlockVolumeHeartbeat() + found2 := findHeartbeatMsg(msgs2, volPath) + if found2 == nil { + t.Fatal("epoch 2: volume not in heartbeat output") + } + if found2.ReplicaDataAddr != "10.0.0.3:14260" { + t.Fatalf("epoch 2 heartbeat: ReplicaDataAddr=%q, want 10.0.0.3:14260 (stale vs2)", found2.ReplicaDataAddr) + } + if found2.ReplicaCtrlAddr != "10.0.0.3:14261" { + t.Fatalf("epoch 2 heartbeat: ReplicaCtrlAddr=%q, want 10.0.0.3:14261", found2.ReplicaCtrlAddr) + } + + t.Logf("P10P2 reported truth: CollectBlockVolumeHeartbeat converged from vs2 → vs3") +} + +func findHeartbeatMsg(msgs []blockvol.BlockVolumeInfoMessage, path string) *blockvol.BlockVolumeInfoMessage { + for i := range msgs { + if msgs[i].Path == path { + return &msgs[i] + } + } + return nil +} + +// --- 4. No split truth: ingress + runtime + reported all agree --- + +func TestP10P2_NoSplitTruth(t *testing.T) { + bs, volPath := createP2BlockService(t) + + // Epoch 1: primary with vs2. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + { + Path: volPath, + Epoch: 1, + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + ReplicaServerID: "vs2-node:18080", + ReplicaDataAddr: "10.0.0.2:14260", + ReplicaCtrlAddr: "10.0.0.2:14261", + }, + }) + + time.Sleep(200 * time.Millisecond) + + // Epoch 2: primary with vs3. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + { + Path: volPath, + Epoch: 2, + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + ReplicaServerID: "vs3-node:18080", + ReplicaDataAddr: "10.0.0.3:14260", + ReplicaCtrlAddr: "10.0.0.3:14261", + }, + }) + + time.Sleep(200 * time.Millisecond) + + // Check all three truth surfaces agree on vs3: + + // 1. Engine (assignment ingress): sender for vs3 exists, vs2 gone. + vs3ID := volPath + "/vs3-node:18080" + vs2ID := volPath + "/vs2-node:18080" + + if bs.v2Orchestrator.Registry.Sender(vs3ID) == nil { + t.Fatal("engine: vs3 sender missing") + } + if bs.v2Orchestrator.Registry.Sender(vs2ID) != nil { + t.Fatal("engine: vs2 sender should be removed") + } + + // 2. Runtime (recovery manager): no task for vs2. + bs.v2Recovery.mu.Lock() + _, hasVs2Task := bs.v2Recovery.tasks[vs2ID] + bs.v2Recovery.mu.Unlock() + if hasVs2Task { + t.Fatal("runtime: stale vs2 recovery task exists") + } + + // 3. Reported (heartbeat output): addresses match vs3. + msgs := bs.CollectBlockVolumeHeartbeat() + hb := findHeartbeatMsg(msgs, volPath) + if hb == nil { + t.Fatal("reported: volume not in heartbeat") + } + if hb.ReplicaDataAddr != "10.0.0.3:14260" { + t.Fatalf("reported: ReplicaDataAddr=%q, want 10.0.0.3:14260", hb.ReplicaDataAddr) + } + + t.Log("P10P2 no-split-truth: engine(vs3) + runtime(no vs2 task) + heartbeat(vs3 addr) — all converged") +} diff --git a/weed/server/qa_block_idempotence_test.go b/weed/server/qa_block_idempotence_test.go new file mode 100644 index 000000000..6e48d9174 --- /dev/null +++ b/weed/server/qa_block_idempotence_test.go @@ -0,0 +1,214 @@ +package weed_server + +import ( + "path/filepath" + "testing" + "time" + + engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication" + "github.com/seaweedfs/seaweedfs/weed/storage" + "github.com/seaweedfs/seaweedfs/weed/storage/blockvol" + "github.com/seaweedfs/seaweedfs/weed/storage/blockvol/v2bridge" +) + +// ============================================================ +// Phase 10 P3: Bounded repeated-assignment idempotence +// +// Proofs: +// 1. Unchanged repeated assignment is idempotent (no rebuild-server relisten) +// 2. Changed assignment still triggers replacement (P2 guard) +// 3. No duplicate runtime side effects after repeated assignment +// ============================================================ + +func createP3BlockService(t *testing.T) (*BlockService, string) { + t.Helper() + dir := t.TempDir() + + volPath := filepath.Join(dir, "vol1.blk") + vol, err := blockvol.CreateBlockVol(volPath, blockvol.CreateOptions{ + VolumeSize: 1 * 1024 * 1024, + BlockSize: 4096, + WALSize: 256 * 1024, + }) + if err != nil { + t.Fatalf("CreateBlockVol: %v", err) + } + vol.Close() + + store := storage.NewBlockVolumeStore() + if _, err := store.AddBlockVolume(volPath, ""); err != nil { + t.Fatalf("AddBlockVolume: %v", err) + } + + bs := &BlockService{ + blockStore: store, + blockDir: dir, + listenAddr: "127.0.0.1:3260", + localServerID: "vs1-node:18080", + v2Bridge: v2bridge.NewControlBridge(), + v2Orchestrator: engine.NewRecoveryOrchestrator(), + replStates: make(map[string]*volReplState), + } + bs.v2Recovery = NewRecoveryManager(bs) + + t.Cleanup(func() { + bs.v2Recovery.Shutdown() + store.Close() + }) + + return bs, volPath +} + +// --- 1. Unchanged repeated assignment is idempotent --- + +func TestP10P3_RepeatedAssignment_Idempotent(t *testing.T) { + bs, volPath := createP3BlockService(t) + + assignment := blockvol.BlockVolumeAssignment{ + Path: volPath, + Epoch: 1, + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + ReplicaServerID: "vs2-node:18080", + ReplicaDataAddr: "10.0.0.2:14260", + ReplicaCtrlAddr: "10.0.0.2:14261", + } + + // First assignment: sets up replication + engine session + recovery. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{assignment}) + + time.Sleep(200 * time.Millisecond) + + // Capture V2 engine event count after first assignment. + replicaID := volPath + "/vs2-node:18080" + eventsAfterFirst := len(bs.v2Orchestrator.Log.EventsFor(replicaID)) + t.Logf("first assignment: %d engine events", eventsAfterFirst) + + // Second assignment: identical. Must be fully idempotent — no V2 engine + // processing, no new sessions, no recovery starts. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{assignment}) + + time.Sleep(100 * time.Millisecond) + + eventsAfterSecond := len(bs.v2Orchestrator.Log.EventsFor(replicaID)) + if eventsAfterSecond != eventsAfterFirst { + t.Fatalf("V2 engine events grew: %d → %d (repeated assignment not idempotent)", + eventsAfterFirst, eventsAfterSecond) + } + + // Third assignment: same again. Still no new events. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{assignment}) + + time.Sleep(100 * time.Millisecond) + + eventsAfterThird := len(bs.v2Orchestrator.Log.EventsFor(replicaID)) + if eventsAfterThird != eventsAfterFirst { + t.Fatalf("V2 engine events grew on third: %d → %d", eventsAfterFirst, eventsAfterThird) + } + + // V1 state also unchanged. + dataAddr, _ := bs.GetReplState(volPath) + if dataAddr != "10.0.0.2:14260" { + t.Fatalf("replState: %q", dataAddr) + } + + t.Logf("P10P3 idempotent: 3x same → V2 events stable at %d, V1 state unchanged", eventsAfterFirst) +} + +// --- 2. Changed assignment triggers replacement (P2 guard) --- + +func TestP10P3_ChangedAssignment_StillReplaces(t *testing.T) { + bs, volPath := createP3BlockService(t) + + // First assignment: vs2. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + { + Path: volPath, + Epoch: 1, + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + ReplicaServerID: "vs2-node:18080", + ReplicaDataAddr: "10.0.0.2:14260", + ReplicaCtrlAddr: "10.0.0.2:14261", + }, + }) + + time.Sleep(100 * time.Millisecond) + + dataAddr1, _ := bs.GetReplState(volPath) + if dataAddr1 != "10.0.0.2:14260" { + t.Fatalf("epoch 1: dataAddr=%q", dataAddr1) + } + + // Second assignment: CHANGED replica (vs3). Must NOT be idempotent. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + { + Path: volPath, + Epoch: 2, + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + ReplicaServerID: "vs3-node:18080", + ReplicaDataAddr: "10.0.0.3:14260", + ReplicaCtrlAddr: "10.0.0.3:14261", + }, + }) + + time.Sleep(100 * time.Millisecond) + + // Verify: state updated to vs3. + dataAddr2, _ := bs.GetReplState(volPath) + if dataAddr2 != "10.0.0.3:14260" { + t.Fatalf("epoch 2: dataAddr=%q, want 10.0.0.3:14260 (should have replaced)", dataAddr2) + } + + // Engine should have vs3, not vs2. + vs3ID := volPath + "/vs3-node:18080" + vs2ID := volPath + "/vs2-node:18080" + if bs.v2Orchestrator.Registry.Sender(vs3ID) == nil { + t.Fatal("vs3 sender should exist") + } + if bs.v2Orchestrator.Registry.Sender(vs2ID) != nil { + t.Fatal("vs2 sender should be removed") + } + + t.Log("P10P3 guard: changed assignment still triggers full replacement") +} + +// --- 3. Heartbeat coherent after repeated assignment --- + +func TestP10P3_HeartbeatCoherent_AfterRepeated(t *testing.T) { + bs, volPath := createP3BlockService(t) + + assignment := blockvol.BlockVolumeAssignment{ + Path: volPath, + Epoch: 1, + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + ReplicaServerID: "vs2-node:18080", + ReplicaDataAddr: "10.0.0.2:14260", + ReplicaCtrlAddr: "10.0.0.2:14261", + } + + // Apply 3 times. + for i := 0; i < 3; i++ { + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{assignment}) + time.Sleep(50 * time.Millisecond) + } + + // Heartbeat should report vs2 exactly once (no duplicates in output). + msgs := bs.CollectBlockVolumeHeartbeat() + count := 0 + for _, m := range msgs { + if m.Path == volPath { + count++ + if m.ReplicaDataAddr != "10.0.0.2:14260" { + t.Fatalf("heartbeat: ReplicaDataAddr=%q", m.ReplicaDataAddr) + } + } + } + if count != 1 { + t.Fatalf("heartbeat: volume appeared %d times, want 1", count) + } + + t.Log("P10P3 heartbeat: coherent after 3x repeated assignment") +} diff --git a/weed/server/qa_block_identity_test.go b/weed/server/qa_block_identity_test.go new file mode 100644 index 000000000..3753e5531 --- /dev/null +++ b/weed/server/qa_block_identity_test.go @@ -0,0 +1,173 @@ +package weed_server + +import ( + "testing" + + "github.com/seaweedfs/seaweedfs/weed/pb/master_pb" + "github.com/seaweedfs/seaweedfs/weed/storage/blockvol" + "github.com/seaweedfs/seaweedfs/weed/storage/blockvol/v2bridge" + + engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication" +) + +// ============================================================ +// Phase 10 P1: Stable identity on the real control wire +// +// Proofs: +// 1. Real ingress: proto wire → decode → ProcessAssignments → ControlBridge → ReplicaID +// 2. Canonical local identity: non-default serverID flows through block/control path +// 3. Fail-closed: missing ServerID on proto wire → replica skipped +// ============================================================ + +// --- Real ingress proof: proto wire → engine ReplicaID --- + +func TestP10P1_RealIngress_ProtoToReplicaID(t *testing.T) { + // Simulate the real control ingress chain: + // 1. Master builds proto assignment with stable ServerID + // 2. Proto is encoded (as it would be over gRPC wire) + // 3. Volume server decodes proto → Go assignment + // 4. ProcessAssignments → ControlBridge → engine + // 5. Engine has sender with correct ReplicaID = / + + // Step 1: Master builds proto (simulates master_grpc_server_block.go). + protoAssignment := &master_pb.BlockVolumeAssignment{ + Path: "pvc-vol-1", + Epoch: 5, + Role: uint32(blockvol.RolePrimary), + LeaseTtlMs: 30000, + ReplicaServerId: "vs2-node.cluster:18080", // stable ID from registry + ReplicaDataAddr: "10.0.0.2:14260", + ReplicaCtrlAddr: "10.0.0.2:14261", + ReplicaAddrs: []*master_pb.ReplicaAddrMessage{ + { + DataAddr: "10.0.0.2:14260", + CtrlAddr: "10.0.0.2:14261", + ServerId: "vs2-node.cluster:18080", + }, + }, + } + + // Step 2: Proto round-trip (simulates gRPC wire encode/decode). + // In real gRPC, this is automatic. Here we verify the Go struct + // carries the fields after proto-to-Go conversion. + goAssignment := blockvol.AssignmentFromProto(protoAssignment) + + // Verify: Go wire type preserves the stable ID. + if goAssignment.ReplicaServerID != "vs2-node.cluster:18080" { + t.Fatalf("scalar ReplicaServerID=%q, want vs2-node.cluster:18080", goAssignment.ReplicaServerID) + } + if len(goAssignment.ReplicaAddrs) != 1 || goAssignment.ReplicaAddrs[0].ServerID != "vs2-node.cluster:18080" { + t.Fatalf("multi-replica ServerID not preserved") + } + + // Step 3-4: ProcessAssignments → ControlBridge → engine. + bs := &BlockService{ + v2Bridge: v2bridge.NewControlBridge(), + v2Orchestrator: engine.NewRecoveryOrchestrator(), + localServerID: "vs1-node.cluster:18080", + } + + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{goAssignment}) + + // Step 5: Verify engine sender has the correct stable ReplicaID. + expectedReplicaID := "pvc-vol-1/vs2-node.cluster:18080" + sender := bs.v2Orchestrator.Registry.Sender(expectedReplicaID) + if sender == nil { + t.Fatalf("engine sender not found for %s", expectedReplicaID) + } + if !sender.HasActiveSession() { + t.Fatal("sender should have active session") + } + + t.Logf("P10P1 real ingress: proto(ServerId=%s) → decode → ProcessAssignments → sender(%s) with session", + "vs2-node.cluster:18080", expectedReplicaID) +} + +// --- Canonical local identity: non-default serverID --- + +func TestP10P1_CanonicalLocalIdentity(t *testing.T) { + // Prove that when the volume server has a canonical ID different from + // plain ip:port, the block/control path uses that canonical ID. + + canonicalID := "my-custom-volume-server-id" + + bs := &BlockService{ + v2Bridge: v2bridge.NewControlBridge(), + v2Orchestrator: engine.NewRecoveryOrchestrator(), + localServerID: canonicalID, // explicitly NOT ip:port shaped + } + + // Process a replica assignment that targets THIS volume server. + // The ControlBridge uses localServerID for replica/rebuild assignments. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + { + Path: "pvc-vol-2", + Epoch: 3, + Role: uint32(blockvol.RolePrimary), + ReplicaServerID: "other-server-id", + ReplicaDataAddr: "10.0.0.2:9333", + ReplicaCtrlAddr: "10.0.0.2:9334", + }, + }) + + // The sender should use the ServerID from the assignment, not localServerID. + sender := bs.v2Orchestrator.Registry.Sender("pvc-vol-2/other-server-id") + if sender == nil { + t.Fatal("sender should exist with ServerID from assignment, not from localServerID") + } + + // Now process a replica assignment where THIS server is the replica. + // The ControlBridge uses localServerID for the local ReplicaID. + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + { + Path: "pvc-vol-3", + Epoch: 1, + Role: uint32(blockvol.RoleReplica), + ReplicaDataAddr: "10.0.0.1:14260", + ReplicaCtrlAddr: "10.0.0.1:14261", + }, + }) + + // For a replica assignment, ControlBridge builds ReplicaID as /. + expectedLocalReplicaID := "pvc-vol-3/" + canonicalID + localSender := bs.v2Orchestrator.Registry.Sender(expectedLocalReplicaID) + if localSender == nil { + t.Fatalf("local replica sender not found for %s — canonical ID not used", expectedLocalReplicaID) + } + + t.Logf("P10P1 canonical identity: localServerID=%q → local ReplicaID=%s", canonicalID, expectedLocalReplicaID) +} + +// --- Fail-closed: missing ServerID on proto wire --- + +func TestP10P1_FailClosed_MissingServerID(t *testing.T) { + // When the proto assignment has addresses but no ServerID, + // the ControlBridge must skip that replica (fail closed). + + bs := &BlockService{ + v2Bridge: v2bridge.NewControlBridge(), + v2Orchestrator: engine.NewRecoveryOrchestrator(), + localServerID: "vs1:18080", + } + + bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{ + { + Path: "pvc-vol-4", + Epoch: 1, + Role: uint32(blockvol.RolePrimary), + ReplicaDataAddr: "10.0.0.2:9333", + ReplicaCtrlAddr: "10.0.0.2:9334", + // ReplicaServerID intentionally empty — should be skipped. + }, + }) + + // No sender should exist — the replica was skipped due to missing ServerID. + // The ControlBridge logs "scalar replica assignment without ServerID" and skips. + // Check that no sender exists with an address-derived ID. + addressDerivedID := "pvc-vol-4/10.0.0.2:9333" + if bs.v2Orchestrator.Registry.Sender(addressDerivedID) != nil { + t.Fatal("address-derived sender should NOT exist — missing ServerID must fail closed") + } + + t.Log("P10P1 fail-closed: missing ServerID → replica skipped, no address-derived fallback") +} diff --git a/weed/server/volume_server_block.go b/weed/server/volume_server_block.go index 399bb9543..8916de98f 100644 --- a/weed/server/volume_server_block.go +++ b/weed/server/volume_server_block.go @@ -22,6 +22,8 @@ import ( type volReplState struct { replicaDataAddr string replicaCtrlAddr string + // allReplicas stores the full replica set for multi-replica idempotence. + allReplicas []blockvol.ReplicaAddr } // NVMeConfig holds NVMe/TCP target configuration passed from CLI flags. @@ -51,6 +53,11 @@ type BlockService struct { // V2 engine bridge (Phase 08 P1). v2Bridge *v2bridge.ControlBridge v2Orchestrator *engine.RecoveryOrchestrator + v2Recovery *RecoveryManager + + // P3: last-applied assignment per volume path for idempotence. + lastAssignMu sync.RWMutex + lastAssign map[string]lastAppliedAssignment // localServerID: stable identity for this volume server. // INTERIM: uses listenAddr (transport-shaped). Should be replaced // with a registry-assigned stable server ID in a later hardening pass. @@ -62,6 +69,13 @@ func (bs *BlockService) V2Orchestrator() *engine.RecoveryOrchestrator { return bs.v2Orchestrator } +// SetServerID sets the stable server identity for V2 control semantics. +// Should be called with the gRPC address that the master knows this VS by, +// replacing the interim listenAddr-based identity. +func (bs *BlockService) SetServerID(id string) { + bs.localServerID = id +} + // WireStateChangeNotify sets up shipper state change callbacks on all // registered volumes so that degradation/recovery triggers an immediate // heartbeat via the provided channel. Non-blocking send (buffered chan 1). @@ -103,6 +117,7 @@ func StartBlockService(listenAddr, blockDir, iqnPrefix, portalAddr string, nvmeC v2Orchestrator: engine.NewRecoveryOrchestrator(), localServerID: listenAddr, // INTERIM: transport-shaped, see field doc } + bs.v2Recovery = NewRecoveryManager(bs) // iSCSI target setup. logger := log.New(os.Stderr, "iscsi: ", log.LstdFlags) @@ -347,13 +362,27 @@ func (bs *BlockService) DeleteBlockVol(name string) error { // V2 bridge: also delivers each assignment to the V2 engine for recovery ownership. func (bs *BlockService) ProcessAssignments(assignments []blockvol.BlockVolumeAssignment) { // V2 bridge: convert and deliver to engine orchestrator (Phase 08 P1). + // P3: skip V2 processing for repeated unchanged assignments. + // P4: RecoveryManager starts/cancels recovery goroutines based on results. if bs.v2Bridge != nil && bs.v2Orchestrator != nil { for _, a := range assignments { + // P3 idempotence: skip V2 processing if this assignment is + // materially unchanged from the last one applied for this path. + if bs.isAssignmentUnchanged(a) { + continue + } + bs.recordAppliedAssignment(a) + intent := bs.v2Bridge.ConvertAssignment(a, bs.localServerID) result := bs.v2Orchestrator.ProcessAssignment(intent) glog.V(1).Infof("v2bridge: assignment %s epoch=%d → added=%d removed=%d sessions=%d", a.Path, a.Epoch, len(result.Added), len(result.Removed), len(result.SessionsCreated)+len(result.SessionsSuperseded)) + + // P4: drive live recovery execution based on engine result. + if bs.v2Recovery != nil && (len(result.SessionsCreated) > 0 || len(result.SessionsSuperseded) > 0 || len(result.Removed) > 0) { + bs.v2Recovery.HandleAssignmentResult(result, assignments) + } } } @@ -397,6 +426,15 @@ func (bs *BlockService) ProcessAssignments(assignments []blockvol.BlockVolumeAss // setupPrimaryReplication configures WAL shipping from primary to replica // and starts the rebuild server (R1-2). func (bs *BlockService) setupPrimaryReplication(path, replicaDataAddr, replicaCtrlAddr string) { + // P3 idempotence: skip if replica state is unchanged. + bs.replMu.RLock() + existing := bs.replStates[path] + bs.replMu.RUnlock() + if existing != nil && existing.replicaDataAddr == replicaDataAddr && existing.replicaCtrlAddr == replicaCtrlAddr { + // Unchanged repeated assignment — idempotent, no side effects. + return + } + // Compute deterministic rebuild listen address. _, _, rebuildPort := bs.ReplicationPorts(path) host := bs.listenAddr @@ -436,6 +474,17 @@ func (bs *BlockService) setupPrimaryReplication(path, replicaDataAddr, replicaCt // setupPrimaryReplicationMulti configures WAL shipping from primary to N replicas // using SetReplicaAddrs (CP8-2: multi-replica support). func (bs *BlockService) setupPrimaryReplicationMulti(path string, addrs []blockvol.ReplicaAddr) { + // P3 idempotence: skip if ALL replica addresses unchanged. + // Compare full replica set, not just the first entry. + if len(addrs) > 0 { + bs.replMu.RLock() + existing := bs.replStates[path] + bs.replMu.RUnlock() + if existing != nil && bs.multiReplicaUnchanged(path, addrs) { + return + } + } + // Compute deterministic rebuild listen address. _, _, rebuildPort := bs.ReplicationPorts(path) host := bs.listenAddr @@ -459,11 +508,15 @@ func (bs *BlockService) setupPrimaryReplicationMulti(path string, addrs []blockv if bs.replStates == nil { bs.replStates = make(map[string]*volReplState) } - // Store first replica in replState for backward compat heartbeat reporting. + // Store full replica set + first replica for backward compat heartbeat. if len(addrs) > 0 { + // Copy the addrs slice to avoid aliasing. + copied := make([]blockvol.ReplicaAddr, len(addrs)) + copy(copied, addrs) bs.replStates[path] = &volReplState{ replicaDataAddr: addrs[0].DataAddr, replicaCtrlAddr: addrs[0].CtrlAddr, + allReplicas: copied, } } bs.replMu.Unlock() @@ -560,6 +613,15 @@ func (bs *BlockService) DeleteBlockSnapshot(name string, snapID uint32) error { }) } +// RestoreBlockSnapshot restores the named volume to the specified snapshot. +// This is a destructive operation: all writes after the snapshot are lost. +func (bs *BlockService) RestoreBlockSnapshot(name string, snapID uint32) error { + path := bs.volumePath(name) + return bs.blockStore.WithVolume(path, func(vol *blockvol.BlockVol) error { + return vol.RestoreSnapshot(snapID) + }) +} + // ListBlockSnapshots lists all snapshots on the named volume. func (bs *BlockService) ListBlockSnapshots(name string) ([]blockvol.SnapshotInfo, uint64, error) { path := bs.volumePath(name) @@ -656,6 +718,101 @@ func (bs *BlockService) CollectBlockVolumeHeartbeat() []blockvol.BlockVolumeInfo return msgs } +// multiReplicaUnchanged checks if the full replica set is unchanged. +func (bs *BlockService) multiReplicaUnchanged(path string, addrs []blockvol.ReplicaAddr) bool { + bs.replMu.RLock() + defer bs.replMu.RUnlock() + existing, ok := bs.replStates[path] + if !ok || existing == nil { + return false + } + if len(existing.allReplicas) != len(addrs) { + return false + } + for i := range addrs { + if existing.allReplicas[i].DataAddr != addrs[i].DataAddr || + existing.allReplicas[i].CtrlAddr != addrs[i].CtrlAddr || + existing.allReplicas[i].ServerID != addrs[i].ServerID { + return false + } + } + return true +} + +// --- P3: Assignment idempotence --- + +// lastAppliedAssignment stores the full assignment for idempotence comparison. +// Keyed by volume path. +type lastAppliedAssignment struct { + Path string + Epoch uint64 + Role uint32 + ReplicaServerID string + ReplicaDataAddr string + ReplicaCtrlAddr string + ReplicaAddrs []blockvol.ReplicaAddr +} + +func lastAppliedFrom(a blockvol.BlockVolumeAssignment) lastAppliedAssignment { + // Copy the slice to avoid aliasing. + var addrs []blockvol.ReplicaAddr + if len(a.ReplicaAddrs) > 0 { + addrs = make([]blockvol.ReplicaAddr, len(a.ReplicaAddrs)) + copy(addrs, a.ReplicaAddrs) + } + return lastAppliedAssignment{ + Path: a.Path, + Epoch: a.Epoch, + Role: a.Role, + ReplicaServerID: a.ReplicaServerID, + ReplicaDataAddr: a.ReplicaDataAddr, + ReplicaCtrlAddr: a.ReplicaCtrlAddr, + ReplicaAddrs: addrs, + } +} + +func (la lastAppliedAssignment) equals(a blockvol.BlockVolumeAssignment) bool { + if la.Path != a.Path || la.Epoch != a.Epoch || la.Role != a.Role { + return false + } + if la.ReplicaServerID != a.ReplicaServerID || la.ReplicaDataAddr != a.ReplicaDataAddr || la.ReplicaCtrlAddr != a.ReplicaCtrlAddr { + return false + } + if len(la.ReplicaAddrs) != len(a.ReplicaAddrs) { + return false + } + for i := range la.ReplicaAddrs { + if la.ReplicaAddrs[i].DataAddr != a.ReplicaAddrs[i].DataAddr || + la.ReplicaAddrs[i].CtrlAddr != a.ReplicaAddrs[i].CtrlAddr || + la.ReplicaAddrs[i].ServerID != a.ReplicaAddrs[i].ServerID { + return false + } + } + return true +} + +func (bs *BlockService) isAssignmentUnchanged(a blockvol.BlockVolumeAssignment) bool { + bs.lastAssignMu.RLock() + defer bs.lastAssignMu.RUnlock() + if bs.lastAssign == nil { + return false + } + last, ok := bs.lastAssign[a.Path] + if !ok { + return false + } + return last.equals(a) +} + +func (bs *BlockService) recordAppliedAssignment(a blockvol.BlockVolumeAssignment) { + bs.lastAssignMu.Lock() + defer bs.lastAssignMu.Unlock() + if bs.lastAssign == nil { + bs.lastAssign = make(map[string]lastAppliedAssignment) + } + bs.lastAssign[a.Path] = lastAppliedFrom(a) +} + // ReplicationPorts computes deterministic replication ports for a volume. // Ports are derived from a hash of the volume path offset from the iSCSI base port. func (bs *BlockService) ReplicationPorts(volPath string) (dataPort, ctrlPort, rebuildPort int) { @@ -681,6 +838,10 @@ func (bs *BlockService) Shutdown() { return } glog.V(0).Infof("block service: shutting down...") + // P4: drain active recovery goroutines before closing volumes. + if bs.v2Recovery != nil { + bs.v2Recovery.Shutdown() + } if bs.nvmeServer != nil { bs.nvmeServer.Close() } diff --git a/weed/storage/blockvol/block_heartbeat_proto.go b/weed/storage/blockvol/block_heartbeat_proto.go index a978f8857..3cbac72f1 100644 --- a/weed/storage/blockvol/block_heartbeat_proto.go +++ b/weed/storage/blockvol/block_heartbeat_proto.go @@ -106,11 +106,13 @@ func AssignmentToProto(a BlockVolumeAssignment) *master_pb.BlockVolumeAssignment ReplicaDataAddr: a.ReplicaDataAddr, ReplicaCtrlAddr: a.ReplicaCtrlAddr, RebuildAddr: a.RebuildAddr, + ReplicaServerId: a.ReplicaServerID, // V2: stable identity } for _, ra := range a.ReplicaAddrs { pb.ReplicaAddrs = append(pb.ReplicaAddrs, &master_pb.ReplicaAddrMessage{ DataAddr: ra.DataAddr, CtrlAddr: ra.CtrlAddr, + ServerId: ra.ServerID, // V2: stable identity }) } return pb @@ -123,11 +125,12 @@ func AssignmentFromProto(p *master_pb.BlockVolumeAssignment) BlockVolumeAssignme return BlockVolumeAssignment{} } a := BlockVolumeAssignment{ - Path: p.Path, - Epoch: p.Epoch, - Role: p.Role, - LeaseTtlMs: p.LeaseTtlMs, - RebuildAddr: p.RebuildAddr, + Path: p.Path, + Epoch: p.Epoch, + Role: p.Role, + LeaseTtlMs: p.LeaseTtlMs, + RebuildAddr: p.RebuildAddr, + ReplicaServerID: p.ReplicaServerId, // V2: stable identity } if len(p.ReplicaAddrs) > 0 { // Multi-replica: populate ReplicaAddrs, leave scalar fields empty. @@ -135,6 +138,7 @@ func AssignmentFromProto(p *master_pb.BlockVolumeAssignment) BlockVolumeAssignme a.ReplicaAddrs = append(a.ReplicaAddrs, ReplicaAddr{ DataAddr: ra.DataAddr, CtrlAddr: ra.CtrlAddr, + ServerID: ra.ServerId, // V2: stable identity }) } } else { diff --git a/weed/storage/blockvol/block_heartbeat_proto_test.go b/weed/storage/blockvol/block_heartbeat_proto_test.go index 591ebc8fd..3a020c231 100644 --- a/weed/storage/blockvol/block_heartbeat_proto_test.go +++ b/weed/storage/blockvol/block_heartbeat_proto_test.go @@ -464,3 +464,76 @@ func TestInfoMessage_HealthFieldsZeroDefault(t *testing.T) { t.Fatal("ReplicaDegraded zero default should be false") } } + +// --- Phase 10 P1: Stable server identity on the proto wire --- + +func TestP10P1_ProtoRoundTrip_ScalarServerID(t *testing.T) { + a := BlockVolumeAssignment{ + Path: "/data/vol1.blk", + Epoch: 5, + Role: RoleToWire(RolePrimary), + ReplicaDataAddr: "10.0.0.2:9333", + ReplicaCtrlAddr: "10.0.0.2:9334", + ReplicaServerID: "vs2-grpc:18080", + RebuildAddr: "10.0.0.1:5000", + } + + pb := AssignmentToProto(a) + if pb.ReplicaServerId != "vs2-grpc:18080" { + t.Fatalf("proto encode: ReplicaServerId=%q, want vs2-grpc:18080", pb.ReplicaServerId) + } + + decoded := AssignmentFromProto(pb) + if decoded.ReplicaServerID != "vs2-grpc:18080" { + t.Fatalf("proto decode: ReplicaServerID=%q, want vs2-grpc:18080", decoded.ReplicaServerID) + } +} + +func TestP10P1_ProtoRoundTrip_MultiReplicaServerID(t *testing.T) { + a := BlockVolumeAssignment{ + Path: "/data/vol1.blk", + Epoch: 5, + Role: RoleToWire(RolePrimary), + ReplicaAddrs: []ReplicaAddr{ + {DataAddr: "10.0.0.2:9333", CtrlAddr: "10.0.0.2:9334", ServerID: "vs2-grpc:18080"}, + {DataAddr: "10.0.0.3:9333", CtrlAddr: "10.0.0.3:9334", ServerID: "vs3-grpc:18080"}, + }, + } + + pb := AssignmentToProto(a) + if pb.ReplicaAddrs[0].ServerId != "vs2-grpc:18080" { + t.Fatalf("encode[0]: ServerId=%q", pb.ReplicaAddrs[0].ServerId) + } + if pb.ReplicaAddrs[1].ServerId != "vs3-grpc:18080" { + t.Fatalf("encode[1]: ServerId=%q", pb.ReplicaAddrs[1].ServerId) + } + + decoded := AssignmentFromProto(pb) + if decoded.ReplicaAddrs[0].ServerID != "vs2-grpc:18080" { + t.Fatalf("decode[0]: ServerID=%q", decoded.ReplicaAddrs[0].ServerID) + } + if decoded.ReplicaAddrs[1].ServerID != "vs3-grpc:18080" { + t.Fatalf("decode[1]: ServerID=%q", decoded.ReplicaAddrs[1].ServerID) + } +} + +func TestP10P1_ProtoRoundTrip_MissingServerID_NotSynthesized(t *testing.T) { + a := BlockVolumeAssignment{ + Path: "/data/vol1.blk", + Epoch: 5, + Role: RoleToWire(RolePrimary), + ReplicaDataAddr: "10.0.0.2:9333", + ReplicaCtrlAddr: "10.0.0.2:9334", + // ReplicaServerID intentionally empty. + } + + pb := AssignmentToProto(a) + if pb.ReplicaServerId != "" { + t.Fatalf("encode: ReplicaServerId=%q, want empty", pb.ReplicaServerId) + } + + decoded := AssignmentFromProto(pb) + if decoded.ReplicaServerID != "" { + t.Fatalf("decode: ReplicaServerID=%q, want empty (not synthesized)", decoded.ReplicaServerID) + } +}