Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
20d6b21
Update database schemas and add job executor loop
thomashoneyman Sep 14, 2024
4b9743c
Split Server module into Env, Router, JobExecutor, and Main
fsoikin Jun 22, 2025
2fe9635
Fix up build
fsoikin Jun 26, 2025
a4f1047
Run job executor
fsoikin Jul 6, 2025
dfd7e78
Fix integration tests
f-f Dec 9, 2025
cdbac72
WIP matrix builds
f-f Dec 14, 2025
253f85c
add missing version to publish fixtures
pacchettibotti Dec 12, 2025
13eaf3a
Add missing packageName and packageVersion to InsertMatrixJob
pacchettibotti Dec 12, 2025
301d348
Fix finishedAt timestamp to capture time after job execution
pacchettibotti Dec 12, 2025
0a13995
Implement matrix jobs, and the recursive enqueuing of new ones
f-f Dec 14, 2025
50cd04b
Reset incomplete jobs so they can be picked up again
f-f Dec 14, 2025
6a57d75
Run matrix jobs for the whole registry when finding a new compiler ve…
f-f Dec 14, 2025
408a46b
Merge branch 'trh/compilers-in-metadata' into f-f/concurrent-jobs-2
thomashoneyman Dec 19, 2025
f1a602b
resolve build issues
thomashoneyman Dec 19, 2025
f943991
fix smoke test
thomashoneyman Dec 19, 2025
ea420fa
Split package jobs into separate tables, return all data from the job…
f-f Dec 22, 2025
9a8d1ba
implement thin client for github issues
thomashoneyman Dec 22, 2025
5ae9449
clean up test failures
thomashoneyman Dec 22, 2025
ad6c328
reinstate missing comments
thomashoneyman Dec 22, 2025
6c023cf
Remove COMMENT effect, add NOTIFY log
thomashoneyman Dec 23, 2025
e69b875
Implement endpoint for returning jobs
f-f Dec 25, 2025
c33a3ad
Check for existing jobs before enqueueing new ones
f-f Dec 25, 2025
e524f00
Add E2E test: publishing a package enqueues matrix jobs
f-f Jan 4, 2026
6dc01f0
Add E2E test: run a whole-registry upgrade when detecting a new compiler
f-f Jan 4, 2026
bf90252
Don't fail job fetch on unreadable logs
f-f Jan 4, 2026
cf91c12
Merge branch 'trh/compilers-in-metadata' into f-f/concurrent-jobs-2
thomashoneyman Jan 5, 2026
96bee58
Fix archive seeder build
thomashoneyman Jan 5, 2026
c9bade0
remove effect-4.0.0 from storage in unit tests
thomashoneyman Jan 5, 2026
9ac3531
avoid race condition in initial jobs test
thomashoneyman Jan 5, 2026
4fe219b
format
thomashoneyman Jan 5, 2026
82c6b5a
second test
thomashoneyman Jan 5, 2026
c6fc970
Merge remote-tracking branch 'origin/trh/compilers-in-metadata' into …
thomashoneyman Jan 7, 2026
12baa9a
Merge branch 'trh/compilers-in-metadata' into f-f/concurrent-jobs-2
thomashoneyman Jan 7, 2026
ab31199
Refactor e2e tests with wiremock scenarios (#713)
thomashoneyman Jan 7, 2026
06ff81f
trim tests down a bit to optimize speed to ~60s
thomashoneyman Jan 7, 2026
198ffcd
Add endpoint for package set jobs + e2e tests for it
f-f Jan 7, 2026
31d247b
tweak unpublish test to verify matrix jobs fail gracefully
thomashoneyman Jan 7, 2026
3e278f4
tweak agents to refer to scratch logs
thomashoneyman Jan 7, 2026
f195b37
remove slow archive seeder test
thomashoneyman Jan 8, 2026
de4c19e
fix tests by bumping compiler
thomashoneyman Jan 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions app-e2e/src/Test/E2E/Publish.purs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ spec = do
, ref: "v4.0.0"
, compiler: Utils.unsafeVersion "0.15.9"
, resolutions: Nothing
, version: Utils.unsafeVersion "4.0.0"
}

-- Submit publish request
Expand All @@ -79,6 +80,6 @@ spec = do
Assert.fail $ "Job failed with errors:\n" <> String.joinWith "\n" errorMessages

Assert.shouldSatisfy job.finishedAt isJust
Assert.shouldEqual job.jobType V1.PublishJob
Assert.shouldEqual job.packageName (Utils.unsafePackageName "effect")
Assert.shouldEqual job.ref "v4.0.0"
-- Assert.shouldEqual job.jobType JobType.PublishJob
-- Assert.shouldEqual job.packageName (Utils.unsafePackageName "effect")
-- Assert.shouldEqual job.ref "v4.0.0"
2 changes: 1 addition & 1 deletion app/spago.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package:
name: registry-app
run:
main: Registry.App.Server
main: Registry.App.Main
publish:
license: BSD-3-Clause
version: 0.0.1
Expand Down
2 changes: 1 addition & 1 deletion app/src/App/API.purs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import Data.FoldableWithIndex (foldMapWithIndex)
import Data.List.NonEmpty as NonEmptyList
import Data.Map (SemigroupMap(..))
import Data.Map as Map
import Data.Newtype (over, unwrap)
import Data.Newtype (over)
import Data.Number.Format as Number.Format
import Data.Set as Set
import Data.Set.NonEmpty as NonEmptySet
Expand Down
130 changes: 93 additions & 37 deletions app/src/App/Effect/Db.purs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import Data.String as String
import Registry.API.V1 (JobId, LogLevel, LogLine)
import Registry.App.Effect.Log (LOG)
import Registry.App.Effect.Log as Log
import Registry.App.SQLite (JobResult, NewJob, SQLite)
import Registry.App.SQLite (FinishJob, InsertMatrixJob, InsertPackageJob, InsertPackageSetJob, JobInfo, MatrixJobDetails, PackageJobDetails, PackageSetJobDetails, SQLite, StartJob)
import Registry.App.SQLite as SQLite
import Run (EFFECT, Run)
import Run as Run
import Run.Except (EXCEPT)
import Run.Except as Except

-- We could separate these by database if it grows too large. Also, for now these
-- simply lift their Effect-based equivalents in the SQLite module, but ideally
Expand All @@ -21,13 +23,20 @@ import Run as Run
-- Also, this does not currently include setup and teardown (those are handled
-- outside the effect), but we may wish to add those in the future if they'll
-- be part of app code we want to test.

data Db a
= InsertLog LogLine a
= InsertPackageJob InsertPackageJob a
| InsertMatrixJob InsertMatrixJob a
| InsertPackageSetJob InsertPackageSetJob a
| FinishJob FinishJob a
| StartJob StartJob a
| SelectJobInfo JobId (Either String (Maybe JobInfo) -> a)
| SelectNextPackageJob (Either String (Maybe PackageJobDetails) -> a)
| SelectNextMatrixJob (Either String (Maybe MatrixJobDetails) -> a)
| SelectNextPackageSetJob (Either String (Maybe PackageSetJobDetails) -> a)
| InsertLogLine LogLine a
| SelectLogsByJob JobId LogLevel (Maybe DateTime) (Array LogLine -> a)
| CreateJob NewJob a
| FinishJob JobResult a
| SelectJob JobId (Either String SQLite.Job -> a)
| RunningJobForPackage PackageName (Either String SQLite.Job -> a)
| DeleteIncompleteJobs a

derive instance Functor Db

Expand All @@ -39,28 +48,51 @@ _db = Proxy

-- | Insert a new log line into the database.
insertLog :: forall r. LogLine -> Run (DB + r) Unit
insertLog log = Run.lift _db (InsertLog log unit)
insertLog log = Run.lift _db (InsertLogLine log unit)

-- | Select all logs for a given job, filtered by loglevel and a time cutoff.
-- | Select all logs for a given job, filtered by loglevel.
selectLogsByJob :: forall r. JobId -> LogLevel -> Maybe DateTime -> Run (DB + r) (Array LogLine)
selectLogsByJob jobId logLevel since = Run.lift _db (SelectLogsByJob jobId logLevel since identity)

-- | Create a new job in the database.
createJob :: forall r. NewJob -> Run (DB + r) Unit
createJob newJob = Run.lift _db (CreateJob newJob unit)

-- | Set a job in the database to the 'finished' state.
finishJob :: forall r. JobResult -> Run (DB + r) Unit
finishJob jobResult = Run.lift _db (FinishJob jobResult unit)
finishJob :: forall r. FinishJob -> Run (DB + r) Unit
finishJob job = Run.lift _db (FinishJob job unit)

-- | Select a job by ID from the database.
selectJob :: forall r. JobId -> Run (DB + r) (Either String SQLite.Job)
selectJob jobId = Run.lift _db (SelectJob jobId identity)
selectJobInfo :: forall r. JobId -> Run (DB + EXCEPT String + r) (Maybe JobInfo)
selectJobInfo jobId = Run.lift _db (SelectJobInfo jobId identity) >>= Except.rethrow

-- | Insert a new package job into the database.
insertPackageJob :: forall r. InsertPackageJob -> Run (DB + r) Unit
insertPackageJob job = Run.lift _db (InsertPackageJob job unit)

-- | Insert a new matrix job into the database.
insertMatrixJob :: forall r. InsertMatrixJob -> Run (DB + r) Unit
insertMatrixJob job = Run.lift _db (InsertMatrixJob job unit)

-- | Insert a new package set job into the database.
insertPackageSetJob :: forall r. InsertPackageSetJob -> Run (DB + r) Unit
insertPackageSetJob job = Run.lift _db (InsertPackageSetJob job unit)

-- | Start a job in the database.
startJob :: forall r. StartJob -> Run (DB + r) Unit
startJob job = Run.lift _db (StartJob job unit)

-- | Select a job by package name from the database, failing if there is no
-- | current job available for that package name.
runningJobForPackage :: forall r. PackageName -> Run (DB + r) (Either String SQLite.Job)
runningJobForPackage name = Run.lift _db (RunningJobForPackage name identity)
-- | Select the next package job from the database.
selectNextPackageJob :: forall r. Run (DB + EXCEPT String + r) (Maybe PackageJobDetails)
selectNextPackageJob = Run.lift _db (SelectNextPackageJob identity) >>= Except.rethrow

-- | Select the next matrix job from the database.
selectNextMatrixJob :: forall r. Run (DB + EXCEPT String + r) (Maybe MatrixJobDetails)
selectNextMatrixJob = Run.lift _db (SelectNextMatrixJob identity) >>= Except.rethrow

-- | Select the next package set job from the database.
selectNextPackageSetJob :: forall r. Run (DB + EXCEPT String + r) (Maybe PackageSetJobDetails)
selectNextPackageSetJob = Run.lift _db (SelectNextPackageSetJob identity) >>= Except.rethrow

-- | Delete all incomplete jobs from the database.
deleteIncompleteJobs :: forall r. Run (DB + r) Unit
deleteIncompleteJobs = Run.lift _db (DeleteIncompleteJobs unit)

interpret :: forall r a. (Db ~> Run r) -> Run (DB + r) a -> Run r a
interpret handler = Run.interpret (Run.on _db handler Run.send)
Expand All @@ -70,28 +102,52 @@ type SQLiteEnv = { db :: SQLite }
-- | Interpret DB by interacting with the SQLite database on disk.
handleSQLite :: forall r a. SQLiteEnv -> Db a -> Run (LOG + EFFECT + r) a
handleSQLite env = case _ of
InsertLog log next -> do
Run.liftEffect $ SQLite.insertLog env.db log
InsertPackageJob job next -> do
Run.liftEffect $ SQLite.insertPackageJob env.db job
pure next

SelectLogsByJob jobId logLevel since reply -> do
logs <- Run.liftEffect $ SQLite.selectLogsByJob env.db jobId logLevel since
unless (Array.null logs.fail) do
Log.warn $ "Some logs are not readable: " <> String.joinWith "\n" logs.fail
pure $ reply logs.success
InsertMatrixJob job next -> do
Run.liftEffect $ SQLite.insertMatrixJob env.db job
pure next

CreateJob newJob next -> do
Run.liftEffect $ SQLite.createJob env.db newJob
InsertPackageSetJob job next -> do
Run.liftEffect $ SQLite.insertPackageSetJob env.db job
pure next

FinishJob jobResult next -> do
Run.liftEffect $ SQLite.finishJob env.db jobResult
FinishJob job next -> do
Run.liftEffect $ SQLite.finishJob env.db job
pure next

SelectJob jobId reply -> do
job <- Run.liftEffect $ SQLite.selectJob env.db jobId
pure $ reply job
StartJob job next -> do
Run.liftEffect $ SQLite.startJob env.db job
pure next

SelectJobInfo jobId reply -> do
result <- Run.liftEffect $ SQLite.selectJobInfo env.db jobId
pure $ reply result

SelectNextPackageJob reply -> do
result <- Run.liftEffect $ SQLite.selectNextPackageJob env.db
pure $ reply result

SelectNextMatrixJob reply -> do
result <- Run.liftEffect $ SQLite.selectNextMatrixJob env.db
pure $ reply result

SelectNextPackageSetJob reply -> do
result <- Run.liftEffect $ SQLite.selectNextPackageSetJob env.db
pure $ reply result

RunningJobForPackage name reply -> do
job <- Run.liftEffect $ SQLite.runningJobForPackage env.db name
pure $ reply job
InsertLogLine log next -> do
Run.liftEffect $ SQLite.insertLogLine env.db log
pure next

SelectLogsByJob jobId logLevel since reply -> do
{ fail, success } <- Run.liftEffect $ SQLite.selectLogsByJob env.db jobId logLevel since
unless (Array.null fail) do
Log.warn $ "Some logs are not readable: " <> String.joinWith "\n" fail
pure $ reply success

DeleteIncompleteJobs next -> do
Run.liftEffect $ SQLite.deleteIncompleteJobs env.db
pure next
Comment thread
f-f marked this conversation as resolved.
Outdated
2 changes: 1 addition & 1 deletion app/src/App/Effect/Log.purs
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,5 @@ handleDb env = case _ of
let
msg = Dodo.print Dodo.plainText Dodo.twoSpaces (toLog message)
row = { timestamp, level, jobId: env.job, message: msg }
Run.liftEffect $ SQLite.insertLog env.db row
Run.liftEffect $ SQLite.insertLogLine env.db row
pure next
90 changes: 90 additions & 0 deletions app/src/App/Main.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
module Registry.App.Main where

import Registry.App.Prelude hiding ((/))

import Data.DateTime (diff)
import Data.Time.Duration (Milliseconds(..), Seconds(..))
import Effect.Aff as Aff
import Effect.Class.Console as Console
import Fetch.Retry as Fetch.Retry
import Node.Process as Process
import Registry.App.Server.Env (ServerEnv, createServerEnv)
import Registry.App.Server.JobExecutor as JobExecutor
import Registry.App.Server.Router as Router

main :: Effect Unit
main = do
createServerEnv # Aff.runAff_ case _ of
Left error -> do
Console.log $ "Failed to start server: " <> Aff.message error
Process.exit' 1
Right env -> do
case env.vars.resourceEnv.healthchecksUrl of
Nothing -> Console.log "HEALTHCHECKS_URL not set, healthcheck pinging disabled"
Just healthchecksUrl -> Aff.launchAff_ $ healthcheck healthchecksUrl
Aff.launchAff_ $ jobExecutor env
Router.runRouter env
where
healthcheck :: String -> Aff Unit
healthcheck healthchecksUrl = loop limit
where
limit = 10
oneMinute = Aff.Milliseconds (1000.0 * 60.0)
fiveMinutes = Aff.Milliseconds (1000.0 * 60.0 * 5.0)

loop n = do
Fetch.Retry.withRetryRequest healthchecksUrl {} >>= case _ of
Succeeded { status } | status == 200 -> do
Aff.delay fiveMinutes
loop n

Cancelled | n >= 0 -> do
Console.warn $ "Healthchecks cancelled, will retry..."
Aff.delay oneMinute
loop (n - 1)

Failed error | n >= 0 -> do
Console.warn $ "Healthchecks failed, will retry: " <> Fetch.Retry.printRetryRequestError error
Aff.delay oneMinute
loop (n - 1)

Succeeded { status } | status /= 200, n >= 0 -> do
Console.error $ "Healthchecks returned non-200 status, will retry: " <> show status
Aff.delay oneMinute
loop (n - 1)

Cancelled -> do
Console.error
"Healthchecks cancelled and failure limit reached, will not retry."

Failed error -> do
Console.error $ "Healthchecks failed and failure limit reached, will not retry: " <> Fetch.Retry.printRetryRequestError error

Succeeded _ -> do
Console.error "Healthchecks returned non-200 status and failure limit reached, will not retry."

jobExecutor :: ServerEnv -> Aff Unit
jobExecutor env = do
loop initialRestartDelay
where
initialRestartDelay = Milliseconds 100.0

loop restartDelay = do
start <- nowUTC
result <- JobExecutor.runJobExecutor env
end <- nowUTC

Console.error case result of
Left error -> "Job executor failed: " <> Aff.message error
Right _ -> "Job executor exited for no reason."

-- This is a heuristic: if the executor keeps crashing immediately, we
-- restart with an exponentially increasing delay, but once the executor
-- had a run longer than a minute, we start over with a small delay.
let
nextRestartDelay
| end `diff` start > Seconds 60.0 = initialRestartDelay
| otherwise = restartDelay <> restartDelay

Aff.delay nextRestartDelay
loop nextRestartDelay
2 changes: 1 addition & 1 deletion app/src/App/Prelude.purs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import Data.List (List) as Extra
import Data.Map (Map) as Extra
import Data.Map as Map
import Data.Maybe (Maybe(..), fromJust, fromMaybe, isJust, isNothing, maybe) as Maybe
import Data.Newtype (class Newtype, un) as Extra
import Data.Newtype (class Newtype, un, unwrap, wrap) as Extra
import Data.Newtype as Newtype
import Data.Nullable (Nullable, toMaybe, toNullable) as Extra
import Data.Set (Set) as Extra
Expand Down
Loading