fix: more work on background tasks

This commit is contained in:
2026-07-24 11:43:11 +02:00
parent c187108f89
commit da13bac755
45 changed files with 2905 additions and 732 deletions

View File

@@ -408,6 +408,8 @@ impl ProtocolError {
}
fn remote_task(snapshot: TaskSnapshot) -> RemoteTask {
let cancellable = matches!(snapshot.status, TaskStatus::Pending | TaskStatus::Running)
&& !snapshot.cancellation_requested;
let (status, failure) = match snapshot.status {
TaskStatus::Pending => ("pending", None),
TaskStatus::Running => ("running", None),
@@ -418,6 +420,10 @@ fn remote_task(snapshot: TaskSnapshot) -> RemoteTask {
RemoteTask {
id: snapshot.id,
label: snapshot.label,
group_id: snapshot.group_id,
group_name: snapshot.group_name,
cancellation_requested: snapshot.cancellation_requested,
cancellable,
status: status.to_owned(),
progress: snapshot.progress,
message: snapshot.message.or(failure),
@@ -610,7 +616,11 @@ mod tests {
let fixture = Fixture::new();
let mut session = fixture.session();
let _ = session.pending();
let task = fixture.host.tasks().submit("Generate site");
let task =
fixture
.host
.tasks()
.submit_grouped("Generate site", "generation-1", "Generate Site");
fixture
.host
.tasks()
@@ -618,7 +628,11 @@ mod tests {
let update = session.pending();
assert!(matches!(
update.as_slice(),
[ServerMessage::Tasks { tasks, .. }] if tasks[0].progress == Some(0.5)
[ServerMessage::Tasks { tasks, .. }]
if tasks[0].progress == Some(0.5)
&& tasks[0].group_id.as_deref() == Some("generation-1")
&& tasks[0].group_name.as_deref() == Some("Generate Site")
&& tasks[0].cancellable
));
assert!(
session

View File

@@ -57,7 +57,12 @@ pub enum ServerMessage {
pub struct RemoteTask {
pub id: u64,
pub label: String,
pub group_id: Option<String>,
pub group_name: Option<String>,
pub status: String,
pub progress: Option<f32>,
pub message: Option<String>,
#[serde(default)]
pub cancellation_requested: bool,
pub cancellable: bool,
}

View File

@@ -2,12 +2,14 @@ use std::collections::{HashMap, HashSet};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::mpsc;
use std::time::Duration;
use anyhow::{Result, anyhow, bail};
use bds_core::db::Database;
use bds_core::engine::ai::{AiEndpointConfig, AiEndpointKind};
use bds_core::engine::task::{TaskId, TaskManager};
use bds_core::engine::{self, domain_events};
use bds_core::i18n::{UiLocale, normalize_language};
use bds_core::model::metadata::ProjectMetadata;
@@ -600,7 +602,7 @@ impl TuiApp {
fn queue_task(
&mut self,
label: &str,
work: impl FnOnce() -> Result<BackgroundResult> + Send + 'static,
work: impl FnOnce(Arc<TaskManager>, TaskId) -> Result<BackgroundResult> + Send + 'static,
) {
let tasks = self.host.tasks();
let task_id = tasks.submit(label);
@@ -608,22 +610,32 @@ impl TuiApp {
self.started_task_ids.insert(task_id);
self.status = self.tr_with("tui.taskRunning", &[("label", label)]);
std::thread::spawn(move || {
if !tasks.wait_until_runnable(task_id) {
let Some(worker) = tasks.admit_blocking(task_id) else {
return;
}
match work() {
};
let work_tasks = Arc::clone(&tasks);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
work(work_tasks, task_id)
}))
.map_err(|_| anyhow!("background task panicked"))
.and_then(|result| result);
drop(worker);
match result {
Ok(result) => {
tasks.complete(task_id);
let _ = sender.send(result);
}
Err(error) => {
let cancelled = tasks.is_cancelled(task_id);
let message = error.to_string();
tasks.fail(task_id, message.clone());
let _ = sender.send(BackgroundResult {
status: message,
panel: None,
reload: false,
});
if !cancelled {
let _ = sender.send(BackgroundResult {
status: message,
panel: None,
reload: false,
});
}
}
}
});
@@ -636,15 +648,19 @@ impl TuiApp {
}
match task.status {
engine::task::TaskStatus::Pending | engine::task::TaskStatus::Running => {
self.status = match task.progress {
Some(progress) => self.tr_with(
"tui.taskProgress",
&[
("label", &task.label),
("percent", &format!("{:.0}", progress * 100.0)),
],
),
None => task.label,
self.status = if task.cancellation_requested {
self.tr_with("tui.taskCancelling", &[("label", &task.label)])
} else {
match task.progress {
Some(progress) => self.tr_with(
"tui.taskProgress",
&[
("label", &task.label),
("percent", &format!("{:.0}", progress * 100.0)),
],
),
None => task.label,
}
};
}
engine::task::TaskStatus::Completed if self.completed_task_ids.insert(task.id) => {
@@ -1550,7 +1566,10 @@ impl TuiApp {
let message = prompt.value;
let locale = self.locale;
let label = self.tr("tui.taskGitCommit");
self.queue_task(&label, move || {
self.queue_task(&label, move |tasks, task_id| {
if tasks.is_cancelled(task_id) {
return Err(anyhow!("operation cancelled"));
}
let output = engine::git::GitEngine::new(&data_dir)
.commit_all(&message)?
.output;
@@ -2730,10 +2749,12 @@ impl TuiApp {
let data_dir = self.data_dir()?.to_owned();
let locale = self.locale;
let label = self.tr("tui.taskGitPull");
self.queue_task(&label, move || {
self.queue_task(&label, move |tasks, task_id| {
let mut output = Vec::new();
let result = engine::git::GitEngine::new(&data_dir)
.pull(|| false, |line| output.push(line.text))?;
let result = engine::git::GitEngine::new(&data_dir).pull(
|| tasks.is_cancelled(task_id),
|line| output.push(line.text),
)?;
output.push(result.output);
Ok(BackgroundResult {
status: bds_core::i18n::translate_with(
@@ -2759,10 +2780,12 @@ impl TuiApp {
let data_dir = self.data_dir()?.to_owned();
let locale = self.locale;
let label = self.tr("tui.taskGitPush");
self.queue_task(&label, move || {
self.queue_task(&label, move |tasks, task_id| {
let mut output = Vec::new();
let result = engine::git::GitEngine::new(&data_dir)
.push(|| false, |line| output.push(line.text))?;
let result = engine::git::GitEngine::new(&data_dir).push(
|| tasks.is_cancelled(task_id),
|line| output.push(line.text),
)?;
output.push(result.output);
Ok(BackgroundResult {
status: bds_core::i18n::translate_with(
@@ -2807,13 +2830,26 @@ impl TuiApp {
"metadata-diff" => {
let locale = self.locale;
let label = self.tr("tui.commandMetadataDiff");
self.queue_task(&label, move || {
self.queue_task(&label, move |tasks, task_id| {
let db = Database::open(&database_path)?;
let report = engine::metadata_diff::compute_metadata_diff_with_progress(
db.conn(),
&data_dir,
&project_id,
|current, total| {
tasks.report_progress(
task_id,
Some(current as f32 / total.max(1) as f32),
Some(bds_core::i18n::translate(locale, "tui.commandMetadataDiff")),
);
!tasks.is_cancelled(task_id)
},
)?;
Ok(BackgroundResult {
status: bds_core::i18n::translate(locale, "tui.metadataDiffComplete"),
panel: Some(Panel::Report {
title: bds_core::i18n::translate(locale, "menu.item.metadataDiff"),
body: metadata_report_body(db.conn(), &data_dir, &project_id, locale)?,
body: metadata_report_body_from_report(&report, locale),
action: ReportAction::MetadataDiff,
}),
reload: false,
@@ -2823,13 +2859,26 @@ impl TuiApp {
"validate-site" => {
let locale = self.locale;
let label = self.tr("tui.commandValidateSite");
self.queue_task(&label, move || {
self.queue_task(&label, move |tasks, task_id| {
let db = Database::open(&database_path)?;
let report = engine::validate_site::validate_site_with_progress(
db.conn(),
&data_dir,
&project_id,
|current, total| {
tasks.report_progress(
task_id,
Some(current as f32 / total.max(1) as f32),
Some(bds_core::i18n::translate(locale, "tui.commandValidateSite")),
);
!tasks.is_cancelled(task_id)
},
)?;
Ok(BackgroundResult {
status: bds_core::i18n::translate(locale, "tui.siteValidationComplete"),
panel: Some(Panel::Report {
title: bds_core::i18n::translate(locale, "menu.item.validateSite"),
body: site_validation_body(db.conn(), &data_dir, &project_id, locale)?,
body: site_validation_body_from_report(&report, locale),
action: ReportAction::SiteValidation,
}),
reload: false,
@@ -2839,18 +2888,86 @@ impl TuiApp {
"force-render" => {
let locale = self.locale;
let label = self.tr("tui.commandForceRender");
self.queue_task(&label, move || {
self.queue_task(&label, move |tasks, task_id| {
let db = Database::open(&database_path)?;
let metadata = engine::meta::read_project_json(&data_dir)?;
let posts = published_sources(db.conn(), &data_dir, &project_id)?;
let report = engine::generation::generate_starter_site_forced(
let posts =
bds_core::db::queries::post::list_posts_by_project(db.conn(), &project_id)?;
let total_posts = posts.len();
let mut sources = Vec::new();
for (index, post) in posts.into_iter().enumerate() {
tasks.report_progress(
task_id,
Some((index + 1) as f32 / total_posts.max(1) as f32 * 0.1),
Some(post.title.clone()),
);
if tasks.is_cancelled(task_id) {
return Err(anyhow!("operation cancelled"));
}
if let Some(source) =
engine::generation::load_published_post_source(&data_dir, post)?
{
sources.push(source);
}
}
let prepared = engine::generation::prepare_site_generation(
db.conn(),
&data_dir.join("html"),
&data_dir,
&project_id,
&metadata,
&posts,
metadata.main_language.as_deref().unwrap_or("en"),
&sources,
)?;
let output_dir = data_dir.join("html");
let mut report = engine::generation::GenerationReport::default();
for (index, section) in engine::generation::GenerationSection::ALL
.into_iter()
.enumerate()
{
let progress_tasks = Arc::clone(&tasks);
let cancel_tasks = Arc::clone(&tasks);
report.append(
engine::generation::render_prepared_site_section_with_progress(
db.conn(),
&output_dir,
&project_id,
&prepared,
section,
true,
&|_| {},
move |current, total, path| {
progress_tasks.report_progress(
task_id,
Some(
0.1 + (index as f32
+ current as f32 / total.max(1) as f32)
/ 6.0
* 0.9,
),
Some(path.to_string()),
);
},
move || cancel_tasks.is_cancelled(task_id),
)?,
);
}
let progress_tasks = Arc::clone(&tasks);
let cancel_tasks = Arc::clone(&tasks);
report.append(
engine::generation::build_site_search_index_forced_with_progress(
db.conn(),
&output_dir,
&project_id,
&metadata,
move |current, total, path| {
progress_tasks.report_progress(
task_id,
Some(0.85 + current as f32 / total.max(1) as f32 * 0.15),
Some(path.to_string()),
);
},
move || cancel_tasks.is_cancelled(task_id),
)?,
);
Ok(BackgroundResult {
status: bds_core::i18n::translate_with(
locale,
@@ -2865,12 +2982,21 @@ impl TuiApp {
"rebuild-database" => {
let locale = self.locale;
let label = self.tr("tui.commandRebuildDatabase");
self.queue_task(&label, move || {
self.queue_task(&label, move |tasks, task_id| {
let db = Database::open(&database_path)?;
let report = engine::rebuild::rebuild_from_filesystem(
let progress_tasks = Arc::clone(&tasks);
let report = engine::rebuild::rebuild_from_filesystem_with_progress(
db.conn(),
&data_dir,
&project_id,
Some(Arc::new(move |progress, event| {
progress_tasks.report_progress(
task_id,
Some(progress),
Some(event.localized(locale)),
);
!progress_tasks.is_cancelled(task_id)
})),
)?;
let count = report.posts_created
+ report.posts_updated
@@ -2894,9 +3020,21 @@ impl TuiApp {
"reindex-search" => {
let locale = self.locale;
let label = self.tr("tui.commandReindexSearch");
self.queue_task(&label, move || {
self.queue_task(&label, move |tasks, task_id| {
let db = Database::open(&database_path)?;
let report = engine::search::reindex_project(db.conn(), &project_id, None)?;
let progress_tasks = Arc::clone(&tasks);
let report = engine::search::reindex_project(
db.conn(),
&project_id,
Some(Box::new(move |current, total, name| {
progress_tasks.report_progress(
task_id,
Some(current as f32 / total.max(1) as f32),
Some(name.to_string()),
);
!progress_tasks.is_cancelled(task_id)
})),
)?;
Ok(BackgroundResult {
status: bds_core::i18n::translate_with(
locale,
@@ -3027,6 +3165,13 @@ fn metadata_report_body(
locale: UiLocale,
) -> Result<String> {
let report = engine::metadata_diff::compute_metadata_diff(conn, data_dir, project_id)?;
Ok(metadata_report_body_from_report(&report, locale))
}
fn metadata_report_body_from_report(
report: &engine::metadata_diff::DiffReport,
locale: UiLocale,
) -> String {
let mut lines = report
.diffs
.iter()
@@ -3058,7 +3203,7 @@ fn metadata_report_body(
"tui.noMetadataDifferences",
));
}
Ok(lines.join("\n"))
lines.join("\n")
}
fn site_validation_body(
@@ -3068,7 +3213,14 @@ fn site_validation_body(
locale: UiLocale,
) -> Result<String> {
let report = engine::validate_site::validate_site(conn, data_dir, project_id)?;
Ok(bds_core::i18n::translate_with(
Ok(site_validation_body_from_report(&report, locale))
}
fn site_validation_body_from_report(
report: &engine::validate_site::SiteValidationReport,
locale: UiLocale,
) -> String {
bds_core::i18n::translate_with(
locale,
"tui.siteValidationReport",
&[
@@ -3076,7 +3228,7 @@ fn site_validation_body(
("extra", &report.extra_pages.join("\n")),
("stale", &report.stale_pages.join("\n")),
],
))
)
}
impl TuiApp {