Tâches de fond
Pour le travail asynchrone (rappels, exports, nettoyage), un module fait tourner son propre worker. PostgreSQL fournit le verrouillage concurrent sûr via FOR UPDATE SKIP LOCKED.
Le worker #
// services/reminder.rs — chaque module gère son propre travail de fond (polling)
pub async fn run_worker(state: Arc<AppState>) {
loop {
tokio::time::sleep(Duration::from_secs(60)).await;
if let Err(e) = process_due(&state).await {
tracing::error!(error = %e, "worker memo");
}
}
}
async fn process_due(state: &AppState) -> Result<()> {
let due: Vec<(Uuid,)> = sqlx::query_as(
"SELECT id FROM memo.jobs WHERE status='pending' AND run_after <= NOW()
ORDER BY run_after FOR UPDATE SKIP LOCKED LIMIT 50"
).fetch_all(&state.db).await?;
for (id,) in due {
// … traiter, puis :
sqlx::query("UPDATE memo.jobs SET status='done', done_at=NOW() WHERE id=$1")
.bind(id).execute(&state.db).await?;
}
Ok(())
}Le lancer au démarrage #
// dans main.rs, avant axum::serve :
tokio::spawn(reminder::run_worker(Arc::new(state.clone())));Le worker — dans d'autres langages #
En Rust c'est une tâche tokio ; ailleurs, un démon (systemd) qui boucle. La requête SKIP LOCKED reste identique :
// Rust — tâche tokio
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(60)).await;
let due = sqlx::query("SELECT id FROM memo.jobs
WHERE status='pending' AND run_after <= NOW()
FOR UPDATE SKIP LOCKED LIMIT 50").fetch_all(&db).await;
// … traiter, puis UPDATE … SET status='done'
}
});<?php
// worker.php — démon lancé par systemd : php worker.php
while (true) {
$rows = $pdo->query("SELECT id FROM memo.jobs
WHERE status='pending' AND run_after <= NOW()
FOR UPDATE SKIP LOCKED LIMIT 50")->fetchAll();
foreach ($rows as $r) { /* traiter, puis marquer 'done' */ }
sleep(60);
}# Python — boucle de worker
import time
while True:
rows = db.execute("""SELECT id FROM memo.jobs
WHERE status='pending' AND run_after <= NOW()
FOR UPDATE SKIP LOCKED LIMIT 50""").fetchall()
for r in rows:
... # traiter, puis status='done'
time.sleep(60)// Go — goroutine
go func() {
for {
time.Sleep(60 * time.Second)
rows, _ := db.Query(`SELECT id FROM memo.jobs
WHERE status='pending' AND run_after <= NOW()
FOR UPDATE SKIP LOCKED LIMIT 50`)
// … traiter, puis UPDATE … SET status='done'
rows.Close()
}
}()# Perl — boucle de démon
while (1) {
my $due = $dbh->selectall_arrayref(q{
SELECT id FROM memo.jobs
WHERE status='pending' AND run_after <= NOW()
FOR UPDATE SKIP LOCKED LIMIT 50}, { Slice => {} });
# ... traiter, puis status='done'
sleep 60;
}Note
SKIP LOCKED permet à plusieurs workers de piocher des jobs sans se marcher dessus. Le core dispose aussi d'une file core.jobs ; un module peut soit la réutiliser, soit gérer la sienne comme ici.