Planlagt publisering (oppgave 14.12): periodisk scheduler i maskinrommet
Legger til en bakgrunns-scheduler som hvert 60. sekund sjekker for belongs_to-edges med publish_at i fortiden der artikkelen ikke er rendret. Ved treff: oppretter render_article-jobb (og render_index for berørte samlinger) i jobbkøen. RSS-feeden oppdateres automatisk siden den genereres dynamisk fra DB. Detaljer: - find_due_articles(): SQL-spørring med deduplisering mot eksisterende jobber - run_publish_scheduler(): orkestrerer én runde med publisering - start_publish_scheduler(): tokio::spawn med 60s poll-intervall - Hooks inn i main.rs ved oppstart, parallelt med jobbkø og pruning Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1d2741a18a
commit
eea66744d8
3 changed files with 145 additions and 2 deletions
|
|
@ -140,6 +140,9 @@ async fn main() {
|
|||
// Start periodisk CAS-pruning i bakgrunnen
|
||||
pruning::start_pruning_loop(db.clone(), cas.clone());
|
||||
|
||||
// Start planlagt publisering-scheduler i bakgrunnen
|
||||
publishing::start_publish_scheduler(db.clone());
|
||||
|
||||
let index_cache = publishing::new_index_cache();
|
||||
let state = AppState { db, jwks, stdb, cas, index_cache };
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ use tokio::sync::RwLock;
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::cas::CasStore;
|
||||
use crate::jobs;
|
||||
use crate::tiptap;
|
||||
use crate::AppState;
|
||||
|
||||
|
|
@ -1270,6 +1271,146 @@ pub async fn preview_theme(
|
|||
.unwrap())
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Planlagt publisering — periodisk scheduler
|
||||
// =============================================================================
|
||||
|
||||
/// Rad fra spørring for planlagte artikler som er klare for publisering.
|
||||
#[derive(sqlx::FromRow, Debug)]
|
||||
struct ScheduledArticle {
|
||||
node_id: Uuid,
|
||||
collection_id: Uuid,
|
||||
}
|
||||
|
||||
/// Finn belongs_to-edges med publish_at i fortiden der artikkelen
|
||||
/// ikke er rendret ennå. Returnerer (node_id, collection_id)-par.
|
||||
async fn find_due_articles(db: &PgPool) -> Result<Vec<ScheduledArticle>, sqlx::Error> {
|
||||
// En artikkel er "due" når:
|
||||
// 1. belongs_to-edge har publish_at <= now()
|
||||
// 2. Noden mangler metadata.rendered.html_hash (ikke rendret)
|
||||
//
|
||||
// Vi sjekker også at det ikke allerede finnes en pending/running
|
||||
// render_article-jobb for denne noden, for å unngå duplikater.
|
||||
sqlx::query_as::<_, ScheduledArticle>(
|
||||
r#"
|
||||
SELECT
|
||||
e.source_id AS node_id,
|
||||
e.target_id AS collection_id
|
||||
FROM edges e
|
||||
JOIN nodes n ON n.id = e.source_id
|
||||
WHERE e.edge_type = 'belongs_to'
|
||||
AND (e.metadata->>'publish_at')::timestamptz <= now()
|
||||
AND (
|
||||
n.metadata->'rendered'->>'html_hash' IS NULL
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM job_queue jq
|
||||
WHERE jq.job_type = 'render_article'
|
||||
AND jq.status IN ('pending', 'running', 'retry')
|
||||
AND jq.payload->>'node_id' = n.id::text
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Kjør én runde med planlagt publisering.
|
||||
/// Returnerer antall artikler som ble lagt i render-kø.
|
||||
async fn run_publish_scheduler(db: &PgPool) -> Result<usize, String> {
|
||||
let due = find_due_articles(db)
|
||||
.await
|
||||
.map_err(|e| format!("Spørring for planlagte artikler feilet: {e}"))?;
|
||||
|
||||
if due.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
tracing::info!(count = due.len(), "Fant planlagte artikler klare for publisering");
|
||||
|
||||
// Samle unike collection_ids for index-oppdatering etterpå
|
||||
let mut collections_to_reindex: std::collections::HashSet<Uuid> = std::collections::HashSet::new();
|
||||
|
||||
for article in &due {
|
||||
let payload = serde_json::json!({
|
||||
"node_id": article.node_id.to_string(),
|
||||
"collection_id": article.collection_id.to_string(),
|
||||
});
|
||||
|
||||
match jobs::enqueue(db, "render_article", payload, Some(article.collection_id), 5).await {
|
||||
Ok(job_id) => {
|
||||
tracing::info!(
|
||||
job_id = %job_id,
|
||||
node_id = %article.node_id,
|
||||
collection_id = %article.collection_id,
|
||||
"Render-jobb opprettet for planlagt artikkel"
|
||||
);
|
||||
collections_to_reindex.insert(article.collection_id);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
node_id = %article.node_id,
|
||||
error = %e,
|
||||
"Kunne ikke opprette render-jobb for planlagt artikkel"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Legg inn render_index-jobb for hver berørt samling (lavere prioritet)
|
||||
for collection_id in &collections_to_reindex {
|
||||
let payload = serde_json::json!({
|
||||
"collection_id": collection_id.to_string(),
|
||||
});
|
||||
|
||||
if let Err(e) = jobs::enqueue(db, "render_index", payload, Some(*collection_id), 3).await {
|
||||
tracing::error!(
|
||||
collection_id = %collection_id,
|
||||
error = %e,
|
||||
"Kunne ikke opprette render_index-jobb"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let count = due.len();
|
||||
tracing::info!(
|
||||
articles = count,
|
||||
collections = collections_to_reindex.len(),
|
||||
"Planlagt publisering: jobber lagt i kø"
|
||||
);
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Start periodisk scheduler for planlagt publisering.
|
||||
/// Sjekker hvert 60. sekund for artikler med publish_at i fortiden.
|
||||
pub fn start_publish_scheduler(db: PgPool) {
|
||||
tokio::spawn(async move {
|
||||
// Vent 30 sekunder etter oppstart før første sjekk
|
||||
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
||||
tracing::info!("Planlagt publisering-scheduler startet (intervall: 60s)");
|
||||
|
||||
loop {
|
||||
match run_publish_scheduler(&db).await {
|
||||
Ok(count) => {
|
||||
if count > 0 {
|
||||
tracing::info!(
|
||||
articles = count,
|
||||
"Planlagt publisering: {} artikler lagt i render-kø",
|
||||
count,
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Planlagt publisering-scheduler feilet");
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tester
|
||||
// =============================================================================
|
||||
|
|
|
|||
3
tasks.md
3
tasks.md
|
|
@ -154,8 +154,7 @@ Uavhengige faser kan fortsatt plukkes.
|
|||
- [x] 14.9 Custom domains: bruker registrerer domene i `publishing`-trait. Maskinrommet validerer DNS, Caddy on-demand TLS med validerings-callback. Re-rendring med riktig canonical URL.
|
||||
- [x] 14.10 Redaksjonell innsending: `submitted_to`-edge med status-metadata (`pending`, `in_review`, `revision_requested`, `rejected`, `approved`). Maskinrommet validerer at kun roller i `submission_roles` kan opprette `submitted_to`, og kun owner/admin kan endre status eller opprette `belongs_to`. Ref: `docs/concepts/publisering.md` § "Innsending".
|
||||
- [x] 14.11 Redaktørens arbeidsflate: frontend-visning av noder med `submitted_to`-edge til samling, gruppert på status. Kanban-stil drag-and-drop for statusendring. Siste kolonne ("Planlagt") setter `publish_at` i edge-metadata.
|
||||
- [~] 14.12 Planlagt publisering: maskinrommet sjekker periodisk (cron/intervall) for `belongs_to`-edges med `publish_at` i fortiden som ikke er rendret. Ved treff: render HTML → CAS → oppdater RSS.
|
||||
> Påbegynt: 2026-03-18T02:10
|
||||
- [x] 14.12 Planlagt publisering: maskinrommet sjekker periodisk (cron/intervall) for `belongs_to`-edges med `publish_at` i fortiden som ikke er rendret. Ved treff: render HTML → CAS → oppdater RSS.
|
||||
- [ ] 14.13 Redaksjonell samtale: ved innsending kan redaktør opprette kommunikasjonsnode knyttet til artikkel + forfatter for diskusjon/feedback utover kort notat i edge-metadata.
|
||||
- [ ] 14.14 Bulk re-rendering: batch-jobb via jobbkø ved temaendring. Paginert (100 artikler om gangen), oppdaterer `renderer_version`. Artikler serveres med gammelt tema til re-rendret.
|
||||
- [ ] 14.15 Dynamiske sider: kategori-sider (filtrert på tag-edges), arkiv (kronologisk med månedsgruppering), søk (PG fulltekst). Alle paginerte, cachet i maskinrommet. Om-side som statisk CAS-node.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue