Skip to content

Commit 3892b62

Browse files
authored
fix: reinstall jobs not cleaning up if instance deleted (#6927)
1 parent 5ffffec commit 3892b62

5 files changed

Lines changed: 130 additions & 30 deletions

File tree

packages/app-lib/.sqlx/query-15b4f72d367d329690f5daddafbdf0e51a285e35404053d62492e8df5fc7132f.json

Lines changed: 0 additions & 12 deletions
This file was deleted.

packages/app-lib/src/api/instance/lifecycle.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ pub async fn remove(instance_id: &str) -> crate::Result<()> {
7777
let instance =
7878
instance_rows::get_instance_display_info(instance_id, &state.pool)
7979
.await?;
80+
crate::install::runner::cancel_jobs_for_instance_deletion(
81+
instance_id,
82+
&state,
83+
)
84+
.await?;
8085
crate::state::remove_instance(instance_id, &state).await?;
8186

8287
if let Some(instance) = instance {

packages/app-lib/src/install/recovery.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,38 @@ async fn recover_interrupted_job(
281281
if job.state.display.is_none() {
282282
job.state.display = display_from_request(&job.state);
283283
}
284+
285+
if let Some(instance_id) = target_instance_id(&job.state.target)
286+
&& instance_rows::get_instance_by_id(instance_id, &state.pool)
287+
.await?
288+
.is_none()
289+
{
290+
let canceled_phase = job.state.progress.phase;
291+
job.state.error = Some(InstallErrorView::from_message(
292+
"canceled",
293+
canceled_phase,
294+
"Install canceled because the instance was deleted",
295+
));
296+
job.state.record_event(InstallJobEventKind::JobCanceled {
297+
phase: canceled_phase,
298+
});
299+
300+
if let Some(record) = store::finish_active(
301+
job.id,
302+
InstallJobStatus::Canceled,
303+
&job.state,
304+
state,
305+
)
306+
.await?
307+
{
308+
store::dismiss(job.id, state).await?;
309+
clear_staging_dir(&job.state).await;
310+
emit_install_job(&record.snapshot()).await?;
311+
}
312+
313+
return Ok(());
314+
}
315+
284316
let interrupted_phase = job.state.progress.phase;
285317
job.state.record_event(InstallJobEventKind::Interrupted {
286318
reason: InstallInterruptReason::AppClosed,
@@ -341,6 +373,13 @@ async fn recover_interrupted_job(
341373
Ok(())
342374
}
343375

376+
fn target_instance_id(target: &InstallTarget) -> Option<&str> {
377+
match target {
378+
InstallTarget::NewInstance { instance_id } => instance_id.as_deref(),
379+
InstallTarget::ExistingInstance { instance_id } => Some(instance_id),
380+
}
381+
}
382+
344383
fn clear_deleted_new_instance_id(job_state: &mut InstallJobState) {
345384
if matches!(job_state.cleanup, InstallCleanup::DeleteNewInstance { .. }) {
346385
job_state.target = InstallTarget::NewInstance { instance_id: None };

packages/app-lib/src/install/runner.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,39 @@ pub async fn cancel_job(job_id: Uuid) -> crate::Result<InstallJobSnapshot> {
295295
Ok(record.snapshot())
296296
}
297297

298+
pub(crate) async fn cancel_jobs_for_instance_deletion(
299+
instance_id: &str,
300+
state: &State,
301+
) -> crate::Result<()> {
302+
for mut job in store::list_active_for_instance(instance_id, state).await? {
303+
let canceled_phase = job.state.progress.phase;
304+
job.state.error = Some(InstallErrorView::from_message(
305+
"canceled",
306+
canceled_phase,
307+
"Install canceled because the instance was deleted",
308+
));
309+
job.state.record_event(InstallJobEventKind::JobCanceled {
310+
phase: canceled_phase,
311+
});
312+
313+
let Some(record) = store::finish_active(
314+
job.id,
315+
InstallJobStatus::Canceled,
316+
&job.state,
317+
state,
318+
)
319+
.await?
320+
else {
321+
continue;
322+
};
323+
324+
store::dismiss(job.id, state).await?;
325+
emit_install_job(&record.snapshot()).await?;
326+
}
327+
328+
Ok(())
329+
}
330+
298331
pub async fn dismiss_job(job_id: Uuid) -> crate::Result<()> {
299332
let state = State::get().await?;
300333
store::dismiss(job_id, &state).await
@@ -577,7 +610,11 @@ async fn run_job(job_id: Uuid) -> crate::Result<()> {
577610

578611
let result = Box::pin(run_request(job_id, &mut job_state, &state)).await;
579612
if let Ok(record) = store::get_required(job_id, &state).await {
613+
let status = record.status;
580614
job_state = record.state;
615+
if status != InstallJobStatus::Running {
616+
return Ok(());
617+
}
581618
}
582619

583620
let result = match result {

packages/app-lib/src/install/store.rs

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,17 @@ pub async fn list_interrupted_candidates(
201201
Ok(deserialize_rows(rows))
202202
}
203203

204+
pub async fn list_active_for_instance(
205+
instance_id: &str,
206+
app_state: &State,
207+
) -> crate::Result<Vec<InstallJobRecord>> {
208+
Ok(list_interrupted_candidates(app_state)
209+
.await?
210+
.into_iter()
211+
.filter(|job| job.instance_id.as_deref() == Some(instance_id))
212+
.collect())
213+
}
214+
204215
pub async fn update_state(
205216
id: Uuid,
206217
state: &InstallJobState,
@@ -212,20 +223,30 @@ pub async fn update_state(
212223
let id_value = id.to_string();
213224
let modified = now.timestamp();
214225

215-
sqlx::query!(
226+
let result = sqlx::query(
216227
"
217228
UPDATE install_jobs
218-
SET instance_id = ?, state = ?, modified = ?
219-
WHERE id = ?
229+
SET
230+
instance_id = (SELECT id FROM instances WHERE id = ?),
231+
state = ?,
232+
modified = ?
233+
WHERE id = ? AND status IN ('queued', 'running')
220234
",
221-
instance_id,
222-
json,
223-
modified,
224-
id_value,
225235
)
236+
.bind(instance_id)
237+
.bind(json)
238+
.bind(modified)
239+
.bind(id_value)
226240
.execute(&app_state.pool)
227241
.await?;
228242

243+
if result.rows_affected() == 0 {
244+
return Err(crate::ErrorKind::InputError(format!(
245+
"Install job {id} is no longer active"
246+
))
247+
.into());
248+
}
249+
229250
get_required(id, app_state).await
230251
}
231252

@@ -319,7 +340,12 @@ pub async fn finish_active(
319340
let result = sqlx::query(
320341
"
321342
UPDATE install_jobs
322-
SET instance_id = ?, status = ?, state = ?, modified = ?, finished = ?
343+
SET
344+
instance_id = (SELECT id FROM instances WHERE id = ?),
345+
status = ?,
346+
state = ?,
347+
modified = ?,
348+
finished = ?
323349
WHERE id = ? AND status IN ('queued', 'running')
324350
",
325351
)
@@ -356,19 +382,24 @@ pub async fn complete_success(
356382
let mut transaction = app_state.pool.begin().await?;
357383

358384
let job_result = sqlx::query(
359-
"
385+
"
360386
UPDATE install_jobs
361-
SET instance_id = ?, status = 'succeeded', state = ?, modified = ?, finished = ?
387+
SET
388+
instance_id = (SELECT id FROM instances WHERE id = ?),
389+
status = 'succeeded',
390+
state = ?,
391+
modified = ?,
392+
finished = ?
362393
WHERE id = ? AND status = 'running'
363394
",
364-
)
365-
.bind(&instance_id)
366-
.bind(json)
367-
.bind(now)
368-
.bind(now)
369-
.bind(id_value)
370-
.execute(&mut *transaction)
371-
.await?;
395+
)
396+
.bind(&instance_id)
397+
.bind(json)
398+
.bind(now)
399+
.bind(now)
400+
.bind(id_value)
401+
.execute(&mut *transaction)
402+
.await?;
372403

373404
if job_result.rows_affected() == 0 {
374405
transaction.rollback().await?;

0 commit comments

Comments
 (0)