Add a [cdn] section and purge the edge after publishing
The site is about to sit behind Cloudflare, where the public pages will be allowed to live at the edge for a day (`s-maxage`, next commit). That is only correct if the edge is emptied the moment a new issue lands, so the origin now does the emptying itself rather than leaving it to an operator to remember. `[cdn]` is inert by default: with no `provider` nothing is called and no token is needed, so an origin with no CDN behaves exactly as before. Setting `provider = "cloudflare"` without both a zone id and `DAILY_EPUB_CDN__API_TOKEN` is a config error — a half-configured purge would publish into a stale edge and say nothing. The purge is `purge_everything` on purpose. A new issue changes more than its own page: `/`, `/issues`, `/feed.xml`, `/issues.json`, and the previous issue's page too, whose "latest" nav marker moves. A per-URL list of that set is exactly the kind of thing that silently rots, and everything expensive at the edge is content-hashed, so refilling it costs one origin fetch. A purge failure is logged at warn and never fails the run: the paper is already published and recorded by then, and a few stale hours are not worth failing over. `daily-epub cdn purge` runs the same code by hand; it takes no run lock because it touches neither the database nor the publish directories. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
This commit is contained in:
+22
@@ -64,6 +64,15 @@ enum Command {
|
||||
/// Operator jobs (what `daily-epub-job@<name>.service` runs).
|
||||
#[command(subcommand)]
|
||||
Job(JobCommand),
|
||||
/// CDN cache maintenance.
|
||||
#[command(subcommand)]
|
||||
Cdn(CdnCommand),
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum CdnCommand {
|
||||
/// Purge the whole edge cache for the configured zone.
|
||||
Purge,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
@@ -369,6 +378,17 @@ async fn main() -> Result<()> {
|
||||
db.migrate().await?;
|
||||
println!("migrations up to date: {}", config.database_path.display());
|
||||
}
|
||||
Command::Cdn(CdnCommand::Purge) => {
|
||||
// No database, no files, no provider budgets: nothing the run lock
|
||||
// protects, so `generate` and a purge may safely overlap.
|
||||
match daily_epub::cdn::purge_all(&config).await {
|
||||
Ok(outcome) => println!("{outcome}"),
|
||||
Err(error) => {
|
||||
eprintln!("cdn purge failed: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::Config(ConfigCommand::Check) => {
|
||||
// Reaching here means `Config::load` already validated it; a bad
|
||||
// config exited non-zero above. Nothing is opened, nothing locked.
|
||||
@@ -426,6 +446,7 @@ fn lock_holder(command: &Command) -> Option<&'static str> {
|
||||
| Command::Features(FeaturesCommand::Prune)
|
||||
| Command::Db(_)
|
||||
| Command::Config(_)
|
||||
| Command::Cdn(_)
|
||||
| Command::Users(_) => None,
|
||||
}
|
||||
}
|
||||
@@ -1101,6 +1122,7 @@ mod tests {
|
||||
vec!["db", "migrate"],
|
||||
vec!["features", "prune"],
|
||||
vec!["config", "check"],
|
||||
vec!["cdn", "purge"],
|
||||
vec!["job", "run", "features-prune"],
|
||||
vec!["job", "run", "not-a-job"],
|
||||
] {
|
||||
|
||||
Reference in New Issue
Block a user