Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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