max / makenotwork
1 file changed,
+76 insertions,
-0 deletions
| @@ -1,0 +1,76 @@ | |||
| 1 | + | //! Whether the platform changelog is published, for the footer link. | |
| 2 | + | //! | |
| 3 | + | //! `base.html` renders the site footer on every page and cannot await a query, | |
| 4 | + | //! so the answer lives in a process-global flag that a background task | |
| 5 | + | //! refreshes. `/changelog` resolves through | |
| 6 | + | //! `db::projects::get_public_project_by_slug(CHANGELOG_PROJECT_SLUG)`; when no | |
| 7 | + | //! such published project exists the route 404s, which turned the footer link | |
| 8 | + | //! into a sitewide dead end. | |
| 9 | + | //! | |
| 10 | + | //! The flag starts `false`, so until the first refresh lands the footer omits a | |
| 11 | + | //! link we cannot yet prove resolves. The route is unchanged: this only decides | |
| 12 | + | //! whether visitors are pointed at it. Publish a changelog project and the link | |
| 13 | + | //! comes back on its own within `REFRESH_INTERVAL_SECS`. | |
| 14 | + | //! | |
| 15 | + | //! Per-process, not per-instance-cluster: the refresher runs in every instance | |
| 16 | + | //! rather than under the scheduler's advisory lock, so a second instance in a | |
| 17 | + | //! rolling deploy does not serve a stale footer. | |
| 18 | + | ||
| 19 | + | use std::sync::atomic::{AtomicBool, Ordering}; | |
| 20 | + | ||
| 21 | + | use sqlx::PgPool; | |
| 22 | + | use tokio::sync::watch; | |
| 23 | + | use tokio::task::JoinHandle; | |
| 24 | + | ||
| 25 | + | use crate::constants; | |
| 26 | + | use crate::db::{self, Slug}; | |
| 27 | + | ||
| 28 | + | static PUBLISHED: AtomicBool = AtomicBool::new(false); | |
| 29 | + | ||
| 30 | + | /// How often each instance re-checks whether the changelog project is public. | |
| 31 | + | /// One indexed lookup per minute per instance, cheap enough that the link's | |
| 32 | + | /// reappearance is prompt without a write-path hook in every publish route. | |
| 33 | + | const REFRESH_INTERVAL_SECS: u64 = 60; | |
| 34 | + | ||
| 35 | + | /// Whether the footer should link to `/changelog`. Called from `base.html`. | |
| 36 | + | pub fn is_published() -> bool { | |
| 37 | + | PUBLISHED.load(Ordering::Relaxed) | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | /// Re-read the changelog project's visibility into the flag. | |
| 41 | + | /// | |
| 42 | + | /// A failed query keeps the previous value rather than hiding the link: a | |
| 43 | + | /// transient DB error is not evidence that the changelog went away. | |
| 44 | + | #[tracing::instrument(skip_all)] | |
| 45 | + | pub async fn refresh(pool: &PgPool) { | |
| 46 | + | let slug = Slug::from_trusted(constants::CHANGELOG_PROJECT_SLUG.to_owned()); | |
| 47 | + | match db::projects::get_public_project_by_slug(pool, &slug).await { | |
| 48 | + | Ok(project) => PUBLISHED.store(project.is_some(), Ordering::Relaxed), | |
| 49 | + | Err(e) => { | |
| 50 | + | tracing::warn!(error = ?e, "changelog visibility refresh failed, keeping last value"); | |
| 51 | + | } | |
| 52 | + | } | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | /// Spawn the per-instance refresh loop. Drop the shutdown sender to stop it. | |
| 56 | + | /// Refreshes once immediately so the first rendered page is already accurate. | |
| 57 | + | pub fn spawn_refresher(pool: PgPool, mut shutdown_rx: watch::Receiver<()>) -> JoinHandle<()> { | |
| 58 | + | tokio::spawn(async move { | |
| 59 | + | let mut interval = | |
| 60 | + | tokio::time::interval(std::time::Duration::from_secs(REFRESH_INTERVAL_SECS)); | |
| 61 | + | interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); | |
| 62 | + | ||
| 63 | + | loop { | |
| 64 | + | tokio::select! { | |
| 65 | + | _ = interval.tick() => refresh(&pool).await, | |
| 66 | + | _ = shutdown_rx.changed() => break, | |
| 67 | + | } | |
| 68 | + | } | |
| 69 | + | }) | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | /// Force the flag for tests that render the footer without a database. | |
| 73 | + | #[doc(hidden)] | |
| 74 | + | pub fn set_published_for_test(published: bool) { | |
| 75 | + | PUBLISHED.store(published, Ordering::Relaxed); | |
| 76 | + | } |