From 18670b269b9bfce44c8efa57c66e0f79587209e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E5=B0=8F=E9=B8=AD?= Date: Sat, 5 Sep 2026 17:10:34 +0800 Subject: [PATCH] fix(replication): persist target delete-marker version ids in MRF purge intents (backlog#2290) A delete-marker purge intent that outlived its watch window was journaled without the version ids the targets assigned to the replicated markers. Replay rebuilt the replication state from a blank ObjectInfo, so `delete_marker_purge_version_id` fell back to the source marker id; a generic S3 target that mints its own ids answers that DELETE with 204, the entry was acknowledged and the real marker stayed on the target. - `MrfReplicateEntry` gains `targetDeleteMarkerVersionIDs` (per-ARN map) and `targetDeleteMarkerVersionIDsCorrupt`; both default and are skipped when empty/false, so old journals decode to the pre-existing shape. - `DeletedObjectReplicationInfo::to_mrf_entry` copies both from the source replication state; `reconstructed_heal_delete_info` restores them into the replayed state so the purge addresses the recorded id and a fail-closed refusal stays a refusal after restart. - MRF envelope capability bit `TargetDeleteMarkerVersionIds` (1 << 4) fences the field like `DeleteMarkerMtime`; readers without the bit refuse envelopes that advertise it, current readers accept old ones. (cherry picked from commit ddacaaa185fda7a5f426138ba5b179f986b862d9) --- .../bucket/replication/replication_pool.rs | 97 ++++++++++ crates/replication/src/delete.rs | 85 +++++++++ crates/replication/src/filemeta.rs | 20 +++ crates/replication/src/mrf.rs | 169 +++++++++++++++++- rustfs/src/app/storage_api.rs | 2 + 5 files changed, 371 insertions(+), 2 deletions(-) diff --git a/crates/ecstore/src/bucket/replication/replication_pool.rs b/crates/ecstore/src/bucket/replication/replication_pool.rs index 9efa9864e..e20ad9dbe 100644 --- a/crates/ecstore/src/bucket/replication/replication_pool.rs +++ b/crates/ecstore/src/bucket/replication/replication_pool.rs @@ -882,6 +882,20 @@ fn reconstructed_heal_delete_info( ) -> DeletedObjectReplicationInfo { let mut rstate = oi.replication_state(); rstate.replicate_decision_str = dsc.to_string(); + // The caller hands us a blank ObjectInfo (the source marker may already be + // gone), so the state above carries no target-assigned marker version ids. + // Restore them from the journal: `delete_marker_purge_version_id` must hit + // the id the target reported, not fall back to the source marker id, which + // a target that mints its own ids answers with an idempotent 204 that would + // acknowledge the intent while the real marker stays behind (backlog#2290). + // The corrupt flag rides along so a refusal stays a refusal after restart. + for (arn, version_id) in &entry.target_delete_marker_version_ids { + rstate + .target_delete_marker_version_ids + .entry(arn.clone()) + .or_insert_with(|| version_id.clone()); + } + rstate.target_delete_marker_version_ids_corrupt |= entry.target_delete_marker_version_ids_corrupt; let delete_marker_mtime = entry .delete_marker_mtime @@ -6601,4 +6615,87 @@ mod tests { replacement_data ); } + + /// backlog#2290: a delete-marker purge intent that survives a restart + /// through the MRF journal addresses the marker version the TARGET + /// assigned, exactly as the live watcher does (see the + /// `requires_delayed_purge` spawn). The journal carries the per-ARN ids + /// (`targetDeleteMarkerVersionIDs`) and replay restores them into the + /// reconstructed replication state; without that the replay would fall + /// back to the source marker id, which a target that mints its own ids + /// answers with an idempotent 204 — the entry would be acknowledged while + /// the real marker stayed behind. + #[test] + fn mrf_delete_marker_purge_replay_preserves_target_assigned_marker_version() { + use super::super::replication_object_decision_boundary::{delete_marker_purge_mrf_entry, delete_marker_purge_version_id}; + + let arn = "arn:minio:replication::generic-target:photos".to_string(); + let source_marker = uuid::Uuid::new_v4(); + let remote_marker = "remote-assigned-marker-version".to_string(); + + let live_oi = ObjectInfo { + bucket: "photos".to_string(), + name: "obj".to_string(), + version_id: Some(source_marker), + delete_marker: true, + ..Default::default() + }; + let mut live_state = live_oi.replication_state(); + live_state.replicate_decision_str = replicate_decision_for_admitted_targets(std::slice::from_ref(&arn)).to_string(); + live_state + .target_delete_marker_version_ids + .insert(arn.clone(), remote_marker.clone()); + let live = DeletedObjectReplicationInfo { + delete_object: ReplicationDeletedObject { + object_name: "obj".to_string(), + delete_marker: true, + delete_marker_version_id: Some(source_marker), + replication_state: Some(live_state), + ..Default::default() + }, + bucket: "photos".to_string(), + ..Default::default() + }; + assert_eq!( + delete_marker_purge_version_id(live.delete_object.replication_state.as_ref(), &arn, source_marker), + Some(Some(remote_marker.clone())), + "the live purge addresses the recorded target version" + ); + + // Watch window exhausted: persist the intent, restart, replay it. + let entry = delete_marker_purge_mrf_entry(&live, vec![arn.clone()]); + let replay_oi = ObjectInfo { + bucket: entry.bucket.clone(), + name: entry.object.clone(), + version_id: entry.version_id, + delete_marker: entry.delete_marker, + ..Default::default() + }; + let dsc = replicate_decision_for_admitted_targets(&entry.target_arns); + let replayed = reconstructed_heal_delete_info(&entry, &replay_oi, &dsc); + + assert_eq!( + delete_marker_purge_version_id(replayed.delete_object.replication_state.as_ref(), &arn, source_marker), + Some(Some(remote_marker)), + "the MRF replay must address the target-assigned marker version, not source marker {source_marker}" + ); + + // A refusal (inconsistent recorded ids) must stay a refusal across the + // journal round trip instead of degrading into the source-id fallback. + let mut refused = live; + refused + .delete_object + .replication_state + .as_mut() + .expect("state was set above") + .target_delete_marker_version_ids_corrupt = true; + let entry = delete_marker_purge_mrf_entry(&refused, vec![arn.clone()]); + assert!(entry.target_delete_marker_version_ids_corrupt); + let replayed = reconstructed_heal_delete_info(&entry, &replay_oi, &dsc); + assert_eq!( + delete_marker_purge_version_id(replayed.delete_object.replication_state.as_ref(), &arn, source_marker), + None, + "the MRF replay must keep refusing to guess when the recorded ids were inconsistent" + ); + } } diff --git a/crates/replication/src/delete.rs b/crates/replication/src/delete.rs index ffd014796..ff6fe1591 100644 --- a/crates/replication/src/delete.rs +++ b/crates/replication/src/delete.rs @@ -76,6 +76,21 @@ impl ReplicationWorkerOperation for DeletedObjectReplicationInfo { .delete_object .delete_marker_mtime .and_then(|t| i64::try_from(t.unix_timestamp_nanos()).ok()), + // Carry the target-assigned marker version ids (and the fail-closed corrupt + // flag) into the journal so a purge intent replayed after a restart addresses + // the same version the live path did (backlog#2290). Only delete-marker state + // ever records these; other deletes serialize an empty map. + target_delete_marker_version_ids: self + .delete_object + .replication_state + .as_ref() + .map(|state| state.target_delete_marker_version_ids.clone()) + .unwrap_or_default(), + target_delete_marker_version_ids_corrupt: self + .delete_object + .replication_state + .as_ref() + .is_some_and(|state| state.target_delete_marker_version_ids_corrupt), target_arns: self.admitted_target_arns(), force_delete_id: self.delete_object.force_delete_id, force_delete_generation: self.delete_object.force_delete_generation, @@ -595,6 +610,76 @@ mod tests { assert_eq!(entry.retry_count, 0); assert_eq!(entry.bucket, "bucket-a"); assert_eq!(entry.object, "doc.txt"); + assert!( + entry.target_delete_marker_version_ids.is_empty(), + "no recorded target marker ids means the journal carries none" + ); + assert!(!entry.target_delete_marker_version_ids_corrupt); + } + + /// backlog#2290: a purge intent journaled to MRF must carry the marker + /// version ids the targets assigned, plus the fail-closed corrupt flag, + /// so a replay after restart addresses the same version the live path did. + #[test] + fn delete_marker_purge_mrf_entry_carries_target_assigned_marker_versions() { + let delete_marker_version_id = Uuid::new_v4(); + let mut state = ReplicationState::default(); + state + .target_delete_marker_version_ids + .insert("arn:a".to_string(), "remote-marker-a".to_string()); + state + .target_delete_marker_version_ids + .insert("arn:b".to_string(), "remote-marker-b".to_string()); + let mut dobj = DeletedObjectReplicationInfo { + delete_object: DeletedObject { + object_name: "doc.txt".to_string(), + delete_marker: false, + version_id: Some(Uuid::new_v4()), + delete_marker_version_id: Some(delete_marker_version_id), + replication_state: Some(state), + ..Default::default() + }, + bucket: "bucket-a".to_string(), + ..Default::default() + }; + + let entry = delete_marker_purge_mrf_entry(&dobj, vec!["arn:a".to_string()]); + assert_eq!( + entry.target_delete_marker_version_ids, + HashMap::from([ + ("arn:a".to_string(), "remote-marker-a".to_string()), + ("arn:b".to_string(), "remote-marker-b".to_string()), + ]), + "every recorded target marker id survives the journal, regardless of the retried ARN subset" + ); + assert!(!entry.target_delete_marker_version_ids_corrupt); + assert_eq!( + delete_marker_purge_version_id( + Some(&ReplicationState { + target_delete_marker_version_ids: entry.target_delete_marker_version_ids, + ..Default::default() + }), + "arn:a", + delete_marker_version_id + ), + Some(Some("remote-marker-a".to_string())) + ); + + // The live path refuses to purge on inconsistent metadata and reports the target + // as failed; the journaled intent must keep refusing after a restart. + dobj.delete_object + .replication_state + .as_mut() + .expect("state was set above") + .target_delete_marker_version_ids_corrupt = true; + let entry = delete_marker_purge_mrf_entry(&dobj, vec!["arn:a".to_string()]); + assert!(entry.target_delete_marker_version_ids_corrupt); + + // A delete without replication state journals an empty map. + dobj.delete_object.replication_state = None; + let entry = dobj.to_mrf_entry(); + assert!(entry.target_delete_marker_version_ids.is_empty()); + assert!(!entry.target_delete_marker_version_ids_corrupt); } #[test] diff --git a/crates/replication/src/filemeta.rs b/crates/replication/src/filemeta.rs index 7af1b3141..555b58c57 100644 --- a/crates/replication/src/filemeta.rs +++ b/crates/replication/src/filemeta.rs @@ -641,6 +641,26 @@ pub struct MrfReplicateEntry { #[serde(rename = "deleteMarkerMtime", skip_serializing_if = "Option::is_none", default)] pub delete_marker_mtime: Option, + // For delete-marker purge intents: the exact version id each target assigned to the + // replicated marker, keyed by target ARN. A generic S3 target mints its own version ids + // and answers a DELETE of an unknown id with 204, so a replay that fell back to the source + // marker id would be acknowledged while the real marker stayed behind (backlog#2290). + // Old files lack this key; default=empty means "unknown" and replay keeps the source-id + // fallback it always had. + #[serde(rename = "targetDeleteMarkerVersionIDs", skip_serializing_if = "HashMap::is_empty", default)] + pub target_delete_marker_version_ids: HashMap, + + // Companion to the map above: the source metadata disagreed about the recorded ids when + // the intent was journaled, so the live path refused to guess and reported the target as + // failed. Replay must keep refusing instead of falling back to the source id. Old files + // lack this key; default=false. + #[serde( + rename = "targetDeleteMarkerVersionIDsCorrupt", + skip_serializing_if = "std::ops::Not::not", + default + )] + pub target_delete_marker_version_ids_corrupt: bool, + #[serde(rename = "targetARNs", skip_serializing_if = "Vec::is_empty", default)] pub target_arns: Vec, diff --git a/crates/replication/src/mrf.rs b/crates/replication/src/mrf.rs index 8698e7c2a..285c8bf82 100644 --- a/crates/replication/src/mrf.rs +++ b/crates/replication/src/mrf.rs @@ -31,8 +31,13 @@ const CAPABILITY_OPERATION_KIND: u64 = 1 << 0; const CAPABILITY_TARGET_ARNS: u64 = 1 << 1; const CAPABILITY_FORCE_DELETE: u64 = 1 << 2; const CAPABILITY_DELETE_MARKER_MTIME: u64 = 1 << 3; -const MRF_KNOWN_CAPABILITIES: u64 = - CAPABILITY_OPERATION_KIND | CAPABILITY_TARGET_ARNS | CAPABILITY_FORCE_DELETE | CAPABILITY_DELETE_MARKER_MTIME; +// Per-ARN target-assigned delete-marker version ids on purge intents (backlog#2290). +const CAPABILITY_TARGET_DELETE_MARKER_VERSION_IDS: u64 = 1 << 4; +const MRF_KNOWN_CAPABILITIES: u64 = CAPABILITY_OPERATION_KIND + | CAPABILITY_TARGET_ARNS + | CAPABILITY_FORCE_DELETE + | CAPABILITY_DELETE_MARKER_MTIME + | CAPABILITY_TARGET_DELETE_MARKER_VERSION_IDS; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MrfCapability { @@ -40,6 +45,7 @@ pub enum MrfCapability { TargetArns, ForceDelete, DeleteMarkerMtime, + TargetDeleteMarkerVersionIds, } impl MrfCapability { @@ -49,6 +55,7 @@ impl MrfCapability { Self::TargetArns => CAPABILITY_TARGET_ARNS, Self::ForceDelete => CAPABILITY_FORCE_DELETE, Self::DeleteMarkerMtime => CAPABILITY_DELETE_MARKER_MTIME, + Self::TargetDeleteMarkerVersionIds => CAPABILITY_TARGET_DELETE_MARKER_VERSION_IDS, } } } @@ -601,9 +608,17 @@ pub fn decode_mrf_file(data: &[u8]) -> Result> { #[cfg(test)] mod tests { use super::*; + use std::collections::HashMap; use uuid::Uuid; + // Capability word 31 = OperationKind | TargetArns | ForceDelete | DeleteMarkerMtime | + // TargetDeleteMarkerVersionIds (backlog#2290). const ENVELOPE_FIXTURE: &[u8] = &[ + b'M', b'R', b'F', b'E', 1, 0, 1, 0, 1, 0, 0, 0, 31, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 1, 2, 3, + ]; + + // The envelope a binary from before backlog#2290 writes: same header, capability word 15. + const PRE_TARGET_MARKER_IDS_ENVELOPE_FIXTURE: &[u8] = &[ b'M', b'R', b'F', b'E', 1, 0, 1, 0, 1, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 1, 2, 3, ]; @@ -626,6 +641,8 @@ mod tests { delete_marker_version_id: None, delete_marker: false, delete_marker_mtime: None, + target_delete_marker_version_ids: HashMap::new(), + target_delete_marker_version_ids_corrupt: false, target_arns: vec!["arn:target-a".to_string()], }, MrfReplicateEntry { @@ -642,6 +659,8 @@ mod tests { delete_marker_version_id: None, delete_marker: false, delete_marker_mtime: None, + target_delete_marker_version_ids: HashMap::new(), + target_delete_marker_version_ids_corrupt: false, target_arns: vec!["arn:target-a".to_string(), "arn:target-b".to_string()], }, MrfReplicateEntry { @@ -658,6 +677,11 @@ mod tests { delete_marker_version_id: Some(del_vid), delete_marker: true, delete_marker_mtime: Some(1_705_312_200_123_456_789), + target_delete_marker_version_ids: HashMap::from([ + ("arn:target-a".to_string(), "remote-marker-a".to_string()), + ("arn:target-b".to_string(), "remote-marker-b".to_string()), + ]), + target_delete_marker_version_ids_corrupt: false, target_arns: vec!["arn:target-a".to_string()], }, ]; @@ -685,6 +709,54 @@ mod tests { Some(1_705_312_200_123_456_789), "delete-marker mtime must survive the MRF disk round-trip" ); + assert!(decoded[0].target_delete_marker_version_ids.is_empty()); + assert!(decoded[1].target_delete_marker_version_ids.is_empty()); + assert_eq!( + decoded[2].target_delete_marker_version_ids, + HashMap::from([ + ("arn:target-a".to_string(), "remote-marker-a".to_string()), + ("arn:target-b".to_string(), "remote-marker-b".to_string()), + ]), + "target-assigned marker version ids must survive the MRF disk round-trip (backlog#2290)" + ); + assert!(!decoded[2].target_delete_marker_version_ids_corrupt); + } + + /// backlog#2290: the corrupt flag rides the same journal round trip, and an + /// entry that carries neither field encodes exactly as it did before the + /// field existed (both keys are skipped when empty/false). + #[test] + fn mrf_file_round_trips_target_marker_ids_corrupt_flag_and_skips_empty_keys() { + let corrupt = MrfReplicateEntry { + bucket: "bucket-a".to_string(), + object: "delete-a".to_string(), + op: MrfOpKind::Delete, + delete_marker: true, + delete_marker_version_id: Some(Uuid::new_v4()), + target_delete_marker_version_ids_corrupt: true, + target_arns: vec!["arn:target-a".to_string()], + ..Default::default() + }; + let decoded = decode_mrf_file(&encode_mrf_file(std::slice::from_ref(&corrupt)).expect("mrf file should encode")) + .expect("mrf file should decode"); + assert_eq!(decoded, vec![corrupt]); + assert!(decoded[0].target_delete_marker_version_ids_corrupt); + + let plain = MrfReplicateEntry { + bucket: "bucket-a".to_string(), + object: "delete-a".to_string(), + op: MrfOpKind::Delete, + delete_marker: true, + target_arns: vec!["arn:target-a".to_string()], + ..Default::default() + }; + let encoded = encode_mrf_file(std::slice::from_ref(&plain)).expect("mrf file should encode"); + let payload = String::from_utf8_lossy(&encoded); + assert!( + !payload.contains("targetDeleteMarkerVersionIDs"), + "an entry without recorded ids must not grow the new keys: {payload}" + ); + assert_eq!(decode_mrf_file(&encoded).expect("mrf file should decode"), vec![plain]); } #[test] @@ -719,6 +791,99 @@ mod tests { // Old files lack the deleteMarkerMtime key; it must default to None so replay keeps the // pre-#867 fallback to the current time. assert_eq!(decoded[0].delete_marker_mtime, None); + // Old files also lack the target marker id keys; they must default to an empty map + // and a clear corrupt flag so replay keeps the pre-#2290 source-id fallback. + assert!(decoded[0].target_delete_marker_version_ids.is_empty()); + assert!(!decoded[0].target_delete_marker_version_ids_corrupt); + } + + /// backlog#2290: a delete-marker entry written by a binary that predates the + /// `targetDeleteMarkerVersionIDs` key decodes with an empty map and a clear + /// corrupt flag — the exact shape replay handled before the field existed. + #[test] + fn mrf_pre_target_marker_ids_delete_entry_decodes_with_empty_map() { + let marker_version_id = Uuid::new_v4(); + let mut payload = Vec::new(); + rmp::encode::write_array_len(&mut payload, 1).expect("array len should encode"); + rmp::encode::write_map_len(&mut payload, 9).expect("map len should encode"); + rmp::encode::write_str(&mut payload, "bucket").expect("bucket key should encode"); + rmp::encode::write_str(&mut payload, "old-bucket").expect("bucket value should encode"); + rmp::encode::write_str(&mut payload, "object").expect("object key should encode"); + rmp::encode::write_str(&mut payload, "old-key").expect("object value should encode"); + rmp::encode::write_str(&mut payload, "retryCount").expect("retry key should encode"); + rmp::encode::write_i32(&mut payload, 0).expect("retry value should encode"); + rmp::encode::write_str(&mut payload, "size").expect("size key should encode"); + rmp::encode::write_i64(&mut payload, 0).expect("size value should encode"); + rmp::encode::write_str(&mut payload, "op").expect("op key should encode"); + rmp::encode::write_str(&mut payload, "delete").expect("op value should encode"); + rmp::encode::write_str(&mut payload, "forceDelete").expect("forceDelete key should encode"); + rmp::encode::write_bool(&mut payload, false).expect("forceDelete value should encode"); + rmp::encode::write_str(&mut payload, "deleteMarkerVersionID").expect("marker id key should encode"); + // Uuid serializes as a 16-byte bin in the MessagePack journal. + rmp::encode::write_bin(&mut payload, marker_version_id.as_bytes()).expect("marker id value should encode"); + rmp::encode::write_str(&mut payload, "deleteMarker").expect("deleteMarker key should encode"); + rmp::encode::write_bool(&mut payload, true).expect("deleteMarker value should encode"); + rmp::encode::write_str(&mut payload, "targetARNs").expect("targetARNs key should encode"); + rmp::encode::write_array_len(&mut payload, 1).expect("targetARNs len should encode"); + rmp::encode::write_str(&mut payload, "arn:target-a").expect("targetARNs value should encode"); + + let mut data = Vec::with_capacity(4 + payload.len()); + data.extend_from_slice(&MRF_META_FORMAT.to_le_bytes()); + data.extend_from_slice(&MRF_META_VERSION.to_le_bytes()); + data.extend_from_slice(&payload); + + let decoded = decode_mrf_file(&data).expect("pre-#2290 delete-marker entry should decode"); + + assert_eq!(decoded.len(), 1); + assert_eq!(decoded[0].op, MrfOpKind::Delete); + assert!(decoded[0].delete_marker); + assert_eq!(decoded[0].delete_marker_version_id, Some(marker_version_id)); + assert_eq!(decoded[0].target_arns, vec!["arn:target-a".to_string()]); + assert!(decoded[0].target_delete_marker_version_ids.is_empty()); + assert!(!decoded[0].target_delete_marker_version_ids_corrupt); + } + + /// backlog#2290: the new field is fenced by its own capability bit exactly + /// like the earlier optional fields — a reader without the bit refuses an + /// envelope that advertises it, while the current reader still accepts the + /// pre-#2290 envelope. + #[test] + fn envelope_target_marker_ids_capability_is_fenced_and_backward_compatible() { + assert!(MrfCapabilities::current().contains(MrfCapability::TargetDeleteMarkerVersionIds)); + assert_eq!(MrfCapabilities::with(MrfCapability::TargetDeleteMarkerVersionIds).bits(), 1 << 4); + + // Old envelope, current reader: accepted, and the negotiated set lacks the new bit. + let legacy = MrfEnvelope::decode(PRE_TARGET_MARKER_IDS_ENVELOPE_FIXTURE, MrfProtocolCapabilities::current()) + .expect("pre-#2290 envelope should decode"); + assert_eq!(legacy.protocol().capabilities().bits(), 15); + assert!( + !legacy + .protocol() + .capabilities() + .contains(MrfCapability::TargetDeleteMarkerVersionIds) + ); + assert_eq!(legacy.payload(), &[1, 2, 3]); + + // Current envelope, reader that only knows the pre-#2290 bits: refused. + let pre_2290_reader = MrfProtocolCapabilities::new(1, 1, MrfCapabilities::from_bits(15).expect("known bits")); + assert_eq!( + MrfEnvelope::decode(ENVELOPE_FIXTURE, pre_2290_reader), + Err(MrfEnvelopeError::MissingCapabilities { + required: 31, + available: 15, + }) + ); + + // Negotiation with such a peer drops the bit instead of failing. + let negotiated = MrfProtocolCapabilities::current() + .negotiate(pre_2290_reader) + .expect("negotiation with a pre-#2290 peer should succeed"); + assert!( + !negotiated + .capabilities() + .contains(MrfCapability::TargetDeleteMarkerVersionIds) + ); + assert!(negotiated.capabilities().contains(MrfCapability::DeleteMarkerMtime)); } #[test] diff --git a/rustfs/src/app/storage_api.rs b/rustfs/src/app/storage_api.rs index 9f543ac95..040301180 100644 --- a/rustfs/src/app/storage_api.rs +++ b/rustfs/src/app/storage_api.rs @@ -726,6 +726,8 @@ pub(crate) mod bucket { delete_marker_version_id: None, delete_marker: false, delete_marker_mtime: None, + target_delete_marker_version_ids: Default::default(), + target_delete_marker_version_ids_corrupt: false, target_arns, force_delete_id: Some(operation_id), force_delete_generation: Some(i64::try_from(generation.unix_timestamp_nanos()).unwrap_or(i64::MAX)),