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
|
// Start periodisk CAS-pruning i bakgrunnen
|
||||||
pruning::start_pruning_loop(db.clone(), cas.clone());
|
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 index_cache = publishing::new_index_cache();
|
||||||
let state = AppState { db, jwks, stdb, cas, index_cache };
|
let state = AppState { db, jwks, stdb, cas, index_cache };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ use tokio::sync::RwLock;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::cas::CasStore;
|
use crate::cas::CasStore;
|
||||||
|
use crate::jobs;
|
||||||
use crate::tiptap;
|
use crate::tiptap;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
|
|
@ -1270,6 +1271,146 @@ pub async fn preview_theme(
|
||||||
.unwrap())
|
.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
|
// 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.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.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.
|
- [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.
|
- [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.
|
||||||
> Påbegynt: 2026-03-18T02:10
|
|
||||||
- [ ] 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.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.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.
|
- [ ] 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