diff --git a/sw-block/.private/phase/phase-17-log.md b/sw-block/.private/phase/phase-17-log.md new file mode 100644 index 000000000..33aa9eebb --- /dev/null +++ b/sw-block/.private/phase/phase-17-log.md @@ -0,0 +1,371 @@ +Purpose: append-only technical pack and delivery log for `Phase 17` +post-`Phase 16` separation tracking. + +--- + +### `Phase 17` Start Note + +Date: 2026-04-04 +Intent: restore a continuous engineering log for the migration batches that +followed `Phase 16` runtime closure + +This phase is intentionally a tracking phase. + +It records the code-separation line that ran after `Phase 16` but before a new +single semantic/runtime claim had replaced it. + +It exists so that: + +1. `Batch 1-9` have one durable phase home +2. reviews can reference a stable migration timeline +3. the next seam can be chosen from a clear current-state snapshot + +--- + +### Batch 1 Delivery Note + +Date: 2026-04-04 +Scope: canonical translation and contract ownership check + +What changed: + +1. canonical replica identity and recovery-target translation were confirmed to + belong to `sw-block/bridge/blockvol` +2. adapter-side inline mapping was removed from `weed/storage/blockvol/v2bridge/control.go` +3. the remaining Batch 1 ports (`reader`, `pinner`, `executor`) were reviewed + and found already aligned with the intended ownership split + +Proof / evidence: + +1. commit `a38e04c03` +2. no further code changes required for `Task B/C/D` + +Conclusion: + +1. Batch 1 closed semantic drift first, without forcing unnecessary code motion + +--- + +### Batch 2 Delivery Note + +Date: 2026-04-04 +Scope: backend-binding shim reduction + +What changed: + +1. `v2bridge.Reader` now returns `bridge.BlockVolState` directly +2. `pinnerShimForRecovery` was removed from `weed/server/block_recovery.go` +3. executor binding was rechecked and kept as the already-correct thin binding + +Proof / evidence: + +1. commit `680b53031` +2. commit `519c84994` + +Conclusion: + +1. the backend-binding layer became thinner without changing semantic ownership + +--- + +### Batch 3 Delivery Note + +Date: 2026-04-04 +Scope: reusable recovery coordination extraction + +What changed: + +1. `sw-block/engine/replication/runtime/pending.go` introduced + `PendingCoordinator` +2. `sw-block/engine/replication/runtime/executor.go` introduced reusable + recovery execution helpers +3. `weed/server/block_recovery.go` was later rewired so production code uses the + runtime helpers directly +4. no-core execution was split into explicit legacy helpers instead of remaining + implicit inline branches + +Proof / evidence: + +1. commit `6fea93e82` +2. commit `e200df779` +3. commit `e075d7761` +4. commit `3a5fbbfde` + +Conclusion: + +1. Batch 3 only became complete after the wiring fix; the final state removes + helper duplication from the production path + +--- + +### Batch 4 Delivery Note + +Date: 2026-04-04 +Scope: typed runtime boundary and host-shell reduction + +What changed: + +1. `PendingExecution` became fully typed +2. type assertions and `interface{}` drift were removed from the production + recovery path +3. rebuild completion shaping moved into a dedicated runtime helper +4. recovery bundle assembly inside `block_recovery.go` was further reduced into + a bounded helper + +Proof / evidence: + +1. commit `0bcfc678d` +2. commit `ded84b25e` + +Conclusion: + +1. Batch 4 made the recovery host shell easier to reason about and safer to + test + +--- + +### Batch 5 Delivery Note + +Date: 2026-04-04 +Scope: recovery binding factory extraction + +What changed: + +1. concrete construction of `Reader`, `Pinner`, `StorageAdapter`, and + `Executor` moved behind `v2bridge.BuildRecoveryBundle()` +2. `weed/server/block_recovery.go` stopped assembling those concrete bindings + directly + +Proof / evidence: + +1. commit `263611004` + +Conclusion: + +1. the backend-binding layer now owns its own assembly seam + +--- + +### Batch 6 Delivery Note + +Date: 2026-04-04 +Scope: recovery context resolver extraction + +What changed: + +1. `resolveRecoveryContext()` consolidated host-side context assembly +2. inline derivation of `rebuildAddr` and related runtime inputs was removed +3. `runCatchUp()` and `runRebuild()` now follow a simple: + - resolve + - plan + - branch + structure + +Proof / evidence: + +1. commit `a48da0f67` +2. commit `41082bf92` + +Conclusion: + +1. the recovery host path became structurally thin enough to review by shape, + not only by behavior + +--- + +### Batch 7 Delivery Note + +Date: 2026-04-04 +Scope: command dispatch extraction + +What changed: + +1. the `engine.Command` switch moved out of `weed/server/volume_server_block.go` +2. new package `weed/server/blockcmd` became the server-adapter command + dispatcher +3. host effects remained intentionally on the server side instead of being + pushed into `v2bridge` + +Proof / evidence: + +1. commit `11c6aaf31` + +Conclusion: + +1. Batch 7 established the correct ownership seam: + - dispatch in server adapter + - backend bindings elsewhere + - host effects still local + +--- + +### Batch 8 Delivery Note + +Date: 2026-04-04 +Scope: `BlockVol` command-binding extraction + +What changed: + +1. concrete `BlockVol` command operations moved into + `weed/storage/blockvol/v2bridge/command_bindings.go` +2. direct `WithVolume` execution for role apply / receiver startup / primary + replication setup stopped living in `volume_server_block.go` + +Proof / evidence: + +1. commit `38b504299` + +Conclusion: + +1. `v2bridge` now owns concrete backend command bindings, which is the correct + side of the seam + +--- + +### Batch 9 Delivery Note + +Date: 2026-04-04 +Scope: non-`BlockVol` command-op extraction + +What changed: + +1. non-backend command operations moved into `weed/server/blockcmd/service_ops.go` +2. dispatcher rebinding no longer requires `volume_server_block.go` to own the + full command-op surface +3. nil-safe service-op construction was added so typed nil pointers are not + smuggled through interfaces + +Proof / evidence: + +1. commit `38b504299` +2. focused server proofs remained green after rebinding + +Conclusion: + +1. after Batch 9, `volume_server_block.go` is much closer to a host shell than + a command-runtime implementation file + +--- + +### Batch 10 Start Note + +Date: 2026-04-04 +Scope: `10A` host-effects adapter extraction + +Execution rule: + +1. extract only command-completion host effects +2. keep the slice bounded to: + - `RecordCommand` + - `EmitCoreEvent` + - `PublishProjection` + - projection-cache write routing +3. do not mix backend readiness mutation into the same cut unless the code + proves it is already inseparable + +Acceptance target: + +1. `coreCommandEffects` disappears from `volume_server_block.go` +2. publish-projection cache writes stop being inline there +3. dispatcher keeps consuming a server-side host-effects object +4. focused proofs stay green + +Why this is the next seam: + +1. dispatch is already extracted +2. backend bindings are already extracted +3. the largest remaining concentrated non-shell logic in + `volume_server_block.go` is now host effects + +--- + +### Batch 10 Delivery Note + +Date: 2026-04-04 +Scope: `10A` host-effects adapter extraction + +What changed: + +1. concrete dispatcher-facing host effects moved into + `weed/server/blockcmd/host_effects.go` +2. `volume_server_block.go` now wires host-effect callbacks and projection cache + storage into that adapter instead of defining `coreCommandEffects` locally +3. server-owned projection cache writes moved behind + `BlockService.StoreProjection()` + +Proof / evidence: + +1. `go test ./weed/server/blockcmd -count=1 -timeout 60s` +2. `go test ./weed/server -count=1 -timeout 120s -run "TestBlockService_(ApplyAssignments|DebugInfoForVolume|CollectBlockVolumeHeartbeat|ReadinessSnapshot|HeartbeatReplicaDegraded)"` +3. result: `PASS` + +Conclusion: + +1. `volume_server_block.go` is thinner again, but host-effect semantics still + remain explicitly on the server side +2. backend readiness mutation was intentionally left untouched in this slice + +--- + +### Batch 11 Delivery Note + +Date: 2026-04-04 +Scope: stop-line review for remaining readiness-state mutation + +What was reviewed: + +1. `noteRoleApplied` +2. `markPrimaryTransportConfigured` +3. `markReceiverReady` +4. `ReadinessSnapshot` as the read-side consumer of the same local state + +Decision: + +1. do not extract these methods into `weed/server/blockcmd` + +Why: + +1. they are direct mutations of `BlockService.replStates` +2. they belong to adapter-local host state, not dispatcher-side orchestration +3. a further extraction would mostly replace direct method calls with callback + plumbing while leaving ownership unchanged + +Accepted stop line: + +1. `blockcmd` keeps dispatch / service ops / host-effects adapter +2. `v2bridge` keeps concrete backend bindings +3. `weed/server` keeps local readiness/cache state and its mutation paths + +Conclusion: + +1. the current boundary is the correct stopping point for the separation line +2. the next meaningful work item should be cleanup within that boundary or a new + semantic/runtime step, not another package shuffle + +--- + +### Current State Snapshot + +Date: 2026-04-04 +State after Batch 10: + +1. `sw-block` owns: + - canonical bridge helpers + - reusable recovery runtime helpers +2. `weed/storage/blockvol/v2bridge` owns: + - concrete `BlockVol` recovery bundle assembly + - concrete `BlockVol` command bindings +3. `weed/server/blockcmd` owns: + - command dispatch + - service-side command operations + - host-effects adapter +4. `weed/server` still owns: + - assignment ingress + - projection/publication cache writes + - host-owned cache/state fields + - product-facing integration state + +Open next seam: + +1. no additional ownership move is currently justified on the readiness-state + path diff --git a/sw-block/.private/phase/phase-17.md b/sw-block/.private/phase/phase-17.md new file mode 100644 index 000000000..a96b7c74c --- /dev/null +++ b/sw-block/.private/phase/phase-17.md @@ -0,0 +1,411 @@ +# Phase 17 + +Date: 2026-04-04 +Status: active +Purpose: track post-`Phase 16` code separation so adapter/runtime ownership in +`weed/server` and backend-binding ownership in `weed/storage/blockvol/v2bridge` +continue to converge without losing engineering continuity + +## Why This Phase Exists + +`Phase 14-16` established and widened one bounded `V2`-owned runtime path: + +1. `Phase 14` made the explicit core real +2. `Phase 15` rebound bounded integrated surfaces to consume core-owned truth +3. `Phase 16` closed one bounded runtime path where the core owns command and + observation semantics + +That semantic/runtime line is now real, but a separate engineering line has +been running in parallel: + +1. move canonical rules toward `sw-block` +2. thin `weed/server` into a host shell +3. keep `v2bridge` limited to backend-binding responsibilities +4. avoid creating a new mixed-ownership layer while the migration proceeds + +This phase exists to track that separation line explicitly so commits, reviews, +and next steps do not drift away from the phase log. + +## Relationship To Phase 16 + +`Phase 16` remains the semantic/runtime-ownership phase. + +`Phase 17` is different: + +1. it does not redefine the bounded `V2` runtime claims from `Phase 16` +2. it tracks code movement and ownership cleanup after those claims became real +3. it records which adapter/binding responsibilities moved where + +In short: + +1. `Phase 16` answers: who owns runtime semantics? +2. `Phase 17` answers: where does the code now live, and what is left to move? + +## Phase Goal + +Track and complete the post-`Phase 16` separation of command/runtime/binding +responsibilities so the codebase approaches this split: + +1. `sw-block` + - owns canonical contracts, core/runtime helpers, and reusable semantics +2. `weed/storage/blockvol/v2bridge` + - owns concrete `BlockVol` and wire/backend bindings +3. `weed/server` + - owns host-shell orchestration, host effects, and product integration only + +## Scope + +### In scope + +1. migration batches that thin `weed/server` +2. migration batches that keep backend binding in `v2bridge` +3. migration batches that move reusable coordination toward `sw-block` +4. explicit tracking of delivered / partial / next separation seams + +### Out of scope + +1. changing the semantic claims already owned by `Phase 14-16` +2. broad protocol redesign +3. full removal of legacy compatibility paths without replacement criteria +4. changing `blockvol` into a `sw-block` package + +## Batch Tracking + +### Batch 1: Contracts And Canonical Translation + +Goal: + +1. stabilize contract ownership and canonical identity/recovery-target mapping + +Status: + +1. delivered + +Delivered result: + +1. `sw-block/bridge/blockvol` owns the canonical contract/helper layer +2. `Task A` removed duplicate identity/recovery-target derivation from adapter + code +3. `Task B/C/D` were confirmed already clean at the acceptance bar + +Evidence: + +1. `a38e04c03` +2. design notes: + - `sw-block/design/v2-first-migration-batch.md` + - `sw-block/design/v2-first-migration-task-pack.md` + +### Batch 2: Backend-Binding Shim Reduction + +Goal: + +1. remove redundant reader/pinner shims and confirm thin backend bindings + +Status: + +1. delivered + +Delivered result: + +1. `v2bridge.Reader` returns bridge contract state directly +2. `pinnerShimForRecovery` was removed +3. `Executor` was confirmed already clean as a direct engine-IO implementer + +Evidence: + +1. `680b53031` +2. `519c84994` +3. design notes: + - `sw-block/design/v2-second-migration-batch.md` + - `sw-block/design/v2-second-migration-task-pack.md` + +### Batch 3: Recovery Runtime Helper Extraction + +Goal: + +1. move reusable recovery coordination out of `weed/server/block_recovery.go` + +Status: + +1. delivered + +Delivered result: + +1. `sw-block/engine/replication/runtime` owns pending coordination and reusable + execution helpers +2. `block_recovery.go` uses those helpers on the production path +3. no-core behavior is structurally isolated behind explicit legacy helpers + +Evidence: + +1. `6fea93e82` +2. `e200df779` +3. `e075d7761` +4. `3a5fbbfde` + +### Batch 4: Typed Runtime Boundary And Host-Shell Reduction + +Goal: + +1. remove `interface{}` drift and simplify recovery host-shell logic + +Status: + +1. delivered + +Delivered result: + +1. `PendingExecution` is fully typed +2. rebuild completion shaping moved into runtime helper +3. `buildRecoveryBundle()` removed repeated host-side assembly + +Evidence: + +1. `0bcfc678d` +2. `ded84b25e` + +### Batch 5: Recovery Binding Factory Extraction + +Goal: + +1. move recovery bundle assembly into the backend-binding layer + +Status: + +1. delivered + +Delivered result: + +1. `block_recovery.go` no longer constructs `Reader/Pinner/StorageAdapter/Executor` + directly +2. `v2bridge.BuildRecoveryBundle()` became the concrete backend-binding seam + +Evidence: + +1. `263611004` + +### Batch 6: Recovery Context Resolver Extraction + +Goal: + +1. make one resolver own the host-side recovery context + +Status: + +1. delivered + +Delivered result: + +1. `resolveRecoveryContext()` owns: + - `volPath` + - `rebuildAddr` + - bundle construction + - replica flushed progress lookup +2. `runCatchUp()` / `runRebuild()` now read as: + - resolve + - plan + - branch + +Evidence: + +1. `a48da0f67` +2. `41082bf92` + +### Batch 7: Command Dispatch Split + +Goal: + +1. move command-switch orchestration out of `volume_server_block.go` +2. keep it out of `v2bridge` + +Status: + +1. delivered + +Delivered result: + +1. `weed/server/blockcmd` owns command dispatch +2. `weed/server` keeps host effects +3. `v2bridge` remains free of `engine.Command` switch / event-emission + semantics + +Evidence: + +1. `11c6aaf31` + +### Batch 8: BlockVol Command-Binding Extraction + +Goal: + +1. move concrete `BlockVol`-backed command operations out of + `volume_server_block.go` + +Status: + +1. delivered + +Delivered result: + +1. `v2bridge.CommandBindings` owns: + - role apply binding + - receiver startup binding + - primary shipper configuration binding + - shipper-connected probe +2. `weed/server` still owns readiness-state updates and event semantics + +Evidence: + +1. `38b504299` + +### Batch 9: Non-BlockVol Service-Ops Extraction + +Goal: + +1. move the remaining non-`BlockVol` command operations out of + `volume_server_block.go` into the server-adapter layer + +Status: + +1. delivered + +Delivered result: + +1. `weed/server/blockcmd.ServiceOps` owns: + - recovery-task startup + - pending catch-up/rebuild execution + - sender invalidation through projection + resolver +2. `volume_server_block.go` now keeps: + - host effects + - backend-facing thin wrappers + - assignment/event ingress + +Evidence: + +1. `38b504299` + +### Batch 10: Host-Effects Adapter Extraction + +Goal: + +1. move the remaining command-completion host effects out of + `volume_server_block.go` while keeping them on the `weed/server` side + +Status: + +1. delivered + +Chosen slice: + +1. `10A`: extract a dedicated server-side host-effects adapter into + `weed/server/blockcmd` +2. keep this slice bounded to: + - `RecordCommand` + - `EmitCoreEvent` + - `PublishProjection` + - projection-cache write routing +3. do not fold backend-side readiness mutation into this first cut + +Acceptance: + +1. `coreCommandEffects` is no longer defined in `volume_server_block.go` +2. `PublishProjection` cache-write logic is no longer inline in + `volume_server_block.go` +3. `weed/server` still owns host-effect semantics, but exposes them through a + thinner adapter object +4. focused proofs remain green + +Delivered result: + +1. `weed/server/blockcmd/host_effects.go` now owns the concrete dispatcher-side + host-effects adapter +2. `volume_server_block.go` now only wires recorder/event/cache dependencies + into that adapter +3. server-owned projection cache writing moved behind `BlockService.StoreProjection()` + +Evidence: + +1. working tree change on top of `38b504299` + +### Batch 11: Stop-Line Review For Readiness Mutation + +Goal: + +1. decide whether the remaining adapter-local readiness mutation should move out + of `volume_server_block.go` + +Status: + +1. delivered + +Decision: + +1. stop at the current package boundary +2. do not extract readiness-state mutation into `weed/server/blockcmd` + +Reason: + +1. the remaining methods are not dispatcher-side host effects +2. they are direct writes to `BlockService`-owned `replStates` +3. they encode adapter-local cache/state semantics, not reusable command + orchestration logic +4. moving them into `blockcmd` would mostly add callback indirection without + clarifying ownership + +Covered methods: + +1. `noteRoleApplied` +2. `markPrimaryTransportConfigured` +3. `markReceiverReady` +4. `ReadinessSnapshot` remains the read-side consumer of the same local state + +Accepted stop line: + +1. `weed/server/blockcmd` + - owns dispatch, service ops, and host-effects adapter +2. `weed/server` + - owns host state fields and adapter-local readiness/cache mutation +3. `weed/storage/blockvol/v2bridge` + - owns concrete backend bindings only + +## Current Position + +The separation line is now in the late-stage thinning phase. + +Current split: + +1. `sw-block` + - canonical contracts / helpers + - runtime coordination helpers +2. `weed/storage/blockvol/v2bridge` + - `BlockVol`-backed recovery and command bindings +3. `weed/server/blockcmd` + - command dispatch, service-side command operations, and host-effects adapter +4. `weed/server` + - assignment ingress, product integration, and adapter-local readiness/cache state + +## Non-Claims + +`Phase 17` does not yet claim: + +1. every remaining host-side helper must move +2. full deletion of compatibility-only paths +3. that `weed/server` becomes empty or trivial + +The stop line is practical, not ideological: + +1. keep real host/product integration in `weed/server` +2. keep backend binding in `v2bridge` +3. keep reusable semantics/runtime helpers out of both + +## Immediate Next Step + +The current separation stop line is now explicit: + +1. keep dispatcher-side logic in `weed/server/blockcmd` +2. keep backend bindings in `weed/storage/blockvol/v2bridge` +3. keep `BlockService` local state mutation in `weed/server` + +Any next task should now be one of two kinds: + +1. cleanup inside the accepted stop line +2. a new semantic/runtime phase, not another ownership-shuffle task diff --git a/weed/server/block_service_host_effects.go b/weed/server/block_service_host_effects.go new file mode 100644 index 000000000..5c4435b59 --- /dev/null +++ b/weed/server/block_service_host_effects.go @@ -0,0 +1,17 @@ +package weed_server + +import engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication" + +// StoreProjection keeps the latest published projection in the server-owned +// adapter cache used by outward/product-facing surfaces. +func (bs *BlockService) StoreProjection(volumeID string, projection engine.PublicationProjection) { + if bs == nil { + return + } + bs.coreProjMu.Lock() + if bs.coreProj == nil { + bs.coreProj = make(map[string]engine.PublicationProjection) + } + bs.coreProj[volumeID] = projection + bs.coreProjMu.Unlock() +} diff --git a/weed/server/blockcmd/dispatch_test.go b/weed/server/blockcmd/dispatch_test.go index 030d788be..6ce3c43e8 100644 --- a/weed/server/blockcmd/dispatch_test.go +++ b/weed/server/blockcmd/dispatch_test.go @@ -189,11 +189,11 @@ type fakeRecoveryCoordinator struct { startedReplica string startedAssigns []blockvol.BlockVolumeAssignment catchUpCalls []struct { - volumeID string + volumeID string targetLSN uint64 } rebuildCalls []struct { - volumeID string + volumeID string targetLSN uint64 } } @@ -205,7 +205,7 @@ func (f *fakeRecoveryCoordinator) StartRecoveryTask(replicaID string, assignment func (f *fakeRecoveryCoordinator) ExecutePendingCatchUp(volumeID string, targetLSN uint64) error { f.catchUpCalls = append(f.catchUpCalls, struct { - volumeID string + volumeID string targetLSN uint64 }{volumeID: volumeID, targetLSN: targetLSN}) return nil @@ -213,7 +213,7 @@ func (f *fakeRecoveryCoordinator) ExecutePendingCatchUp(volumeID string, targetL func (f *fakeRecoveryCoordinator) ExecutePendingRebuild(volumeID string, targetLSN uint64) error { f.rebuildCalls = append(f.rebuildCalls, struct { - volumeID string + volumeID string targetLSN uint64 }{volumeID: volumeID, targetLSN: targetLSN}) return nil @@ -231,6 +231,17 @@ func (f fakeProjectionReader) Projection(volumeID string) (engine.PublicationPro return f.proj, true } +type fakeProjectionCache struct { + published map[string]engine.PublicationProjection +} + +func (f *fakeProjectionCache) StoreProjection(volumeID string, projection engine.PublicationProjection) { + if f.published == nil { + f.published = make(map[string]engine.PublicationProjection) + } + f.published[volumeID] = projection +} + type fakeSessionInvalidator struct { reasons []string states []engine.ReplicaState @@ -257,6 +268,56 @@ func TestServiceOps_StartRecoveryTaskUsesRecoveryCoordinator(t *testing.T) { } } +func TestHostEffects_PublishProjectionPrefersLatestCoreProjection(t *testing.T) { + cache := &fakeProjectionCache{} + effects := NewHostEffects( + nil, + nil, + fakeProjectionReader{ + proj: engine.PublicationProjection{VolumeID: "vol1", Role: engine.RoleReplica}, + ok: true, + }, + cache, + ) + err := effects.PublishProjection("vol1", engine.PublicationProjection{VolumeID: "vol1", Role: engine.RolePrimary}) + if err != nil { + t.Fatal(err) + } + got, ok := cache.published["vol1"] + if !ok { + t.Fatal("expected cached projection") + } + if got.Role != engine.RoleReplica { + t.Fatalf("role=%v", got.Role) + } +} + +func TestHostEffects_RecordAndEmitUseCallbacks(t *testing.T) { + var recorded []string + var events []engine.Event + effects := NewHostEffects( + func(volumeID, name string) { + recorded = append(recorded, volumeID+":"+name) + }, + func(ev engine.Event) { + events = append(events, ev) + }, + nil, + nil, + ) + effects.RecordCommand("vol1", "apply_role") + effects.EmitCoreEvent(engine.RoleApplied{ID: "vol1"}) + if !reflect.DeepEqual(recorded, []string{"vol1:apply_role"}) { + t.Fatalf("recorded=%v", recorded) + } + if len(events) != 1 { + t.Fatalf("events=%d", len(events)) + } + if _, ok := events[0].(engine.RoleApplied); !ok { + t.Fatalf("event=%T", events[0]) + } +} + func TestServiceOps_InvalidateSessionUsesProjectionAndSenderResolver(t *testing.T) { s1 := &fakeSessionInvalidator{} s2 := &fakeSessionInvalidator{} diff --git a/weed/server/blockcmd/host_effects.go b/weed/server/blockcmd/host_effects.go new file mode 100644 index 000000000..413ca196e --- /dev/null +++ b/weed/server/blockcmd/host_effects.go @@ -0,0 +1,66 @@ +package blockcmd + +import engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication" + +// CommandRecorder records one executed command on the host side. +type CommandRecorder func(volumeID, name string) + +// CoreEventEmitter feeds one runtime observation back into the core. +type CoreEventEmitter func(ev engine.Event) + +// ProjectionCacheWriter stores the latest published projection in a host-local +// cache owned by the server adapter. +type ProjectionCacheWriter interface { + StoreProjection(volumeID string, projection engine.PublicationProjection) +} + +type hostEffects struct { + recordCommand CommandRecorder + emitCoreEvent CoreEventEmitter + projection ProjectionReader + cache ProjectionCacheWriter +} + +// NewHostEffects keeps host-side command completion effects in the server +// adapter layer while exposing only the narrow Dispatcher-facing surface. +func NewHostEffects( + recordCommand CommandRecorder, + emitCoreEvent CoreEventEmitter, + projection ProjectionReader, + cache ProjectionCacheWriter, +) HostEffects { + return &hostEffects{ + recordCommand: recordCommand, + emitCoreEvent: emitCoreEvent, + projection: projection, + cache: cache, + } +} + +func (effects *hostEffects) RecordCommand(volumeID, name string) { + if effects == nil || effects.recordCommand == nil { + return + } + effects.recordCommand(volumeID, name) +} + +func (effects *hostEffects) EmitCoreEvent(ev engine.Event) { + if effects == nil || effects.emitCoreEvent == nil { + return + } + effects.emitCoreEvent(ev) +} + +func (effects *hostEffects) PublishProjection(volumeID string, projection engine.PublicationProjection) error { + if effects == nil || effects.cache == nil { + return nil + } + proj := projection + if effects.projection != nil { + if latest, ok := effects.projection.Projection(volumeID); ok { + proj = latest + } + } + effects.cache.StoreProjection(volumeID, proj) + return nil +} diff --git a/weed/server/volume_server_block.go b/weed/server/volume_server_block.go index e20ba8ba3..9e59b6802 100644 --- a/weed/server/volume_server_block.go +++ b/weed/server/volume_server_block.go @@ -11,8 +11,8 @@ import ( bridgeblockvol "github.com/seaweedfs/seaweedfs/sw-block/bridge/blockvol" engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication" - "github.com/seaweedfs/seaweedfs/weed/server/blockcmd" "github.com/seaweedfs/seaweedfs/weed/glog" + "github.com/seaweedfs/seaweedfs/weed/server/blockcmd" "github.com/seaweedfs/seaweedfs/weed/storage" "github.com/seaweedfs/seaweedfs/weed/storage/blockvol" "github.com/seaweedfs/seaweedfs/weed/storage/blockvol/iscsi" @@ -580,6 +580,14 @@ func (bs *BlockService) coreCommandDispatcher() *blockcmd.Dispatcher { if bs != nil && bs.v2Core != nil { projection = bs.v2Core } + var recordCommand blockcmd.CommandRecorder + var emitCoreEvent blockcmd.CoreEventEmitter + var projectionCache blockcmd.ProjectionCacheWriter + if bs != nil { + recordCommand = bs.recordExecutedCoreCommand + emitCoreEvent = bs.applyCoreEvent + projectionCache = bs + } return blockcmd.NewDispatcher( blockcmd.NewServiceOps( coreCommandBackend{bs: bs}, @@ -592,7 +600,7 @@ func (bs *BlockService) coreCommandDispatcher() *blockcmd.Dispatcher { return bs.v2Orchestrator.Registry.Sender(replicaID) }, ), - coreCommandEffects{bs: bs}, + blockcmd.NewHostEffects(recordCommand, emitCoreEvent, projection, projectionCache), ) } @@ -660,43 +668,6 @@ func (ops coreCommandBackend) ConfigureShipper(volumeID string, replicas []engin return true, ops.bs.isPrimaryShipperConnected(volumeID), nil } -type coreCommandEffects struct { - bs *BlockService -} - -func (effects coreCommandEffects) RecordCommand(volumeID, name string) { - if effects.bs == nil { - return - } - effects.bs.recordExecutedCoreCommand(volumeID, name) -} - -func (effects coreCommandEffects) EmitCoreEvent(ev engine.Event) { - if effects.bs == nil { - return - } - effects.bs.applyCoreEvent(ev) -} - -func (effects coreCommandEffects) PublishProjection(volumeID string, projection engine.PublicationProjection) error { - if effects.bs == nil { - return nil - } - proj := projection - if core := effects.bs.V2Core(); core != nil { - if latest, ok := core.Projection(volumeID); ok { - proj = latest - } - } - effects.bs.coreProjMu.Lock() - if effects.bs.coreProj == nil { - effects.bs.coreProj = make(map[string]engine.PublicationProjection) - } - effects.bs.coreProj[volumeID] = proj - effects.bs.coreProjMu.Unlock() - return nil -} - func (bs *BlockService) applyRoleAssignment(a blockvol.BlockVolumeAssignment) error { if bs == nil || bs.blockStore == nil { return nil