Add custom errors with thiserror, log thread id

This commit is contained in:
2026-02-03 23:02:44 +02:00
parent 2e5568fccb
commit f84d812602
8 changed files with 126 additions and 58 deletions

35
src-tauri/Cargo.lock generated
View File

@@ -497,7 +497,7 @@ dependencies = [
"semver",
"serde",
"serde_json",
"thiserror 2.0.17",
"thiserror 2.0.18",
]
[[package]]
@@ -2345,7 +2345,7 @@ dependencies = [
"once_cell",
"png",
"serde",
"thiserror 2.0.17",
"thiserror 2.0.18",
"windows-sys 0.60.2",
]
@@ -3310,7 +3310,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror 2.0.17",
"thiserror 2.0.18",
]
[[package]]
@@ -3460,6 +3460,7 @@ dependencies = [
"tauri-build",
"tauri-plugin-log",
"tauri-plugin-opener",
"thiserror 2.0.18",
]
[[package]]
@@ -4144,7 +4145,7 @@ dependencies = [
"tauri-runtime",
"tauri-runtime-wry",
"tauri-utils",
"thiserror 2.0.17",
"thiserror 2.0.18",
"tokio",
"tray-icon",
"url",
@@ -4196,7 +4197,7 @@ dependencies = [
"sha2",
"syn 2.0.111",
"tauri-utils",
"thiserror 2.0.17",
"thiserror 2.0.18",
"time",
"url",
"uuid",
@@ -4252,7 +4253,7 @@ dependencies = [
"swift-rs",
"tauri",
"tauri-plugin",
"thiserror 2.0.17",
"thiserror 2.0.18",
"time",
]
@@ -4272,7 +4273,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.17",
"thiserror 2.0.18",
"url",
"windows",
"zbus",
@@ -4296,7 +4297,7 @@ dependencies = [
"serde",
"serde_json",
"tauri-utils",
"thiserror 2.0.17",
"thiserror 2.0.18",
"url",
"webkit2gtk",
"webview2-com",
@@ -4360,7 +4361,7 @@ dependencies = [
"serde_json",
"serde_with",
"swift-rs",
"thiserror 2.0.17",
"thiserror 2.0.18",
"toml 0.9.8",
"url",
"urlpattern",
@@ -4414,11 +4415,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.17"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl 2.0.17",
"thiserror-impl 2.0.18",
]
[[package]]
@@ -4434,9 +4435,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.17"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
@@ -4739,7 +4740,7 @@ dependencies = [
"once_cell",
"png",
"serde",
"thiserror 2.0.17",
"thiserror 2.0.18",
"windows-sys 0.60.2",
]
@@ -5113,7 +5114,7 @@ version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
dependencies = [
"thiserror 2.0.17",
"thiserror 2.0.18",
"windows",
"windows-core 0.61.2",
]
@@ -5623,7 +5624,7 @@ dependencies = [
"sha2",
"soup3",
"tao-macros",
"thiserror 2.0.17",
"thiserror 2.0.18",
"url",
"webkit2gtk",
"webkit2gtk-sys",

View File

@@ -31,6 +31,7 @@ rand = "0.9.2"
docx-rs = "0.4.18"
anyhow = "1.0.100"
rayon = "1.11"
thiserror = "2.0.18"
[dev-dependencies]
criterion = { version = "0.8.1", features = ["html_reports"] }

59
src-tauri/src/errors.rs Normal file
View File

@@ -0,0 +1,59 @@
use std::io;
use serde::Serialize;
use thiserror::Error;
#[derive(Error, Debug, Serialize)]
#[serde(tag = "kind", content = "details")]
pub enum SearchError {
#[error("another thread found a solution")]
SolutionFound,
#[error("time limit exceeded")]
Timeout,
#[error("schedule is already manually filled")]
ScheduleFull,
#[error("user configuration is invalid")]
Config(String),
#[error("no solution found")]
NoSolutionFound,
}
impl From<anyhow::Error> for SearchError {
fn from(err: anyhow::Error) -> Self {
SearchError::Config(err.to_string())
}
}
#[derive(Error, Debug)]
pub enum ExportError {
#[error("path not found: {0}")]
InvalidPath(#[from] io::Error),
#[error("docx packaging error: {0}")]
Packaging(String),
#[error("failed to open doc: {0}")]
OpenFailed(String),
}
impl Serialize for ExportError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut s = serializer.serialize_struct("ExportError", 2)?;
s.serialize_field(
"kind",
match self {
ExportError::InvalidPath(_) => "InvalidPath",
ExportError::Packaging(_) => "Packaging",
ExportError::OpenFailed(_) => "OpenFailed",
},
)?;
s.serialize_field("details", &self.to_string())?;
s.end()
}
}

View File

@@ -1,10 +1,10 @@
use std::{fs::File, io::Write};
use anyhow::Context;
use docx_rs::{Docx, Paragraph, Run, RunFonts, Table, TableCell, TableRow};
use crate::{
config::UserConfig,
errors::ExportError,
schedule::MonthlySchedule,
slot::{month_to_greek, weekday_to_greek, Day, ShiftPosition, Slot},
workload::WorkloadTracker,
@@ -22,7 +22,7 @@ pub trait Export {
file_type: FileType,
config: &UserConfig,
tracker: &WorkloadTracker,
) -> anyhow::Result<()>;
) -> Result<(), ExportError>;
}
impl Export for MonthlySchedule {
@@ -31,7 +31,7 @@ impl Export for MonthlySchedule {
file_type: FileType,
config: &UserConfig,
tracker: &WorkloadTracker,
) -> anyhow::Result<()> {
) -> Result<(), ExportError> {
match file_type {
FileType::Txt => self.export_as_txt(config, tracker)?,
FileType::Docx => self.export_as_docx(config)?,
@@ -46,7 +46,7 @@ impl MonthlySchedule {
&self,
config: &UserConfig,
tracker: &WorkloadTracker,
) -> anyhow::Result<()> {
) -> Result<(), ExportError> {
let file = File::create("rota.txt")?;
let mut writer = std::io::BufWriter::new(file);
@@ -157,15 +157,17 @@ impl MonthlySchedule {
doc
}
pub fn export_as_docx(&self, config: &UserConfig) -> anyhow::Result<()> {
pub fn export_as_docx(&self, config: &UserConfig) -> Result<(), ExportError> {
let path = std::path::Path::new("rota.docx");
let file = std::fs::File::create(path)?;
let doc = self.generate_docx(config);
doc.build().pack(file)?;
doc.build()
.pack(file)
.map_err(|e| ExportError::Packaging(e.to_string()))?;
tauri_plugin_opener::open_path(path, None::<&str>)
.context("Created file but failed to open it")?;
.map_err(|e| ExportError::OpenFailed(e.to_string()))?;
Ok(())
}
@@ -173,7 +175,6 @@ impl MonthlySchedule {
#[cfg(test)]
mod tests {
use anyhow::Ok;
use rstest::{fixture, rstest};
use crate::{

View File

@@ -1,9 +1,10 @@
use std::sync::Mutex;
use log::{error, info};
use log::info;
use crate::{
config::{UserConfig, UserConfigDTO},
errors::{ExportError, SearchError},
export::{Export, FileType},
schedule::MonthlySchedule,
scheduler::Scheduler,
@@ -11,6 +12,7 @@ use crate::{
};
pub mod config;
pub mod errors;
pub mod export;
pub mod resident;
pub mod schedule;
@@ -32,16 +34,17 @@ struct AppState {
fn generate(
config: UserConfigDTO,
state: tauri::State<'_, AppState>,
) -> Result<MonthlySchedule, String> {
) -> Result<MonthlySchedule, SearchError> {
let mut schedule = MonthlySchedule::new();
let mut tracker = WorkloadTracker::default();
let config = UserConfig::try_from(config).map_err(|e| e.to_string())?;
let config = UserConfig::try_from(config)?;
let scheduler = Scheduler::new_with_config(config.clone());
scheduler
.run(&mut schedule, &mut tracker)
.inspect_err(|e| error!("{e}"))
.map_err(|e| e.to_string())?;
let solved = scheduler.run(&mut schedule, &mut tracker)?;
if !solved {
return Err(SearchError::NoSolutionFound);
}
info!(
"Scheduler finished successfully in {}ms",
@@ -60,19 +63,13 @@ fn generate(
}
#[tauri::command]
fn export(state: tauri::State<'_, AppState>) -> Result<(), String> {
fn export(state: tauri::State<'_, AppState>) -> Result<(), ExportError> {
let schedule = state.schedule.lock().unwrap();
let tracker = state.tracker.lock().unwrap();
let config = state.config.lock().unwrap();
schedule
.export(FileType::Docx, &config, &tracker)
.inspect_err(|e| error!("{e}"))
.map_err(|e| e.to_string())?;
schedule
.export(FileType::Txt, &config, &tracker)
.inspect_err(|e| error!("{e}"))
.map_err(|e| e.to_string())?;
schedule.export(FileType::Docx, &config, &tracker)?;
schedule.export(FileType::Txt, &config, &tracker)?;
let log_dir = std::env::current_dir().unwrap_or(std::path::PathBuf::from("."));
info!("Files exported at {}", log_dir.display());

View File

@@ -2,6 +2,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use crate::{
config::UserConfig,
errors::SearchError,
resident::ResidentId,
schedule::MonthlySchedule,
slot::Slot,
@@ -9,10 +10,12 @@ use crate::{
workload::{WorkloadBounds, WorkloadTracker},
};
use anyhow::bail;
use log::warn;
use log::info;
use rand::Rng;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use rayon::{
current_thread_index,
iter::{IntoParallelRefIterator, ParallelIterator},
};
pub struct Scheduler {
pub config: UserConfig,
@@ -43,7 +46,7 @@ impl Scheduler {
&self,
schedule: &mut MonthlySchedule,
tracker: &mut WorkloadTracker,
) -> anyhow::Result<bool> {
) -> Result<bool, SearchError> {
schedule.prefill(&self.config);
for (slot, res_id) in schedule.0.iter() {
tracker.insert(*res_id, &self.config, *slot);
@@ -55,7 +58,7 @@ impl Scheduler {
let slot = (0..=self.config.total_slots)
.find(|&slot_idx| !schedule.0.contains_key(&Slot::from(slot_idx)))
.map(Slot::from)
.ok_or_else(|| anyhow::anyhow!("Schedule is already full"))?;
.ok_or(SearchError::ScheduleFull)?;
let resident_ids = self.valid_residents(slot, schedule);
let solved_in_thread = AtomicBool::new(false);
@@ -78,7 +81,7 @@ impl Scheduler {
Ok(true) => Some((local_schedule, local_tracker)),
Ok(false) => None,
Err(e) => {
warn!("Search error: {}", e);
info!("Thread Id: [{}] {}", current_thread_index().unwrap(), e);
None
}
}
@@ -102,13 +105,13 @@ impl Scheduler {
tracker: &mut WorkloadTracker,
slot: Slot,
solved_in_thread: &AtomicBool,
) -> anyhow::Result<bool> {
) -> Result<bool, SearchError> {
if solved_in_thread.load(Ordering::Relaxed) {
bail!("Another thread found the solution")
return Err(SearchError::SolutionFound);
}
if self.timer.limit_exceeded() {
bail!("Time exceeded. Restrictions too tight");
return Err(SearchError::Timeout);
}
if !slot.is_first()
@@ -132,10 +135,10 @@ impl Scheduler {
// sort candidates by current workload, add rng for tie breakers
let mut valid_resident_ids = self.valid_residents(slot, schedule);
valid_resident_ids.sort_unstable_by_key(|res_id| {
let type_count = tracker.get_type_count(res_id, slot.shift_type());
let workload = tracker.current_workload(res_id);
let tie_breaker: f64 = rand::rng().random();
(type_count, workload, (tie_breaker * 1000.0) as usize)
let type_count = tracker.get_type_count(res_id, slot.shift_type());
let workload = tracker.current_workload(res_id);
let tie_breaker: f64 = rand::rng().random();
(type_count, workload, (tie_breaker * 1000.0) as usize)
});
for id in valid_resident_ids {

View File

@@ -1,6 +1,5 @@
#[cfg(test)]
mod integration_tests {
use anyhow::Ok;
use rota_lib::{
config::{ToxicPair, UserConfig},
resident::Resident,

View File

@@ -34,16 +34,22 @@
return day % 2 === 0 ? 1 : 2;
}
interface AppError {
kind: string;
details: string;
}
async function generate() {
let config = rota.toDTO();
console.log(config);
try {
let schedule = await invoke<MonthlyScheduleDTO>("generate", { config });
console.log("replyFromGenerate:", schedule);
console.log(schedule);
rota.solution = schedule;
} catch (error) {
console.error("Error:", error);
const { kind, details } = error as AppError;
console.error(`[${kind}] - ${details}`);
}
}
@@ -53,7 +59,8 @@
try {
await invoke("export", { schedule });
} catch (error) {
console.error("Error:", error);
const { kind, details } = error as AppError;
console.error(`[${kind}] - ${details}`);
}
}
</script>