use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{Receiver, SyncSender, TrySendError, sync_channel};
use std::sync::{Arc, OnceLock, Weak};
use std::thread::JoinHandle;
use crate::error::DbResult;
use crate::shard::{ImmutableFile, Shards};
use crate::sync::{self, Mutex};
const QUEUE_CAPACITY: usize = 16;
pub(crate) struct HintJob {
pub(crate) shard_idx: usize,
pub(crate) file_id: u32,
pub(crate) file: Arc<ImmutableFile>,
pub(crate) file_len: u64,
pub(crate) key_len: usize,
pub(crate) data_path: PathBuf,
#[cfg(feature = "encryption")]
pub(crate) cipher: Option<Arc<crate::crypto::PageCipher>>,
#[cfg(test)]
pub(crate) gate: Option<Arc<crate::shard::HintGate>>,
}
pub(crate) struct HintService {
worker: Mutex<Option<Worker>>,
shards: OnceLock<Weak<Shards>>,
dropped: AtomicU64,
}
struct Worker {
tx: SyncSender<HintJob>,
handle: JoinHandle<()>,
}
impl HintService {
pub(crate) fn new() -> Self {
Self {
worker: Mutex::new(None),
shards: OnceLock::new(),
dropped: AtomicU64::new(0),
}
}
pub(crate) fn attach_shards(&self, shards: Weak<Shards>) {
let _ = self.shards.set(shards);
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn dropped(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
pub(crate) fn submit(&self, job: HintJob) {
let file_id = job.file_id;
let mut guard = sync::lock(&self.worker);
let worker = guard.get_or_insert_with(|| self.spawn());
match worker.tx.try_send(job) {
Ok(()) => {}
Err(TrySendError::Full(job)) => {
self.dropped.fetch_add(1, Ordering::Relaxed);
tracing::warn!(
file_id = job.file_id,
capacity = QUEUE_CAPACITY,
"hint queue full — skipping hint for this file; recovery \
will scan it instead"
);
}
Err(TrySendError::Disconnected(_)) => {
tracing::warn!(file_id, "hint worker stopped — skipping hint for this file");
}
}
}
fn spawn(&self) -> Worker {
let (tx, rx) = sync_channel(QUEUE_CAPACITY);
let shards = self.shards.get().cloned().unwrap_or_else(Weak::new);
let handle = std::thread::Builder::new()
.name("armdb-hint".into())
.spawn(move || run(&shards, rx))
.expect("spawn hint worker");
Worker { tx, handle }
}
pub(crate) fn drain_and_stop(&self) {
let worker = sync::lock(&self.worker).take();
if let Some(Worker { tx, handle }) = worker {
drop(tx);
let _ = handle.join();
}
}
}
#[cfg(test)]
pub(crate) static LIVE_WORKERS: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
fn run(shards: &Weak<Shards>, rx: Receiver<HintJob>) {
#[cfg(test)]
LIVE_WORKERS.fetch_add(1, Ordering::Relaxed);
#[cfg(test)]
let _guard = scopeguard();
while let Ok(job) = rx.recv() {
let file_id = job.file_id;
if let Err(e) = process(shards, job) {
tracing::warn!(
file_id,
error = %e,
"hint generation failed — recovery will scan this file instead"
);
}
}
}
fn process(shards: &Weak<Shards>, job: HintJob) -> DbResult<()> {
#[cfg(test)]
if let Some(gate) = &job.gate {
gate.wait();
}
let data = match generate(&job) {
Ok(data) => data,
Err(crate::error::DbError::CorruptedEntry { offset }) => {
tracing::warn!(
file_id = job.file_id,
offset,
"hint generation stopped early — skipping hint file"
);
return Ok(());
}
Err(e) => return Err(e),
};
let hint_path = crate::hint::hint_path_for_data(&job.data_path);
let tmp = crate::hint::write_hint_tmp(&hint_path, &data)?;
finish_job(shards, &job, &tmp, &hint_path)
}
fn finish_job(
shards: &Weak<Shards>,
job: &HintJob,
tmp: &std::path::Path,
hint_path: &std::path::Path,
) -> DbResult<()> {
let Some(shards) = shards.upgrade() else {
let _ = std::fs::remove_file(tmp);
return Ok(());
};
let Some(shard) = shards.get(job.shard_idx) else {
let _ = std::fs::remove_file(tmp);
return Ok(());
};
let inner = shard.lock();
if !inner.immutable.iter().any(|f| f.file_id == job.file_id) {
drop(inner);
let _ = std::fs::remove_file(tmp);
tracing::debug!(
file_id = job.file_id,
"retired file was compacted before its hint was written"
);
return Ok(());
}
crate::hint::publish_hint_tmp(tmp, hint_path)
}
impl Drop for HintService {
fn drop(&mut self) {
self.drain_and_stop();
}
}
fn generate(job: &HintJob) -> DbResult<Vec<u8>> {
#[cfg(feature = "encryption")]
{
if let (Some(cipher), Some(tag_file)) = (&job.cipher, &job.file.tag_file) {
return crate::hint::generate_hint_data_dyn_encrypted(
&job.file.file,
job.file_len,
job.key_len,
cipher,
tag_file.as_ref(),
job.file_id,
);
}
}
crate::hint::generate_hint_data_dyn(&job.file.file, job.file_len, job.key_len)
}
#[cfg(test)]
fn scopeguard() -> impl Drop {
struct Guard;
impl Drop for Guard {
fn drop(&mut self) {
LIVE_WORKERS.fetch_sub(1, Ordering::Relaxed);
}
}
Guard
}