Publish Clusterflux 45bbc21
This commit is contained in:
parent
eb1077f380
commit
3f88254c70
28 changed files with 896 additions and 167 deletions
|
|
@ -195,12 +195,14 @@ fn coordinator_run_report(plan: RunPlan) -> Result<Value> {
|
|||
);
|
||||
}
|
||||
let process = "vp-current".to_owned();
|
||||
let launch_attempt = new_launch_attempt_id();
|
||||
let mut session = JsonLineSession::connect(&coordinator)?;
|
||||
let mut request = json!({
|
||||
"type": "start_process",
|
||||
"tenant": tenant.clone(),
|
||||
"project": project.clone(),
|
||||
"process": process.clone(),
|
||||
"launch_attempt": launch_attempt.clone(),
|
||||
"restart": false,
|
||||
});
|
||||
request = authenticated_human_or_local_trusted_workflow(
|
||||
|
|
@ -219,6 +221,7 @@ fn coordinator_run_report(plan: RunPlan) -> Result<Value> {
|
|||
&tenant,
|
||||
&project,
|
||||
&process,
|
||||
&launch_attempt,
|
||||
)?;
|
||||
let run_start = run_start_summary(&response);
|
||||
let status = run_start
|
||||
|
|
@ -286,6 +289,7 @@ fn coordinator_run_report(plan: RunPlan) -> Result<Value> {
|
|||
tenant: &tenant,
|
||||
project: &project,
|
||||
process: &process,
|
||||
launch_attempt: &launch_attempt,
|
||||
};
|
||||
let cleanup = rollback_failed_process_launch_reconnecting(
|
||||
&coordinator,
|
||||
|
|
@ -340,6 +344,7 @@ fn request_process_start_with_rollback(
|
|||
tenant: &str,
|
||||
project: &str,
|
||||
process: &str,
|
||||
launch_attempt: &str,
|
||||
) -> Result<Value> {
|
||||
let rollback = ProcessLaunchRollback {
|
||||
cli_session,
|
||||
|
|
@ -348,9 +353,25 @@ fn request_process_start_with_rollback(
|
|||
tenant,
|
||||
project,
|
||||
process,
|
||||
launch_attempt,
|
||||
};
|
||||
match session.request_allow_error(request) {
|
||||
Ok(response) => Ok(response),
|
||||
Ok(response) => {
|
||||
if response.get("type").and_then(Value::as_str) == Some("process_started")
|
||||
&& response.get("launch_attempt").and_then(Value::as_str) != Some(launch_attempt)
|
||||
{
|
||||
let ownership_error = anyhow::anyhow!(
|
||||
"coordinator returned a process-start acknowledgement owned by a different launch attempt"
|
||||
);
|
||||
rollback_failed_process_launch_reconnecting(
|
||||
coordinator,
|
||||
&rollback,
|
||||
&json!({ "error": ownership_error.to_string(), "phase": "start_process_ownership" }),
|
||||
)?;
|
||||
return Err(ownership_error);
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
Err(start_error) => {
|
||||
let cleanup = rollback_failed_process_launch_reconnecting(
|
||||
coordinator,
|
||||
|
|
@ -370,6 +391,17 @@ fn request_process_start_with_rollback(
|
|||
}
|
||||
}
|
||||
|
||||
static NEXT_LAUNCH_ATTEMPT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
|
||||
fn new_launch_attempt_id() -> String {
|
||||
let sequence = NEXT_LAUNCH_ATTEMPT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos();
|
||||
format!("launch-{}-{nanos}-{sequence}", std::process::id())
|
||||
}
|
||||
|
||||
struct ProcessLaunchRollback<'a> {
|
||||
cli_session: &'a CliSession,
|
||||
fallback_user: &'a str,
|
||||
|
|
@ -377,6 +409,7 @@ struct ProcessLaunchRollback<'a> {
|
|||
tenant: &'a str,
|
||||
project: &'a str,
|
||||
process: &'a str,
|
||||
launch_attempt: &'a str,
|
||||
}
|
||||
|
||||
fn validate_inline_bundle_size(module_size_bytes: usize) -> Result<()> {
|
||||
|
|
@ -402,6 +435,7 @@ fn rollback_failed_process_launch(
|
|||
"tenant": context.tenant,
|
||||
"project": context.project,
|
||||
"process": context.process,
|
||||
"launch_attempt": context.launch_attempt,
|
||||
}),
|
||||
context.cli_session,
|
||||
context.fallback_user,
|
||||
|
|
@ -983,6 +1017,10 @@ mod transactional_launch_tests {
|
|||
payload.get("process").and_then(Value::as_str),
|
||||
Some("vp-current")
|
||||
);
|
||||
assert_eq!(
|
||||
payload.get("launch_attempt").and_then(Value::as_str),
|
||||
Some("launch-test")
|
||||
);
|
||||
writeln!(
|
||||
stream,
|
||||
"{}",
|
||||
|
|
@ -1003,6 +1041,7 @@ mod transactional_launch_tests {
|
|||
tenant: "tenant-a",
|
||||
project: "project-a",
|
||||
process: "vp-current",
|
||||
launch_attempt: "launch-test",
|
||||
};
|
||||
rollback_failed_process_launch(
|
||||
&mut session,
|
||||
|
|
@ -1024,6 +1063,7 @@ mod transactional_launch_tests {
|
|||
.read_line(&mut start_line)
|
||||
.unwrap();
|
||||
assert!(start_line.contains("\"type\":\"start_process\""));
|
||||
assert!(start_line.contains("\"launch_attempt\":\"launch-test\""));
|
||||
drop(stream);
|
||||
|
||||
let (mut cleanup_stream, _) = listener.accept().unwrap();
|
||||
|
|
@ -1032,6 +1072,7 @@ mod transactional_launch_tests {
|
|||
.read_line(&mut cleanup_line)
|
||||
.unwrap();
|
||||
assert!(cleanup_line.contains("\"type\":\"abort_process\""));
|
||||
assert!(cleanup_line.contains("\"launch_attempt\":\"launch-test\""));
|
||||
writeln!(
|
||||
cleanup_stream,
|
||||
"{}",
|
||||
|
|
@ -1043,6 +1084,12 @@ mod transactional_launch_tests {
|
|||
})
|
||||
)
|
||||
.unwrap();
|
||||
listener.set_nonblocking(true).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(25));
|
||||
assert!(matches!(
|
||||
listener.accept().unwrap_err().kind(),
|
||||
std::io::ErrorKind::WouldBlock
|
||||
));
|
||||
});
|
||||
let mut session = JsonLineSession::connect(&address).unwrap();
|
||||
let error = request_process_start_with_rollback(
|
||||
|
|
@ -1053,6 +1100,7 @@ mod transactional_launch_tests {
|
|||
"project": "project-a",
|
||||
"actor_user": "user-a",
|
||||
"process": "vp-current",
|
||||
"launch_attempt": "launch-test",
|
||||
"restart": false
|
||||
}),
|
||||
&address,
|
||||
|
|
@ -1062,10 +1110,63 @@ mod transactional_launch_tests {
|
|||
"tenant-a",
|
||||
"project-a",
|
||||
"vp-current",
|
||||
"launch-test",
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(error.contains("closed") || error.contains("response"));
|
||||
server.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn definitive_start_rejection_does_not_abort_existing_process() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let address = listener.local_addr().unwrap().to_string();
|
||||
let server = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().unwrap();
|
||||
let mut line = String::new();
|
||||
std::io::BufReader::new(stream.try_clone().unwrap())
|
||||
.read_line(&mut line)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
stream,
|
||||
"{}",
|
||||
json!({
|
||||
"type": "error",
|
||||
"message": "project already has active virtual process vp-existing"
|
||||
})
|
||||
)
|
||||
.unwrap();
|
||||
listener.set_nonblocking(true).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(25));
|
||||
assert_eq!(
|
||||
listener.accept().unwrap_err().kind(),
|
||||
std::io::ErrorKind::WouldBlock
|
||||
);
|
||||
});
|
||||
let mut session = JsonLineSession::connect(&address).unwrap();
|
||||
let response = request_process_start_with_rollback(
|
||||
&mut session,
|
||||
json!({
|
||||
"type": "start_process",
|
||||
"tenant": "tenant-a",
|
||||
"project": "project-a",
|
||||
"actor_user": "user-a",
|
||||
"process": "vp-current",
|
||||
"launch_attempt": "launch-rejected",
|
||||
"restart": false
|
||||
}),
|
||||
&address,
|
||||
&CliSession::Anonymous,
|
||||
"user-a",
|
||||
None,
|
||||
"tenant-a",
|
||||
"project-a",
|
||||
"vp-current",
|
||||
"launch-rejected",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(response.get("type").and_then(Value::as_str), Some("error"));
|
||||
server.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,15 @@ fn parse(args: &[&str]) -> Cli {
|
|||
Cli::parse_from(args)
|
||||
}
|
||||
|
||||
fn launch_attempt_from_wire(line: &str) -> String {
|
||||
let wire: Value = serde_json::from_str(line).unwrap();
|
||||
wire.pointer("/payload/request/launch_attempt")
|
||||
.or_else(|| wire.pointer("/payload/launch_attempt"))
|
||||
.and_then(Value::as_str)
|
||||
.expect("start request must carry a launch attempt")
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn write_runnable_wasm_project(project: &Path) {
|
||||
let cli_manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let sdk = cli_manifest_dir.parent().unwrap().join("clusterflux-sdk");
|
||||
|
|
@ -438,10 +447,11 @@ fn run_with_agent_public_key_sends_attributable_workflow_actor() {
|
|||
assert!(line.contains(r#""nonce":"agent-signature-"#));
|
||||
assert!(line.contains(r#""signature":"ed25519:"#));
|
||||
assert!(!line.contains(r#""actor_user""#));
|
||||
let launch_attempt = launch_attempt_from_wire(&line);
|
||||
stream
|
||||
.write_all(
|
||||
format!(
|
||||
r#"{{"type":"process_started","process":"vp-current","epoch":7,"actor":{{"kind":"agent","user":"user-live","agent":"agent-ci","credential_kind":"public_key","public_key_fingerprint":"{expected_fingerprint}","authenticated_without_browser":true,"scopes":["project:read","project:run"]}}}}"#
|
||||
r#"{{"type":"process_started","process":"vp-current","launch_attempt":"{launch_attempt}","epoch":7,"actor":{{"kind":"agent","user":"user-live","agent":"agent-ci","credential_kind":"public_key","public_key_fingerprint":"{expected_fingerprint}","authenticated_without_browser":true,"scopes":["project:read","project:run"]}}}}"#
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
|
|
@ -588,7 +598,32 @@ fn run_with_human_session_derives_workflow_scope_from_authenticated_envelope() {
|
|||
assert!(wire["payload"]["request"].get("tenant").is_none());
|
||||
assert!(wire["payload"]["request"].get("project").is_none());
|
||||
assert!(wire["payload"]["request"].get("actor_user").is_none());
|
||||
stream.write_all(response).unwrap();
|
||||
if expected_operation == "start_process" {
|
||||
let launch_attempt = launch_attempt_from_wire(&line);
|
||||
stream
|
||||
.write_all(
|
||||
serde_json::to_string(&json!({
|
||||
"type": "process_started",
|
||||
"process": "vp-current",
|
||||
"launch_attempt": launch_attempt,
|
||||
"epoch": 7,
|
||||
"actor": {
|
||||
"kind": "user",
|
||||
"user": "user-session",
|
||||
"agent": null,
|
||||
"credential_kind": "cli_device_session",
|
||||
"public_key_fingerprint": null,
|
||||
"authenticated_without_browser": false,
|
||||
"scopes": ["project:read", "project:run"]
|
||||
}
|
||||
}))
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
stream.write_all(response).unwrap();
|
||||
}
|
||||
stream.write_all(b"\n").unwrap();
|
||||
}
|
||||
});
|
||||
|
|
@ -661,6 +696,18 @@ fn run_contacts_configured_coordinator_and_reports_active_process_conflicts() {
|
|||
assert!(line.contains(r#""project":"project-live""#));
|
||||
assert!(line.contains(r#""process":"vp-current""#));
|
||||
assert!(line.contains(r#""restart":false"#));
|
||||
let response = if index == 0 {
|
||||
let launch_attempt = launch_attempt_from_wire(&line);
|
||||
serde_json::to_string(&json!({
|
||||
"type": "process_started",
|
||||
"process": "vp-current",
|
||||
"launch_attempt": launch_attempt,
|
||||
"epoch": 7,
|
||||
}))
|
||||
.unwrap()
|
||||
} else {
|
||||
response.to_owned()
|
||||
};
|
||||
stream.write_all(response.as_bytes()).unwrap();
|
||||
stream.write_all(b"\n").unwrap();
|
||||
if index == 0 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue