1use std::collections::BTreeMap;
21use std::sync::Arc;
22
23use parking_lot::RwLock;
24use serde::{Deserialize, Serialize};
25use serde_json::{json, Value};
26
27use fakecloud_core::multi_account::{AccountState, MultiAccountState};
28
29use crate::shared::now_epoch;
30
31pub const COMPREHEND_SNAPSHOT_SCHEMA_VERSION: u32 = 1;
32
33#[derive(Debug, Clone, Default, Serialize, Deserialize)]
35pub struct ComprehendData {
36 #[serde(default)]
38 pub account_id: String,
39 #[serde(default)]
41 pub region: String,
42
43 #[serde(default)]
46 pub jobs: BTreeMap<String, Value>,
47 #[serde(default)]
50 pub job_families: BTreeMap<String, String>,
51
52 #[serde(default)]
54 pub document_classifiers: BTreeMap<String, Value>,
55 #[serde(default)]
57 pub entity_recognizers: BTreeMap<String, Value>,
58 #[serde(default)]
60 pub endpoints: BTreeMap<String, Value>,
61 #[serde(default)]
63 pub flywheels: BTreeMap<String, Value>,
64 #[serde(default)]
66 pub datasets: BTreeMap<String, Value>,
67 #[serde(default)]
70 pub flywheel_iterations: BTreeMap<String, Value>,
71
72 #[serde(default)]
76 pub resource_policies: BTreeMap<String, Value>,
77
78 #[serde(default)]
80 pub tags: BTreeMap<String, BTreeMap<String, String>>,
81}
82
83impl AccountState for ComprehendData {
84 fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
85 Self {
86 account_id: account_id.to_string(),
87 region: region.to_string(),
88 ..Default::default()
89 }
90 }
91}
92
93pub type SharedComprehendState = Arc<RwLock<MultiAccountState<ComprehendData>>>;
94
95#[derive(Debug, Serialize, Deserialize)]
96pub struct ComprehendSnapshot {
97 pub schema_version: u32,
98 pub accounts: MultiAccountState<ComprehendData>,
99}
100
101impl ComprehendData {
102 pub fn reconcile(&mut self) -> bool {
106 let now = now_epoch();
107 let mut changed = false;
108
109 for job in self.jobs.values_mut() {
111 if settle_status(
112 job,
113 "JobStatus",
114 &["SUBMITTED", "IN_PROGRESS"],
115 "COMPLETED",
116 &[("EndTime", json!(now))],
117 ) || settle_status(
118 job,
119 "JobStatus",
120 &["STOP_REQUESTED"],
121 "STOPPED",
122 &[("EndTime", json!(now))],
123 ) {
124 changed = true;
125 }
126 }
127
128 for model in self
130 .document_classifiers
131 .values_mut()
132 .chain(self.entity_recognizers.values_mut())
133 {
134 if settle_status(
135 model,
136 "Status",
137 &["SUBMITTED", "TRAINING"],
138 "TRAINED",
139 &[
140 ("TrainingStartTime", json!(now)),
141 ("TrainingEndTime", json!(now)),
142 ("EndTime", json!(now)),
143 ],
144 ) || settle_status(
145 model,
146 "Status",
147 &["STOP_REQUESTED"],
148 "STOPPED",
149 &[("EndTime", json!(now))],
150 ) {
151 changed = true;
152 }
153 }
154
155 for ep in self.endpoints.values_mut() {
157 let desired = ep.get("DesiredInferenceUnits").cloned().unwrap_or(json!(1));
158 if settle_status(
159 ep,
160 "Status",
161 &["CREATING", "UPDATING"],
162 "IN_SERVICE",
163 &[
164 ("CurrentInferenceUnits", desired),
165 ("LastModifiedTime", json!(now)),
166 ],
167 ) {
168 changed = true;
169 }
170 }
171
172 for fw in self.flywheels.values_mut() {
174 if settle_status(
175 fw,
176 "Status",
177 &["CREATING", "UPDATING"],
178 "ACTIVE",
179 &[("LastModifiedTime", json!(now))],
180 ) {
181 changed = true;
182 }
183 }
184
185 for ds in self.datasets.values_mut() {
187 if settle_status(
188 ds,
189 "Status",
190 &["CREATING"],
191 "COMPLETED",
192 &[("EndTime", json!(now)), ("NumberOfDocuments", json!(0))],
193 ) {
194 changed = true;
195 }
196 }
197
198 for it in self.flywheel_iterations.values_mut() {
200 if settle_status(
201 it,
202 "Status",
203 &["TRAINING", "EVALUATING"],
204 "COMPLETED",
205 &[("EndTime", json!(now))],
206 ) || settle_status(
207 it,
208 "Status",
209 &["STOP_REQUESTED"],
210 "STOPPED",
211 &[("EndTime", json!(now))],
212 ) {
213 changed = true;
214 }
215 }
216
217 changed
218 }
219}
220
221fn settle_status(
224 res: &mut Value,
225 status_field: &str,
226 from: &[&str],
227 to: &str,
228 stamp: &[(&str, Value)],
229) -> bool {
230 let Some(obj) = res.as_object_mut() else {
231 return false;
232 };
233 let cur = obj.get(status_field).and_then(Value::as_str).unwrap_or("");
234 if !from.contains(&cur) {
235 return false;
236 }
237 obj.insert(status_field.to_string(), json!(to));
238 for (k, v) in stamp {
239 obj.insert((*k).to_string(), v.clone());
240 }
241 true
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 fn data() -> ComprehendData {
249 ComprehendData::new_for_account("000000000000", "us-east-1", "")
250 }
251
252 #[test]
253 fn job_settles_to_completed() {
254 let mut d = data();
255 d.jobs.insert(
256 "j1".into(),
257 json!({ "JobId": "j1", "JobStatus": "SUBMITTED" }),
258 );
259 d.job_families.insert("j1".into(), "sentiment".into());
260 assert!(d.reconcile());
261 assert_eq!(d.jobs["j1"]["JobStatus"], "COMPLETED");
262 assert!(d.jobs["j1"].get("EndTime").is_some());
263 assert!(!d.reconcile());
264 }
265
266 #[test]
267 fn stop_requested_job_settles_to_stopped() {
268 let mut d = data();
269 d.jobs.insert(
270 "j1".into(),
271 json!({ "JobId": "j1", "JobStatus": "STOP_REQUESTED" }),
272 );
273 assert!(d.reconcile());
274 assert_eq!(d.jobs["j1"]["JobStatus"], "STOPPED");
275 }
276
277 #[test]
278 fn classifier_settles_to_trained() {
279 let mut d = data();
280 d.document_classifiers
281 .insert("arn".into(), json!({ "Status": "SUBMITTED" }));
282 assert!(d.reconcile());
283 assert_eq!(d.document_classifiers["arn"]["Status"], "TRAINED");
284 }
285
286 #[test]
287 fn endpoint_settles_in_service() {
288 let mut d = data();
289 d.endpoints.insert(
290 "arn".into(),
291 json!({ "Status": "CREATING", "DesiredInferenceUnits": 2 }),
292 );
293 assert!(d.reconcile());
294 assert_eq!(d.endpoints["arn"]["Status"], "IN_SERVICE");
295 assert_eq!(d.endpoints["arn"]["CurrentInferenceUnits"], 2);
296 }
297
298 #[test]
299 fn flywheel_and_dataset_settle() {
300 let mut d = data();
301 d.flywheels
302 .insert("arn".into(), json!({ "Status": "CREATING" }));
303 d.datasets
304 .insert("arn".into(), json!({ "Status": "CREATING" }));
305 assert!(d.reconcile());
306 assert_eq!(d.flywheels["arn"]["Status"], "ACTIVE");
307 assert_eq!(d.datasets["arn"]["Status"], "COMPLETED");
308 }
309}