mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-29 11:15:34 +00:00
volume server: VolumeMarkReadonly answers NotFound when the volume vanished under the lock (#11443)
* volume server: VolumeMarkReadonly answers NotFound when the volume vanished under the lock
make_volume_readonly looked the volume up, notified the master (step 1),
then took the store write lock (step 2) and marked the volume only `if
let Some(..)`. When the volume left the store during step 1 -- a master
round trip, during which an unmount or a heartbeat expiry can land --
the missing else meant the RPC reported success for a volume the server
no longer has, and step 3 told the master again that it is read-only.
Go's Store.MarkVolumeReadonly (weed/storage/store.go) returns
"volume %d not found" when findVolume comes back nil, and
makeVolumeReadonly (weed/server/volume_grpc_admin.go) returns that error
before the step-3 notification. The Rust step 2 now does the same:
find_volume_mut(vid) -> Status::not_found("volume {vid} not found"), and
the `?` skips step 3, as it already did for a set_read_only_persist
failure. The scrub caller already matches NotFound to skip such a
volume instead of failing the whole report; it now actually gets it.
volume_mark_writable already returns NotFound under its write lock.
The regression test opens the step-1 window deterministically: step 1
awaits the current_master_url read lock, so the test holds its write
guard, lets make_volume_readonly park there after its own lookup
succeeded, unmounts the volume, then releases the guard. With no master
configured the notification is a no-op, so the write lock in step 2 is
the only place left that can notice the volume is gone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* volume server: trim comments on the vanished-volume mark-readonly path
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Chris Lu
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
b750853c42
commit
df4995b894
@@ -401,13 +401,15 @@ impl VolumeGrpcService {
|
||||
// Step 1: stop master from redirecting traffic here
|
||||
self.notify_master_volume_readonly(&info, true).await?;
|
||||
|
||||
// Step 2: mark local volume readonly
|
||||
// Step 2: mark local volume readonly; Go's MarkVolumeReadonly errors
|
||||
// when the volume left the store during the step-1 master round trip.
|
||||
{
|
||||
let mut store = self.state.store.write().unwrap();
|
||||
if let Some((_, vol)) = store.find_volume_mut(vid) {
|
||||
vol.set_read_only_persist(can_delete, persist)
|
||||
.map_err(|e| Status::internal(e.to_string()))?;
|
||||
}
|
||||
let (_, vol) = store
|
||||
.find_volume_mut(vid)
|
||||
.ok_or_else(|| Status::not_found(format!("volume {} not found", vid)))?;
|
||||
vol.set_read_only_persist(can_delete, persist)
|
||||
.map_err(|e| Status::internal(e.to_string()))?;
|
||||
self.state.volume_state_notify.notify_one();
|
||||
}
|
||||
|
||||
@@ -8046,6 +8048,37 @@ mod tests {
|
||||
assert!(err.message().contains("volume id 17 not found"), "{}", err);
|
||||
}
|
||||
|
||||
/// A volume can vanish between make_volume_readonly's lookup and its write
|
||||
/// lock, a window spanning the step-1 master round trip; Go's
|
||||
/// MarkVolumeReadonly answers "not found" there. Holding the
|
||||
/// current_master_url write guard parks the call after its lookup, so
|
||||
/// join!'s in-order polls land the unmount inside the window.
|
||||
#[tokio::test]
|
||||
async fn test_make_volume_readonly_answers_not_found_when_volume_vanished_under_lock() {
|
||||
let (service, _tmp) = make_local_service_with_volume("", None);
|
||||
let park = service.state.current_master_url.write().await;
|
||||
|
||||
let (result, ()) = tokio::join!(
|
||||
service.make_volume_readonly(VolumeId(1), false, true),
|
||||
async {
|
||||
assert!(
|
||||
service
|
||||
.state
|
||||
.store
|
||||
.write()
|
||||
.unwrap()
|
||||
.unmount_volume(VolumeId(1)),
|
||||
"the volume must still be mounted when the lookup ran"
|
||||
);
|
||||
drop(park);
|
||||
}
|
||||
);
|
||||
|
||||
let err = result.expect_err("marking a volume that vanished under the lock must fail");
|
||||
assert_eq!(err.code(), tonic::Code::NotFound, "{err:?}");
|
||||
assert!(err.message().contains("volume 1 not found"), "{err:?}");
|
||||
}
|
||||
|
||||
/// Same rule for EC volumes, which the heartbeat also expires under a store
|
||||
/// write: delete_expired_ec_volumes destroys a volume whose destroy time has
|
||||
/// passed, and volume_ec_shards_delete unmounts one on demand.
|
||||
|
||||
Reference in New Issue
Block a user