Skip to main content

max / makenotwork

3.1 KB · 77 lines History Blame Raw
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 }
77