review classifier: recover from a stored record it can't decode
The controller keeps the classifier's newest runs as one JSON array and its settings as one JSON object, each under one KV key. A document this build could not decode, as after a rollback past a build that added an outcome or a settings field, failed every read and every write until someone deleted the key by hand. A classifier newer than its controller refused every run request for the missing `model`. The run store now decodes the array one record at a time: a record that does not decode is dropped with a WARN naming the bucket, key, index and error, and a document that is not an array reads as no runs. Listing keeps working, and the next recorded run writes back only the records that decoded. The stored settings ignore a field this build does not know; one that still does not decode reads as the defaults with a WARN. The PUT body keeps `deny_unknown_fields`. A PUT now always writes, so a PUT of the defaults also replaces an undecodable document. A run request without `model` runs on haiku, the swarm-wide setting's default, now one constant in swarm-queue-client. `schedules::read` gains a sibling, `read_with`, taking the decoder. Closes #5162
This commit is contained in:
parent
30d9e1d8e6
commit
6f0a72b246
5 changed files with 208 additions and 17 deletions
|
|
@ -33,6 +33,14 @@ pub const CLASSIFICATION_DEADLINE: Duration = Duration::from_mins(3);
|
|||
/// this long.
|
||||
pub const RUN_DEADLINE: Duration = Duration::from_mins(31);
|
||||
|
||||
/// The model a run uses when its request names none, and the swarm-wide
|
||||
/// setting's default.
|
||||
pub const DEFAULT_MODEL: &str = "haiku";
|
||||
|
||||
fn default_model() -> String {
|
||||
DEFAULT_MODEL.to_owned()
|
||||
}
|
||||
|
||||
/// One run: the PR as the controller read it, and the classifications to
|
||||
/// answer about it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
@ -51,6 +59,9 @@ pub struct TryOut {
|
|||
pub diff: String,
|
||||
pub classifications: Vec<Classification>,
|
||||
/// The model every classification runs on, as claude's `--model` takes it.
|
||||
/// Absent is [`DEFAULT_MODEL`], so a controller that sends none still gets
|
||||
/// its run answered.
|
||||
#[serde(default = "default_model")]
|
||||
pub model: String,
|
||||
/// The effort every classification runs at; `None` passes no `--effort`.
|
||||
pub effort: Option<Effort>,
|
||||
|
|
@ -176,4 +187,27 @@ mod tests {
|
|||
let all = CLASSIFICATION_DEADLINE * u32::try_from(MAX_CLASSIFICATIONS).expect("small");
|
||||
assert!(RUN_DEADLINE > all);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_request_without_a_model_runs_on_the_default() {
|
||||
let mut request = serde_json::json!({
|
||||
"operator": "alice",
|
||||
"repo": "o/r",
|
||||
"pr": 7,
|
||||
"head_sha": "abc123",
|
||||
"commits": [],
|
||||
"diff": "",
|
||||
"classifications": [],
|
||||
});
|
||||
let run: TryOut = serde_json::from_value(request.clone()).expect("decodes");
|
||||
assert_eq!((run.model.as_str(), run.effort), (DEFAULT_MODEL, None));
|
||||
// Control: a named model and effort are the ones used.
|
||||
request["model"] = "sonnet".into();
|
||||
request["effort"] = "high".into();
|
||||
let run: TryOut = serde_json::from_value(request).expect("decodes");
|
||||
assert_eq!(
|
||||
(run.model.as_str(), run.effort),
|
||||
("sonnet", Some(Effort::High))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue