From d505e2eaaa1b7bab65954c804bc2feabac1b39d5 Mon Sep 17 00:00:00 2001 From: Thomas Honeyman Date: Sat, 10 Oct 2026 08:09:39 -0400 Subject: [PATCH 1/4] Restore one-minute retries for healthcheck delivery Keep database and executor reporting, retry failed deliveries after one minute, and log the actual error. Leave registry jobs unchanged. --- CONTRIBUTING.md | 16 +--------------- app/src/App/Server/Healthcheck.purs | 17 +++++++---------- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e3ee6d6..1214a486 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,21 +64,7 @@ The server will load environment variables from the `.env` file in the project r ### Healthchecks Reporting -Set `HEALTHCHECKS_URL` to a Healthchecks success ping URL to enable operational -reports. After a one-minute startup allowance, the server sends a POST every -five minutes, including a short diagnostic body. Configure the check with a -five-minute period and ten minutes of grace. - -A successful database query and an operational job executor produce a success -ping. An initializing, restarting, or livelock-paused executor, or a failed -database query, produces a `/fail` ping. Idle and busy executors are both healthy; -an individual failed package job does not mark the service unhealthy. Reports -time out after ten seconds and retry on the next scheduled report indefinitely. - -This is an internal operational check, not a probe of public DNS, TLS, or nginx, -and does not detect every possible stalled job or external dependency failure. -`/api/v1/status` remains a simple HTTP liveness check. Ping URLs are secrets: do -not commit them or expose them in logs. +Set `HEALTHCHECKS_URL` to a Healthchecks success ping URL to enable operational reports. After a one-minute startup allowance, the server sends a POST every five minutes, including a short diagnostic body. This expects a simple schedule with five-minute internals and ten minutes of grace. ## Quick Start: Running Integration Tests diff --git a/app/src/App/Server/Healthcheck.purs b/app/src/App/Server/Healthcheck.purs index c239ea81..8a57e657 100644 --- a/app/src/App/Server/Healthcheck.purs +++ b/app/src/App/Server/Healthcheck.purs @@ -3,7 +3,6 @@ module Registry.App.Server.Healthcheck (run, report) where import Registry.App.Prelude import Control.Parallel as Parallel -import Data.DateTime (diff) import Data.Time.Duration (Milliseconds(..)) import Effect.Aff as Aff import Effect.Class.Console as Console @@ -13,14 +12,12 @@ import Registry.App.Effect.Db as Db import Registry.App.Server.Env (ServerEnv, runEffects) import Registry.App.Server.Env as Env --- | Give startup one minute, then report on a five-minute cadence. Failures to --- | deliver a ping never stop the reporter; the next scheduled ping retries. +-- | Give startup one minute, then wait five minutes after a delivered report. +-- | Failed deliveries retry after one minute, indefinitely. run :: ServerEnv -> String -> Aff Unit run env url = Aff.delay (Milliseconds 60_000.0) *> loop where - interval = Milliseconds 300_000.0 loop = do - start <- nowUTC database <- runEffects env $ void Db.selectNextPublishJob executor <- liftEffect $ Ref.read env.executorStatus let @@ -32,11 +29,11 @@ run env url = Aff.delay (Milliseconds 60_000.0) *> loop Env.Paused -> Left "Executor paused after repeated job resets" Env.Restarting -> Left "Executor restarting after unexpected exit; see server logs" result <- Aff.attempt $ report url health - for_ (either Just (const Nothing) result) \_ -> - Console.warn "Healthcheck report failed; retrying next scheduled report" - end <- nowUTC - let Milliseconds elapsed = end `diff` start - Aff.delay $ Milliseconds $ max 0.0 (unwrap interval - elapsed) + case result of + Left error -> do + Console.warn $ "Healthcheck report failed; retrying in one minute: " <> Aff.message error + Aff.delay $ Milliseconds 60_000.0 + Right _ -> Aff.delay $ Milliseconds 300_000.0 loop -- | Bound requests and use abortable fetch so an unreachable monitoring service From 8068a193232b670e6b10a89c126460dd89793b09 Mon Sep 17 00:00:00 2001 From: Thomas Honeyman Date: Sat, 10 Oct 2026 11:10:12 -0400 Subject: [PATCH 2/4] Log underlying healthcheck delivery errors Include nested Fetch causes, error codes, and connection details in server logs. Keep healthcheck timing and registry job behavior unchanged. --- app/src/App/Server/Healthcheck.purs | 4 +++- foreign/src/Foreign/Console.js | 3 +++ foreign/src/Foreign/Console.purs | 8 ++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 foreign/src/Foreign/Console.js create mode 100644 foreign/src/Foreign/Console.purs diff --git a/app/src/App/Server/Healthcheck.purs b/app/src/App/Server/Healthcheck.purs index 8a57e657..93ddf268 100644 --- a/app/src/App/Server/Healthcheck.purs +++ b/app/src/App/Server/Healthcheck.purs @@ -11,6 +11,7 @@ import Fetch as Fetch import Registry.App.Effect.Db as Db import Registry.App.Server.Env (ServerEnv, runEffects) import Registry.App.Server.Env as Env +import Registry.Foreign.Console as Console.Foreign -- | Give startup one minute, then wait five minutes after a delivered report. -- | Failed deliveries retry after one minute, indefinitely. @@ -31,7 +32,8 @@ run env url = Aff.delay (Milliseconds 60_000.0) *> loop result <- Aff.attempt $ report url health case result of Left error -> do - Console.warn $ "Healthcheck report failed; retrying in one minute: " <> Aff.message error + Console.warn "Healthcheck report failed; retrying in one minute:" + liftEffect $ Console.Foreign.warnError error Aff.delay $ Milliseconds 60_000.0 Right _ -> Aff.delay $ Milliseconds 300_000.0 loop diff --git a/foreign/src/Foreign/Console.js b/foreign/src/Foreign/Console.js new file mode 100644 index 00000000..070bd7d6 --- /dev/null +++ b/foreign/src/Foreign/Console.js @@ -0,0 +1,3 @@ +import { inspect } from "node:util"; + +export const warnError = (error) => () => console.warn(inspect(error, { depth: null })); diff --git a/foreign/src/Foreign/Console.purs b/foreign/src/Foreign/Console.purs new file mode 100644 index 00000000..1a5c242e --- /dev/null +++ b/foreign/src/Foreign/Console.purs @@ -0,0 +1,8 @@ +module Registry.Foreign.Console (warnError) where + +import Prelude + +import Effect (Effect) +import Effect.Exception (Error) + +foreign import warnError :: Error -> Effect Unit From af85afd19d4a3c3d60769641a4bf178c0f924efc Mon Sep 17 00:00:00 2001 From: Thomas Honeyman Date: Sat, 10 Oct 2026 13:50:56 -0400 Subject: [PATCH 3/4] Use Debug to log healthcheck errors without a custom binding --- app/spago.yaml | 1 + app/src/App/Server/Healthcheck.purs | 4 ++-- foreign/src/Foreign/Console.js | 3 --- foreign/src/Foreign/Console.purs | 8 -------- spago.lock | 10 ++++++++++ 5 files changed, 13 insertions(+), 13 deletions(-) delete mode 100644 foreign/src/Foreign/Console.js delete mode 100644 foreign/src/Foreign/Console.purs diff --git a/app/spago.yaml b/app/spago.yaml index 6c2b4bd5..dd7e4eac 100644 --- a/app/spago.yaml +++ b/app/spago.yaml @@ -17,6 +17,7 @@ package: - const - control - datetime + - debug - dodo-printer - dotenv - effect diff --git a/app/src/App/Server/Healthcheck.purs b/app/src/App/Server/Healthcheck.purs index 93ddf268..06289449 100644 --- a/app/src/App/Server/Healthcheck.purs +++ b/app/src/App/Server/Healthcheck.purs @@ -4,6 +4,7 @@ import Registry.App.Prelude import Control.Parallel as Parallel import Data.Time.Duration (Milliseconds(..)) +import Debug as Debug import Effect.Aff as Aff import Effect.Class.Console as Console import Effect.Ref as Ref @@ -11,7 +12,6 @@ import Fetch as Fetch import Registry.App.Effect.Db as Db import Registry.App.Server.Env (ServerEnv, runEffects) import Registry.App.Server.Env as Env -import Registry.Foreign.Console as Console.Foreign -- | Give startup one minute, then wait five minutes after a delivered report. -- | Failed deliveries retry after one minute, indefinitely. @@ -33,7 +33,7 @@ run env url = Aff.delay (Milliseconds 60_000.0) *> loop case result of Left error -> do Console.warn "Healthcheck report failed; retrying in one minute:" - liftEffect $ Console.Foreign.warnError error + Debug.traceM error Aff.delay $ Milliseconds 60_000.0 Right _ -> Aff.delay $ Milliseconds 300_000.0 loop diff --git a/foreign/src/Foreign/Console.js b/foreign/src/Foreign/Console.js deleted file mode 100644 index 070bd7d6..00000000 --- a/foreign/src/Foreign/Console.js +++ /dev/null @@ -1,3 +0,0 @@ -import { inspect } from "node:util"; - -export const warnError = (error) => () => console.warn(inspect(error, { depth: null })); diff --git a/foreign/src/Foreign/Console.purs b/foreign/src/Foreign/Console.purs deleted file mode 100644 index 1a5c242e..00000000 --- a/foreign/src/Foreign/Console.purs +++ /dev/null @@ -1,8 +0,0 @@ -module Registry.Foreign.Console (warnError) where - -import Prelude - -import Effect (Effect) -import Effect.Exception (Error) - -foreign import warnError :: Error -> Effect Unit diff --git a/spago.lock b/spago.lock index 8e89fad9..b9296e1b 100644 --- a/spago.lock +++ b/spago.lock @@ -16,6 +16,7 @@ "const", "control", "datetime", + "debug", "dodo-printer", "dotenv", "effect", @@ -1251,6 +1252,15 @@ "tuples" ] }, + "debug": { + "type": "registry", + "version": "6.0.2", + "integrity": "sha256-d/EzRm/J2JyzvlIeo2Ex78Y7HRB0IXBitnVJywL4gHk=", + "dependencies": [ + "functions", + "prelude" + ] + }, "distributive": { "type": "registry", "version": "6.0.0", From 6f51d444ea36edfc59062b3bf585c110f87a463f Mon Sep 17 00:00:00 2001 From: Thomas Honeyman Date: Sat, 10 Oct 2026 13:57:35 -0400 Subject: [PATCH 4/4] Remove Debug logging and restore full error output --- app/spago.yaml | 1 - app/src/App/Server/Healthcheck.purs | 4 ++-- foreign/src/Foreign/Console.js | 3 +++ foreign/src/Foreign/Console.purs | 8 ++++++++ spago.lock | 10 ---------- 5 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 foreign/src/Foreign/Console.js create mode 100644 foreign/src/Foreign/Console.purs diff --git a/app/spago.yaml b/app/spago.yaml index dd7e4eac..6c2b4bd5 100644 --- a/app/spago.yaml +++ b/app/spago.yaml @@ -17,7 +17,6 @@ package: - const - control - datetime - - debug - dodo-printer - dotenv - effect diff --git a/app/src/App/Server/Healthcheck.purs b/app/src/App/Server/Healthcheck.purs index 06289449..93ddf268 100644 --- a/app/src/App/Server/Healthcheck.purs +++ b/app/src/App/Server/Healthcheck.purs @@ -4,7 +4,6 @@ import Registry.App.Prelude import Control.Parallel as Parallel import Data.Time.Duration (Milliseconds(..)) -import Debug as Debug import Effect.Aff as Aff import Effect.Class.Console as Console import Effect.Ref as Ref @@ -12,6 +11,7 @@ import Fetch as Fetch import Registry.App.Effect.Db as Db import Registry.App.Server.Env (ServerEnv, runEffects) import Registry.App.Server.Env as Env +import Registry.Foreign.Console as Console.Foreign -- | Give startup one minute, then wait five minutes after a delivered report. -- | Failed deliveries retry after one minute, indefinitely. @@ -33,7 +33,7 @@ run env url = Aff.delay (Milliseconds 60_000.0) *> loop case result of Left error -> do Console.warn "Healthcheck report failed; retrying in one minute:" - Debug.traceM error + liftEffect $ Console.Foreign.warnError error Aff.delay $ Milliseconds 60_000.0 Right _ -> Aff.delay $ Milliseconds 300_000.0 loop diff --git a/foreign/src/Foreign/Console.js b/foreign/src/Foreign/Console.js new file mode 100644 index 00000000..070bd7d6 --- /dev/null +++ b/foreign/src/Foreign/Console.js @@ -0,0 +1,3 @@ +import { inspect } from "node:util"; + +export const warnError = (error) => () => console.warn(inspect(error, { depth: null })); diff --git a/foreign/src/Foreign/Console.purs b/foreign/src/Foreign/Console.purs new file mode 100644 index 00000000..1a5c242e --- /dev/null +++ b/foreign/src/Foreign/Console.purs @@ -0,0 +1,8 @@ +module Registry.Foreign.Console (warnError) where + +import Prelude + +import Effect (Effect) +import Effect.Exception (Error) + +foreign import warnError :: Error -> Effect Unit diff --git a/spago.lock b/spago.lock index b9296e1b..8e89fad9 100644 --- a/spago.lock +++ b/spago.lock @@ -16,7 +16,6 @@ "const", "control", "datetime", - "debug", "dodo-printer", "dotenv", "effect", @@ -1252,15 +1251,6 @@ "tuples" ] }, - "debug": { - "type": "registry", - "version": "6.0.2", - "integrity": "sha256-d/EzRm/J2JyzvlIeo2Ex78Y7HRB0IXBitnVJywL4gHk=", - "dependencies": [ - "functions", - "prelude" - ] - }, "distributive": { "type": "registry", "version": "6.0.0",