Hot-reload content on a NATS trigger instead of requiring a restart
Deploy / deploy (push) Successful in 29s
Deploy / deploy (push) Successful in 29s
questions is now Arc<ArcSwap<HashMap<...>>> - readers do a lock-free atomic load (state.questions.load().get(&id).cloned()), never blocking on or blocked by a reload. content::watch_for_reload subscribes to portal.content.reload (published by the questions repo's own CI after it lints a push - see that repo's lint-and-reload.yml) and swaps in a freshly re-fetched HashMap on each message. A fetch/parse failure logs and keeps serving the last-good content rather than clearing it.
This commit is contained in:
+47
-4
@@ -146,10 +146,10 @@ impl Requirement {
|
||||
/// Fetches every `*.yaml` file under `subdir` in a Gitea repo as a
|
||||
/// `Question`, keyed by its own `id`. `repo_url` is the repo's normal
|
||||
/// browser URL (e.g. `https://project.uhhm.no/uhhm/questions`) - the
|
||||
/// Gitea host, owner and repo name are all read from it. Runs once at
|
||||
/// startup, over Gitea's public contents API (no auth - the content
|
||||
/// repo is public); no hot-reload yet - restart the process (or add
|
||||
/// polling later) to pick up content changes.
|
||||
/// Gitea host, owner and repo name are all read from it. Called once at
|
||||
/// startup, and again on every `CONTENT_RELOAD_SUBJECT` message (see
|
||||
/// `watch_for_reload`), over Gitea's public contents API (no auth - the
|
||||
/// content repo is public).
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn load_questions_from_gitea(
|
||||
repo_url: &str,
|
||||
@@ -218,3 +218,46 @@ pub async fn load_questions_from_gitea(
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Published by the content repo's own CI (after it lints a push) to
|
||||
/// tell every running instance to pick up the change - a plain fire
|
||||
/// and forget NATS publish, no payload, matching `events.rs`'s
|
||||
/// `ANSWERS_SUBJECT` pattern.
|
||||
#[cfg(feature = "ssr")]
|
||||
pub const CONTENT_RELOAD_SUBJECT: &str = "portal.content.reload";
|
||||
|
||||
/// Runs for the life of the process: re-fetches `repo_url`/`branch` and
|
||||
/// atomically swaps it into `questions` on every `CONTENT_RELOAD_SUBJECT`
|
||||
/// message. A fetch/parse failure logs and keeps serving the last-good
|
||||
/// content rather than clearing it - a bad push to the content repo
|
||||
/// (which should already have been caught by its own lint step) doesn't
|
||||
/// take the site down.
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn watch_for_reload(
|
||||
nats: async_nats::Client,
|
||||
repo_url: String,
|
||||
branch: String,
|
||||
subdir: String,
|
||||
questions: std::sync::Arc<arc_swap::ArcSwap<std::collections::HashMap<String, Question>>>,
|
||||
) {
|
||||
let mut sub = match nats.subscribe(CONTENT_RELOAD_SUBJECT).await {
|
||||
Ok(sub) => sub,
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "failed to subscribe to content reload subject");
|
||||
return;
|
||||
}
|
||||
};
|
||||
use futures::StreamExt;
|
||||
while sub.next().await.is_some() {
|
||||
match load_questions_from_gitea(&repo_url, &branch, &subdir).await {
|
||||
Ok(loaded) => {
|
||||
let count = loaded.len();
|
||||
questions.store(std::sync::Arc::new(loaded));
|
||||
tracing::info!(count, "reloaded content");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "content reload failed, keeping last-good content");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user