mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-24 13:16:28 +00:00
fix(notify): close core notify correctness and safety gaps (#4502)
Land the remaining notify-crate audit fixes. backlog#979(b): remove_target now enforces the same bucket-binding guard as remove_target_config, refusing to delete a target still referenced by a bucket rule so notification rules are not left orphaned. backlog#984: - event.rs: an unversioned object omits versionId entirely instead of serializing versionId:"" (empty object/request versions treated as "no version"). - notifier.rs: RUSTFS_NOTIFY_SEND_CONCURRENCY=0 coerces back to the default instead of building a zero-permit semaphore that deadlocks every dispatch; init_bucket_targets_shared closes the replaced targets instead of dropping them without close() (connection leak). - subscriber_index.rs: store_snapshot uses an atomic compute_if_absent upsert, removing the get-then-insert TOCTOU that could clobber a concurrent first-writer's snapshot cell. - pipeline.rs: send_event assigns the history sequence and broadcasts to live subscribers under one critical section so broadcast order matches recorded sequence order. - xml_config.rs: filter value length is bounded by character count, not byte length, so valid multi-byte keys are no longer wrongly rejected. - global.rs: a losing initialize() race shuts the just-initialized system down instead of leaking its targets/replay workers. backlog#970 (notify part): reload_config stops the running replay workers before activating the new ones, so old and new workers do not concurrently drain the same persisted stores. The full signal+join shutdown lives in the targets crate under the same issue. Tests: added regression coverage for each fix. cargo build -p rustfs-notify, cargo test -p rustfs-notify --lib (98 passed), cargo clippy -p rustfs-notify --all-targets (clean). Relates to rustfs/backlog#979 Relates to rustfs/backlog#984 Relates to rustfs/backlog#970 Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
@@ -249,7 +249,19 @@ impl Event {
|
||||
let key_name = form_urlencoded::byte_serialize(args.object.name.as_bytes()).collect::<String>();
|
||||
let principal_id = args.req_params.get("principalId").unwrap_or(&String::new()).to_string();
|
||||
|
||||
let version_id = args.object.version_id.clone().or_else(|| Some(args.version_id.clone()));
|
||||
// An unversioned object must omit `versionId` entirely rather than emit it as
|
||||
// an empty string. Prefer the object's own version id, fall back to the
|
||||
// request-scoped one, and treat an empty value from either source as "no
|
||||
// version" so serialization skips the field (`skip_serializing_if` on
|
||||
// `Object::version_id`). This matches S3 and the repo's tier convention that a
|
||||
// `None`/`""` version means "unversioned" (backlog#984).
|
||||
let version_id = args
|
||||
.object
|
||||
.version_id
|
||||
.clone()
|
||||
.filter(|v| !v.is_empty())
|
||||
.or_else(|| Some(args.version_id.clone()))
|
||||
.filter(|v| !v.is_empty());
|
||||
|
||||
let mut s3_metadata = Metadata {
|
||||
schema_version: "1.0".to_string(),
|
||||
@@ -619,6 +631,68 @@ mod tests {
|
||||
assert_eq!(user_metadata.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unversioned_object_omits_version_id() {
|
||||
// Neither the object nor the request carries a version id: the field must be
|
||||
// `None` so it is omitted from the serialized event, not `Some("")` (backlog#984).
|
||||
let args = EventArgsBuilder::new(
|
||||
EventName::ObjectCreatedPut,
|
||||
"bucket",
|
||||
NotifyObjectInfo {
|
||||
bucket: "bucket".to_string(),
|
||||
name: "key".to_string(),
|
||||
version_id: None,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.version_id(String::new())
|
||||
.build();
|
||||
let event = Event::new(args);
|
||||
assert_eq!(event.s3.object.version_id, None);
|
||||
|
||||
let json = serde_json::to_value(&event).expect("event should serialize");
|
||||
assert!(
|
||||
json.pointer("/s3/object/versionId").is_none(),
|
||||
"unversioned object must not serialize a versionId field"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_object_version_falls_back_then_omits() {
|
||||
// Empty object version must not shadow a real request-scoped version.
|
||||
let args = EventArgsBuilder::new(
|
||||
EventName::ObjectCreatedPut,
|
||||
"bucket",
|
||||
NotifyObjectInfo {
|
||||
bucket: "bucket".to_string(),
|
||||
name: "key".to_string(),
|
||||
version_id: Some(String::new()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.version_id("v-42".to_string())
|
||||
.build();
|
||||
let event = Event::new(args);
|
||||
assert_eq!(event.s3.object.version_id.as_deref(), Some("v-42"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn present_object_version_is_preserved() {
|
||||
let args = EventArgsBuilder::new(
|
||||
EventName::ObjectCreatedPut,
|
||||
"bucket",
|
||||
NotifyObjectInfo {
|
||||
bucket: "bucket".to_string(),
|
||||
name: "key".to_string(),
|
||||
version_id: Some("v-1".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.build();
|
||||
let event = Event::new(args);
|
||||
assert_eq!(event.s3.object.version_id.as_deref(), Some("v-1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_time_serializes_with_millisecond_precision() {
|
||||
let mut event = Event::new_test_event("bucket", "key", EventName::ObjectCreatedPut);
|
||||
|
||||
Reference in New Issue
Block a user