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