Publish Clusterflux release candidate 6e83783

This commit is contained in:
Clusterflux release 2026-07-20 19:32:05 +02:00
parent 4d75257c6e
commit f3640643bd
16 changed files with 4667 additions and 106 deletions

View file

@ -55,6 +55,7 @@ enum AdapterEvent {
},
BreakpointsUpdated {
generation: u64,
revision: u64,
result: std::result::Result<(), String>,
},
}
@ -62,6 +63,7 @@ enum AdapterEvent {
struct LaunchCompletion {
state: AdapterState,
local_runtime_session: Option<LocalRuntimeSession>,
breakpoint_revision: u64,
}
pub(crate) fn run_adapter() -> Result<()> {
@ -93,8 +95,13 @@ pub(crate) fn run_adapter() -> Result<()> {
runtime_starting = false;
match result {
Ok(mut completion) => {
completion.state.breakpoints = state.breakpoints.clone();
completion.state.breakpoints_installed = true;
let requested_breakpoints = state.breakpoints.clone();
let requested_revision = state.breakpoint_revision;
let installed_revision = completion.breakpoint_revision;
completion.state.breakpoints = requested_breakpoints;
completion.state.breakpoint_revision = requested_revision;
completion.state.breakpoints_installed =
installed_revision == requested_revision;
let previous_threads = state.threads.clone();
state = completion.state;
emit_thread_lifecycle(&mut writer, &previous_threads, &state.threads)?;
@ -102,7 +109,24 @@ pub(crate) fn run_adapter() -> Result<()> {
_local_runtime_session = Some(session);
}
runtime_started = true;
emit_verified_breakpoints(&mut writer, &state)?;
if state.breakpoints_installed {
emit_verified_breakpoints(&mut writer, &state)?;
} else {
let breakpoint_state = state.clone();
let breakpoint_tx = event_tx.clone();
let revision = state.breakpoint_revision;
thread::spawn(move || {
let result = set_services_debug_breakpoints(&breakpoint_state)
.map_err(|error| {
format!("coordinator breakpoint update failed: {error:#}")
});
let _ = breakpoint_tx.send(AdapterEvent::BreakpointsUpdated {
generation,
revision,
result,
});
});
}
writer.output(
"console",
if state.session_mode == DapSessionMode::Attach {
@ -127,15 +151,7 @@ pub(crate) fn run_adapter() -> Result<()> {
Err(message) => {
runtime_started = false;
writer.output("stderr", format!("{message}\n"))?;
writer.event(
"stopped",
json!({
"reason": "exception",
"description": message,
"threadId": MAIN_THREAD,
"allThreadsStopped": false,
}),
)?;
writer.event("terminated", json!({ "restart": false }))?;
}
}
continue;
@ -231,8 +247,12 @@ pub(crate) fn run_adapter() -> Result<()> {
}
continue;
}
AdapterEvent::BreakpointsUpdated { generation, result } => {
if generation == runtime_generation {
AdapterEvent::BreakpointsUpdated {
generation,
revision,
result,
} => {
if generation == runtime_generation && revision == state.breakpoint_revision {
match result {
Ok(()) => {
state.breakpoints_installed = true;
@ -306,7 +326,16 @@ pub(crate) fn run_adapter() -> Result<()> {
.and_then(Value::as_bool)
.unwrap_or(false);
if let Some(process_id) = args.get("processId").and_then(Value::as_str) {
state.process = clusterflux_core::ProcessId::new(process_id);
match clusterflux_core::ProcessId::try_new(process_id.to_owned()) {
Ok(process) => state.process = process,
Err(error) => {
writer.error_response(
&request,
format!("invalid DAP processId: {error}"),
)?;
continue;
}
}
}
if command == "attach" {
state.session_mode = DapSessionMode::Attach;
@ -338,6 +367,8 @@ pub(crate) fn run_adapter() -> Result<()> {
requested_source_path,
requested_lines,
);
state.breakpoint_revision = state.breakpoint_revision.saturating_add(1);
let breakpoint_revision = state.breakpoint_revision;
let coordinator_backed = matches!(
state.runtime_backend,
RuntimeBackend::LocalServices | RuntimeBackend::LiveServices
@ -371,8 +402,11 @@ pub(crate) fn run_adapter() -> Result<()> {
set_services_debug_breakpoints(&breakpoint_state).map_err(|error| {
format!("coordinator breakpoint update failed: {error:#}")
});
let _ = breakpoint_tx
.send(AdapterEvent::BreakpointsUpdated { generation, result });
let _ = breakpoint_tx.send(AdapterEvent::BreakpointsUpdated {
generation,
revision: breakpoint_revision,
result,
});
});
}
}
@ -433,11 +467,17 @@ pub(crate) fn run_adapter() -> Result<()> {
let mut completed_state = launch_state;
let result = if completed_state.session_mode == DapSessionMode::Attach {
attach_services_runtime(&completed_state)
.map(|record| {
.and_then(|record| {
completed_state.apply_attach_record(record);
set_services_debug_breakpoints(&completed_state)?;
Ok(())
})
.map(|()| {
let breakpoint_revision = completed_state.breakpoint_revision;
LaunchCompletion {
state: completed_state,
local_runtime_session: None,
breakpoint_revision,
}
})
.map_err(|error| format!("services runtime attach failed: {error:#}"))
@ -447,9 +487,12 @@ pub(crate) fn run_adapter() -> Result<()> {
run_local_services_runtime(&completed_state)
.map(|(record, session)| {
completed_state.apply_runtime_record(record);
let breakpoint_revision =
completed_state.breakpoint_revision;
LaunchCompletion {
state: completed_state,
local_runtime_session: Some(session),
breakpoint_revision,
}
})
.map_err(|error| {
@ -460,9 +503,12 @@ pub(crate) fn run_adapter() -> Result<()> {
run_live_services_runtime(&completed_state)
.map(|record| {
completed_state.apply_runtime_record(record);
let breakpoint_revision =
completed_state.breakpoint_revision;
LaunchCompletion {
state: completed_state,
local_runtime_session: None,
breakpoint_revision,
}
})
.map_err(|error| {

View file

@ -422,8 +422,31 @@ fn launch_services_debug_entrypoint(
}
// The launch acknowledgement is the commit point. Failures while fetching
// observation state after this line must not abort a process that is running.
let task_snapshots = fetch_task_snapshots(coordinator, state)?;
let (process_statuses, process_status) = fetch_current_process_status(coordinator, state)?;
let mut observation_diagnostics = Vec::new();
let inject_post_commit_observation_failure =
std::env::var_os("CLUSTERFLUX_TEST_DAP_POST_COMMIT_OBSERVATION_FAILURE").is_some();
let task_snapshots = (if inject_post_commit_observation_failure {
Err(anyhow!("injected post-commit task observation failure"))
} else {
fetch_task_snapshots(coordinator, state)
})
.unwrap_or_else(|error| {
observation_diagnostics.push(format!(
"initial task observation failed after main_launched: {error:#}"
));
json!({ "snapshots": [] })
});
let (process_statuses, process_status) = (if inject_post_commit_observation_failure {
Err(anyhow!("injected post-commit process observation failure"))
} else {
fetch_current_process_status(coordinator, state)
})
.unwrap_or_else(|error| {
observation_diagnostics.push(format!(
"initial process observation failed after main_launched: {error:#}"
));
(json!({ "processes": [] }), None)
});
let node = process_status
.as_ref()
.and_then(|status| status.get("connected_nodes"))
@ -441,6 +464,7 @@ fn launch_services_debug_entrypoint(
"process_status": process_status,
"process_statuses": process_statuses,
"task_snapshots": task_snapshots,
"observation_diagnostics": observation_diagnostics,
}),
task_events: json!({ "events": [] }),
placed_task_launched: true,
@ -1002,6 +1026,12 @@ pub(crate) fn observe_services_runtime(
let inject_connection_loss =
std::env::var("CLUSTERFLUX_TEST_DAP_OBSERVER_CONNECTION_LOSS").as_deref() == Ok("1");
let mut connection_loss_injected = false;
let inject_fallback_failure =
std::env::var_os("CLUSTERFLUX_TEST_DAP_OBSERVER_FALLBACK_FAILURE").is_some();
let mut fallback_failure_stage = 0_u8;
let inject_debug_epoch_wait_failure =
std::env::var_os("CLUSTERFLUX_TEST_DAP_OBSERVER_DEBUG_EPOCH_WAIT_FAILURE").is_some();
let mut debug_epoch_wait_failure_injected = false;
loop {
if cancelled.load(Ordering::Acquire) {
return Ok(());
@ -1191,16 +1221,22 @@ pub(crate) fn observe_services_runtime(
return Ok(());
}
let breakpoint = match current_session.request(client_user_request(
state,
json!({
"type": "inspect_debug_breakpoints",
"tenant": state.tenant,
"project": state.project_id,
"actor_user": state.actor_user,
"process": state.process.as_str(),
}),
)) {
let breakpoint_request = if inject_fallback_failure && fallback_failure_stage == 0 {
fallback_failure_stage = 1;
Err(anyhow!("injected breakpoint inspection transport failure"))
} else {
current_session.request(client_user_request(
state,
json!({
"type": "inspect_debug_breakpoints",
"tenant": state.tenant,
"project": state.project_id,
"actor_user": state.actor_user,
"process": state.process.as_str(),
}),
))
};
let breakpoint = match breakpoint_request {
Ok(breakpoint) => breakpoint,
Err(inspect_error) => {
// Main completion records its terminal event and clears ephemeral
@ -1208,16 +1244,36 @@ pub(crate) fn observe_services_runtime(
// the event read above and this inspection request. Re-read the
// durable event stream before treating the missing debug state as
// an adapter failure.
let events = current_session.request(client_user_request(
state,
json!({
"type": "list_task_events",
"tenant": state.tenant,
"project": state.project_id,
"actor_user": state.actor_user,
"process": state.process.as_str(),
}),
))?;
let fallback_request = if inject_fallback_failure && fallback_failure_stage == 1 {
fallback_failure_stage = 2;
Err(anyhow!("injected fallback event transport failure"))
} else {
current_session.request(client_user_request(
state,
json!({
"type": "list_task_events",
"tenant": state.tenant,
"project": state.project_id,
"actor_user": state.actor_user,
"process": state.process.as_str(),
}),
))
};
let events = match fallback_request {
Ok(events) => events,
Err(fallback_error) => {
if !emit(RuntimeContinuationOutcome::Diagnostic(format!(
"runtime breakpoint and fallback event observation failed: {inspect_error:#}; {fallback_error:#}; reconnecting in {} ms",
reconnect_delay.as_millis()
))) {
return Ok(());
}
session = None;
std::thread::sleep(reconnect_delay);
reconnect_delay = (reconnect_delay * 2).min(Duration::from_secs(5));
continue;
}
};
if let Some(outcome) = terminal_runtime_outcome(&coordinator, state, &events) {
emit(outcome);
return Ok(());
@ -1236,7 +1292,28 @@ pub(crate) fn observe_services_runtime(
};
if let Some(epoch) = breakpoint.get("hit_epoch").and_then(Value::as_u64) {
if epoch > previous_debug_epoch {
let frozen = wait_for_debug_epoch_state_at(&coordinator, state, epoch, true)?;
let frozen_request =
if inject_debug_epoch_wait_failure && !debug_epoch_wait_failure_injected {
debug_epoch_wait_failure_injected = true;
Err(anyhow!("injected Debug Epoch wait transport failure"))
} else {
wait_for_debug_epoch_state_at(&coordinator, state, epoch, true)
};
let frozen = match frozen_request {
Ok(frozen) => frozen,
Err(error) => {
if !emit(RuntimeContinuationOutcome::Diagnostic(format!(
"runtime Debug Epoch observation failed: {error:#}; reconnecting in {} ms",
reconnect_delay.as_millis()
))) {
return Ok(());
}
session = None;
std::thread::sleep(reconnect_delay);
reconnect_delay = (reconnect_delay * 2).min(Duration::from_secs(5));
continue;
}
};
let node = frozen
.get("acknowledgements")
.and_then(Value::as_array)

View file

@ -75,6 +75,7 @@ pub(crate) struct AdapterState {
pub(crate) debug_probes: Vec<BundleDebugProbe>,
pub(crate) breakpoints: Vec<i64>,
pub(crate) breakpoints_installed: bool,
pub(crate) breakpoint_revision: u64,
pub(crate) threads: BTreeMap<i64, VirtualThread>,
}
@ -133,6 +134,7 @@ impl Default for AdapterState {
debug_probes: Vec::new(),
breakpoints: Vec::new(),
breakpoints_installed: false,
breakpoint_revision: 0,
}
}
}
@ -198,18 +200,24 @@ impl AdapterState {
}
self.coordinator_endpoint =
crate::view_state::normalize_coordinator_endpoint(&session.coordinator);
self.tenant = TenantId::new(session.tenant);
self.project_id = ProjectId::new(session.project);
self.actor_user = UserId::new(session.user);
self.tenant = TenantId::try_new(session.tenant)
.map_err(|error| anyhow!("invalid tenant in CLI session: {error}"))?;
self.project_id = ProjectId::try_new(session.project)
.map_err(|error| anyhow!("invalid project in CLI session: {error}"))?;
self.actor_user = UserId::try_new(session.user)
.map_err(|error| anyhow!("invalid user in CLI session: {error}"))?;
self.client_session_secret = Some(session.session_secret);
} else {
self.coordinator_endpoint = crate::view_state::normalize_coordinator_endpoint(
coordinator_endpoint.as_deref().unwrap_or("127.0.0.1:0"),
);
if let Some(scope) = project_scope {
self.tenant = TenantId::new(scope.tenant);
self.project_id = ProjectId::new(scope.project);
self.actor_user = UserId::new(scope.user);
self.tenant = TenantId::try_new(scope.tenant)
.map_err(|error| anyhow!("invalid tenant in project scope: {error}"))?;
self.project_id = ProjectId::try_new(scope.project)
.map_err(|error| anyhow!("invalid project in project scope: {error}"))?;
self.actor_user = UserId::try_new(scope.user)
.map_err(|error| anyhow!("invalid user in project scope: {error}"))?;
}
self.client_session_secret = None;
}
@ -230,6 +238,7 @@ impl AdapterState {
self.epoch = 0;
self.breakpoints.clear();
self.breakpoints_installed = self.runtime_backend == RuntimeBackend::Simulated;
self.breakpoint_revision = 0;
self.threads = if self.runtime_backend == RuntimeBackend::Simulated {
launch_threads(&self.entry)
} else {