use std::collections::BTreeMap;
use std::sync::Arc;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use fakecloud_core::multi_account::{AccountState, MultiAccountState};
use crate::shared::now_epoch;
pub const COMPREHEND_SNAPSHOT_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ComprehendData {
#[serde(default)]
pub account_id: String,
#[serde(default)]
pub region: String,
#[serde(default)]
pub jobs: BTreeMap<String, Value>,
#[serde(default)]
pub job_families: BTreeMap<String, String>,
#[serde(default)]
pub document_classifiers: BTreeMap<String, Value>,
#[serde(default)]
pub entity_recognizers: BTreeMap<String, Value>,
#[serde(default)]
pub endpoints: BTreeMap<String, Value>,
#[serde(default)]
pub flywheels: BTreeMap<String, Value>,
#[serde(default)]
pub datasets: BTreeMap<String, Value>,
#[serde(default)]
pub flywheel_iterations: BTreeMap<String, Value>,
#[serde(default)]
pub resource_policies: BTreeMap<String, Value>,
#[serde(default)]
pub tags: BTreeMap<String, BTreeMap<String, String>>,
}
impl AccountState for ComprehendData {
fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
Self {
account_id: account_id.to_string(),
region: region.to_string(),
..Default::default()
}
}
}
pub type SharedComprehendState = Arc<RwLock<MultiAccountState<ComprehendData>>>;
#[derive(Debug, Serialize, Deserialize)]
pub struct ComprehendSnapshot {
pub schema_version: u32,
pub accounts: MultiAccountState<ComprehendData>,
}
impl ComprehendData {
pub fn reconcile(&mut self) -> bool {
let now = now_epoch();
let mut changed = false;
for job in self.jobs.values_mut() {
if settle_status(
job,
"JobStatus",
&["SUBMITTED", "IN_PROGRESS"],
"COMPLETED",
&[("EndTime", json!(now))],
) || settle_status(
job,
"JobStatus",
&["STOP_REQUESTED"],
"STOPPED",
&[("EndTime", json!(now))],
) {
changed = true;
}
}
for model in self
.document_classifiers
.values_mut()
.chain(self.entity_recognizers.values_mut())
{
if settle_status(
model,
"Status",
&["SUBMITTED", "TRAINING"],
"TRAINED",
&[
("TrainingStartTime", json!(now)),
("TrainingEndTime", json!(now)),
("EndTime", json!(now)),
],
) || settle_status(
model,
"Status",
&["STOP_REQUESTED"],
"STOPPED",
&[("EndTime", json!(now))],
) {
changed = true;
}
}
for ep in self.endpoints.values_mut() {
let desired = ep.get("DesiredInferenceUnits").cloned().unwrap_or(json!(1));
if settle_status(
ep,
"Status",
&["CREATING", "UPDATING"],
"IN_SERVICE",
&[
("CurrentInferenceUnits", desired),
("LastModifiedTime", json!(now)),
],
) {
changed = true;
}
}
for fw in self.flywheels.values_mut() {
if settle_status(
fw,
"Status",
&["CREATING", "UPDATING"],
"ACTIVE",
&[("LastModifiedTime", json!(now))],
) {
changed = true;
}
}
for ds in self.datasets.values_mut() {
if settle_status(
ds,
"Status",
&["CREATING"],
"COMPLETED",
&[("EndTime", json!(now)), ("NumberOfDocuments", json!(0))],
) {
changed = true;
}
}
for it in self.flywheel_iterations.values_mut() {
if settle_status(
it,
"Status",
&["TRAINING", "EVALUATING"],
"COMPLETED",
&[("EndTime", json!(now))],
) || settle_status(
it,
"Status",
&["STOP_REQUESTED"],
"STOPPED",
&[("EndTime", json!(now))],
) {
changed = true;
}
}
changed
}
}
fn settle_status(
res: &mut Value,
status_field: &str,
from: &[&str],
to: &str,
stamp: &[(&str, Value)],
) -> bool {
let Some(obj) = res.as_object_mut() else {
return false;
};
let cur = obj.get(status_field).and_then(Value::as_str).unwrap_or("");
if !from.contains(&cur) {
return false;
}
obj.insert(status_field.to_string(), json!(to));
for (k, v) in stamp {
obj.insert((*k).to_string(), v.clone());
}
true
}
#[cfg(test)]
mod tests {
use super::*;
fn data() -> ComprehendData {
ComprehendData::new_for_account("000000000000", "us-east-1", "")
}
#[test]
fn job_settles_to_completed() {
let mut d = data();
d.jobs.insert(
"j1".into(),
json!({ "JobId": "j1", "JobStatus": "SUBMITTED" }),
);
d.job_families.insert("j1".into(), "sentiment".into());
assert!(d.reconcile());
assert_eq!(d.jobs["j1"]["JobStatus"], "COMPLETED");
assert!(d.jobs["j1"].get("EndTime").is_some());
assert!(!d.reconcile());
}
#[test]
fn stop_requested_job_settles_to_stopped() {
let mut d = data();
d.jobs.insert(
"j1".into(),
json!({ "JobId": "j1", "JobStatus": "STOP_REQUESTED" }),
);
assert!(d.reconcile());
assert_eq!(d.jobs["j1"]["JobStatus"], "STOPPED");
}
#[test]
fn classifier_settles_to_trained() {
let mut d = data();
d.document_classifiers
.insert("arn".into(), json!({ "Status": "SUBMITTED" }));
assert!(d.reconcile());
assert_eq!(d.document_classifiers["arn"]["Status"], "TRAINED");
}
#[test]
fn endpoint_settles_in_service() {
let mut d = data();
d.endpoints.insert(
"arn".into(),
json!({ "Status": "CREATING", "DesiredInferenceUnits": 2 }),
);
assert!(d.reconcile());
assert_eq!(d.endpoints["arn"]["Status"], "IN_SERVICE");
assert_eq!(d.endpoints["arn"]["CurrentInferenceUnits"], 2);
}
#[test]
fn flywheel_and_dataset_settle() {
let mut d = data();
d.flywheels
.insert("arn".into(), json!({ "Status": "CREATING" }));
d.datasets
.insert("arn".into(), json!({ "Status": "CREATING" }));
assert!(d.reconcile());
assert_eq!(d.flywheels["arn"]["Status"], "ACTIVE");
assert_eq!(d.datasets["arn"]["Status"], "COMPLETED");
}
}