fakecloud_s3/
simulation.rs1use crate::lifecycle::LifecycleProcessor;
2use crate::state::SharedS3State;
3
4pub struct LifecycleTickResult {
6 pub processed_buckets: u64,
7 pub expired_objects: u64,
8 pub transitioned_objects: u64,
9}
10
11struct BucketSnapshot {
13 name: String,
14 object_count: usize,
15 storage_classes: Vec<(String, String)>,
16}
17
18pub fn tick_lifecycle(state: &SharedS3State) -> LifecycleTickResult {
20 let (buckets_with_lifecycle, before_snapshot) = {
22 let __mas = state.read();
23 let mut count = 0u64;
24 let mut snapshot: Vec<BucketSnapshot> = Vec::new();
25 for (_, s) in __mas.iter() {
26 for bucket in s.buckets.values() {
27 let classes: Vec<(String, String)> = bucket
28 .objects
29 .iter()
30 .map(|(k, o)| (k.clone(), o.storage_class.clone()))
31 .collect();
32 snapshot.push(BucketSnapshot {
33 name: bucket.name.clone(),
34 object_count: bucket.objects.len(),
35 storage_classes: classes,
36 });
37 if bucket.lifecycle_config.is_some() {
38 count += 1;
39 }
40 }
41 }
42 (count, snapshot)
43 };
44
45 let processor = LifecycleProcessor::new(state.clone());
47 processor.tick();
48
49 let mut expired_objects = 0u64;
51 let mut transitioned_objects = 0u64;
52
53 let __mas = state.read();
54 for snap in &before_snapshot {
55 let bucket = __mas.iter().find_map(|(_, s)| s.buckets.get(&snap.name));
57 let bucket = match bucket {
58 Some(b) => b,
59 None => continue,
60 };
61
62 let after_count = bucket.objects.len();
64 if snap.object_count > after_count {
65 expired_objects += (snap.object_count - after_count) as u64;
66 }
67
68 for (key, old_class) in &snap.storage_classes {
70 if let Some(obj) = bucket.objects.get(key) {
71 if &obj.storage_class != old_class {
72 transitioned_objects += 1;
73 }
74 }
75 }
76 }
77
78 LifecycleTickResult {
79 processed_buckets: buckets_with_lifecycle,
80 expired_objects,
81 transitioned_objects,
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::state::{S3Bucket, S3Object};
89 use bytes::Bytes;
90 use chrono::{Duration, Utc};
91 use parking_lot::RwLock;
92 use std::sync::Arc;
93
94 fn make_state() -> SharedS3State {
95 Arc::new(RwLock::new(
96 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
97 ))
98 }
99
100 fn make_object(key: &str, age_days: i64) -> S3Object {
101 S3Object {
102 key: key.to_string(),
103 body: crate::state::memory_body(Bytes::from("test")),
104 content_type: "application/octet-stream".to_string(),
105 etag: "\"abc\"".to_string(),
106 size: 4,
107 last_modified: Utc::now() - Duration::days(age_days),
108 storage_class: "STANDARD".to_string(),
109 ..Default::default()
110 }
111 }
112
113 #[test]
114 fn tick_lifecycle_expires_objects() {
115 let state = make_state();
116
117 {
118 let mut __mas = state.write();
119 let s = __mas.default_mut();
120 let mut bucket = S3Bucket::new("test-bucket", "us-east-1", "123456789012");
121 bucket.lifecycle_config = Some(
122 r#"<LifecycleConfiguration>
123 <Rule>
124 <Filter><Prefix></Prefix></Filter>
125 <Status>Enabled</Status>
126 <Expiration><Days>1</Days></Expiration>
127 </Rule>
128 </LifecycleConfiguration>"#
129 .to_string(),
130 );
131 bucket
132 .objects
133 .insert("old-file.txt".to_string(), make_object("old-file.txt", 5));
134 bucket
135 .objects
136 .insert("new-file.txt".to_string(), make_object("new-file.txt", 0));
137 s.buckets.insert("test-bucket".to_string(), bucket);
138 }
139
140 let result = tick_lifecycle(&state);
141 assert_eq!(result.processed_buckets, 1);
142 assert_eq!(result.expired_objects, 1);
143 assert_eq!(result.transitioned_objects, 0);
144
145 let __mas = state.read();
146 let s = __mas.default_ref();
147 let bucket = s.buckets.get("test-bucket").unwrap();
148 assert_eq!(bucket.objects.len(), 1);
149 assert!(bucket.objects.contains_key("new-file.txt"));
150 }
151
152 #[test]
153 fn tick_lifecycle_transitions_objects() {
154 let state = make_state();
155
156 {
157 let mut __mas = state.write();
158 let s = __mas.default_mut();
159 let mut bucket = S3Bucket::new("trans-bucket", "us-east-1", "123456789012");
160 bucket.lifecycle_config = Some(
161 r#"<LifecycleConfiguration>
162 <Rule>
163 <Filter><Prefix></Prefix></Filter>
164 <Status>Enabled</Status>
165 <Transition>
166 <Days>1</Days>
167 <StorageClass>GLACIER</StorageClass>
168 </Transition>
169 </Rule>
170 </LifecycleConfiguration>"#
171 .to_string(),
172 );
173 bucket
174 .objects
175 .insert("old-file.txt".to_string(), make_object("old-file.txt", 5));
176 s.buckets.insert("trans-bucket".to_string(), bucket);
177 }
178
179 let result = tick_lifecycle(&state);
180 assert_eq!(result.processed_buckets, 1);
181 assert_eq!(result.expired_objects, 0);
182 assert_eq!(result.transitioned_objects, 1);
183
184 let __mas = state.read();
185 let s = __mas.default_ref();
186 let obj = s.buckets["trans-bucket"]
187 .objects
188 .get("old-file.txt")
189 .unwrap();
190 assert_eq!(obj.storage_class, "GLACIER");
191 }
192
193 #[test]
194 fn tick_lifecycle_no_config_returns_zero() {
195 let state = make_state();
196
197 {
198 let mut __mas = state.write();
199 let s = __mas.default_mut();
200 let bucket = S3Bucket::new("empty-bucket", "us-east-1", "123456789012");
201 s.buckets.insert("empty-bucket".to_string(), bucket);
202 }
203
204 let result = tick_lifecycle(&state);
205 assert_eq!(result.processed_buckets, 0);
206 assert_eq!(result.expired_objects, 0);
207 assert_eq!(result.transitioned_objects, 0);
208 }
209}