Skip to content

Commit 24dc9b3

Browse files
add readonly mode, more efficient package sets (#760)
1 parent 724e46b commit 24dc9b3

File tree

6 files changed

+113
-10
lines changed

6 files changed

+113
-10
lines changed

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ SPACES_KEY="digitalocean_spaces_key"
3535
SPACES_SECRET="digitalocean_spaces_secret"
3636

3737

38+
# -----------------------------------------------------------------------------
39+
# Debug / Development Options
40+
# -----------------------------------------------------------------------------
41+
42+
# When "true", the server skips all writes: git push, S3 upload, Pursuit publish.
43+
# Reads and compilations still run normally. Useful for reproducing slow jobs
44+
# locally without affecting the real registry.
45+
# READONLY=true
46+
3847
# -----------------------------------------------------------------------------
3948
# Script-only Secrets (not used by server, used by scripts like legacy-importer)
4049
# -----------------------------------------------------------------------------

app/src/App/Effect/Env.purs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,20 @@ pacchettibottiED25519Pub = EnvKey
281281
pure keyFields.key
282282
}
283283

284+
-- | When set to "true", the server will skip all writes (git push, S3 upload,
285+
-- | Pursuit publish). Reads and compilations still run normally, which is
286+
-- | useful for debugging locally without affecting the real registry.
287+
readOnly :: EnvKey Boolean
288+
readOnly = EnvKey
289+
{ key: "READONLY"
290+
, decode: case _ of
291+
"true" -> Right true
292+
"false" -> Right false
293+
"1" -> Right true
294+
"0" -> Right false
295+
other -> Left $ "Expected 'true' or 'false', got: " <> other
296+
}
297+
284298
-- | A file path to the JSON payload describing the triggered GitHub event.
285299
githubEventPath :: EnvKey FilePath
286300
githubEventPath = EnvKey { key: "GITHUB_EVENT_PATH", decode: pure }

app/src/App/Effect/PackageSets.purs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,26 @@ handle env = case _ of
9191
UpgradeAtomic oldSet@(PackageSet { packages }) compiler changes reply -> reply <$> Except.runExcept do
9292
Log.info $ "Performing atomic upgrade of package set " <> Version.print (un PackageSet oldSet).version
9393

94-
-- It is possible to reuse a workdir when processing package set batches, so
95-
-- we need to clean up before doing work.
96-
for_ [ packagesWorkDir, outputWorkDir, backupWorkDir ] \dir -> do
97-
exists <- Run.liftEffect $ FS.Sync.exists dir
94+
-- Always clean up the backup directory, which is transient and may be
95+
-- left over from a previous crashed job.
96+
do
97+
exists <- Run.liftEffect $ FS.Sync.exists backupWorkDir
9898
when exists do
99-
Log.debug $ "Removing existing working directory " <> dir
100-
FS.Extra.remove dir
99+
Log.debug $ "Removing leftover backup directory " <> backupWorkDir
100+
FS.Extra.remove backupWorkDir
101+
102+
-- Wipe the compiler output if the compiler version has changed, because
103+
-- output from a different compiler version is not safe to reuse.
104+
when (compiler /= (un PackageSet oldSet).compiler) do
105+
outputExists <- Run.liftEffect $ FS.Sync.exists outputWorkDir
106+
when outputExists do
107+
Log.info $ "Compiler version changed, wiping output directory"
108+
FS.Extra.remove outputWorkDir
109+
110+
-- Sync the packages directory: remove any extraneous packages left from
111+
-- a previous job, then install missing ones. This lets the compiler do
112+
-- incremental compilation when resubmitting the same or similar jobs.
113+
syncPackages packages
101114

102115
installPackages packages
103116
compileInstalledPackages compiler >>= case _ of
@@ -183,6 +196,19 @@ handle env = case _ of
183196
backupWorkDir :: FilePath
184197
backupWorkDir = Path.concat [ env.workdir, "output-backup" ]
185198

199+
-- | Remove directories in packages/ that don't correspond to a package
200+
-- | in the target set. This prevents stale packages from being compiled.
201+
syncPackages :: Map PackageName Version -> Run _ Unit
202+
syncPackages targetPackages = do
203+
packagesExist <- Run.liftEffect $ FS.Sync.exists packagesWorkDir
204+
when packagesExist do
205+
existingDirs <- Run.liftAff $ FS.Aff.readdir packagesWorkDir
206+
let extraneous = extraneousPackageDirs targetPackages existingDirs
207+
unless (Array.null extraneous) do
208+
Log.info $ "Removing " <> show (Array.length extraneous) <> " extraneous packages from previous job: " <> String.joinWith ", " extraneous
209+
for_ extraneous \dir ->
210+
FS.Extra.remove (Path.concat [ packagesWorkDir, dir ])
211+
186212
printMissingCompiler version = "Compilation failed because compiler " <> Version.print version <> " is missing."
187213
printUnknownError error = "Compilation failed because of an unknown error: " <> error
188214
printCompilationError errors = "Compilation failed with errors:\n" <> Purs.printCompilerErrors errors
@@ -345,6 +371,13 @@ commitMessage (PackageSet set) accepted newVersion = String.joinWith "\n" $ fold
345371
Tuple packageName version <- removed
346372
pure $ Array.fold [ " - ", formatPackageVersion packageName version ]
347373

374+
-- | Compute directory names in packages/ that don't correspond to any package
375+
-- | in the target set and should be removed before compilation.
376+
extraneousPackageDirs :: Map PackageName Version -> Array String -> Array String
377+
extraneousPackageDirs targetPackages existingDirs = do
378+
let expectedDirs = Set.fromFoldable $ map (\(Tuple name version) -> formatPackageVersion name version) (Map.toUnfoldable targetPackages :: Array _)
379+
Array.filter (\dir -> not (Set.member dir expectedDirs)) existingDirs
380+
348381
-- | Computes new package set version from old package set and version information of successfully added/updated packages.
349382
-- | Note: this must be called with the old `PackageSet` that has not had updates applied.
350383
computeNewVersion :: Version -> PackageSet -> ChangeSet -> Version

app/src/App/Main.purs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ main = createServerEnv # Aff.runAff_ case _ of
1818
Console.log $ "Failed to start server: " <> Aff.message error
1919
Process.exit' 1
2020
Right env -> do
21+
when env.vars.readOnly do
22+
Console.log "READONLY mode enabled: git push, S3 upload, and Pursuit publish are disabled."
2123
case env.vars.resourceEnv.healthchecksUrl of
2224
Nothing -> Console.log "HEALTHCHECKS_URL not set, healthcheck pinging disabled"
2325
Just healthchecksUrl -> Aff.launchAff_ $ healthcheck healthchecksUrl

app/src/App/Server/Env.purs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type ServerEnvVars =
5050
, spacesKey :: String
5151
, spacesSecret :: String
5252
, resourceEnv :: ResourceEnv
53+
, readOnly :: Boolean
5354
}
5455

5556
readServerEnvVars :: Aff ServerEnvVars
@@ -62,7 +63,8 @@ readServerEnvVars = do
6263
spacesKey <- Env.lookupRequired Env.spacesKey
6364
spacesSecret <- Env.lookupRequired Env.spacesSecret
6465
resourceEnv <- Env.lookupResourceEnv
65-
pure { token, publicKey, privateKey, spacesKey, spacesSecret, resourceEnv }
66+
isReadOnly <- Env.lookupWithDefault Env.readOnly false
67+
pure { token, publicKey, privateKey, spacesKey, spacesSecret, resourceEnv, readOnly: isReadOnly }
6668

6769
type ServerEnv =
6870
{ cacheDir :: FilePath
@@ -137,20 +139,24 @@ runEffects env operation = Aff.attempt do
137139
today <- nowUTC
138140
let logFile = String.take 10 (Formatter.DateTime.format Internal.Format.iso8601Date today) <> ".log"
139141
let logPath = Path.concat [ env.logsDir, logFile ]
142+
let
143+
writeMode
144+
| env.vars.readOnly = Registry.ReadOnly
145+
| otherwise = Registry.CommitAs (Git.pacchettibottiCommitter env.vars.token)
140146
operation
141147
# PackageSets.interpret (PackageSets.handle { workdir: scratchDir })
142148
# Registry.interpret
143149
( Registry.handle
144150
{ repos: Registry.defaultRepos
145151
, pull: Git.ForceClean
146-
, write: Registry.CommitAs (Git.pacchettibottiCommitter env.vars.token)
152+
, write: writeMode
147153
, workdir: scratchDir
148154
, debouncer: env.debouncer
149155
, cacheRef: env.registryCacheRef
150156
}
151157
)
152-
# Pursuit.interpret (Pursuit.handleAff env.vars.token)
153-
# Storage.interpret (Storage.handleS3 { s3: { key: env.vars.spacesKey, secret: env.vars.spacesSecret }, cache: env.cacheDir })
158+
# Pursuit.interpret (if env.vars.readOnly then Pursuit.handlePure else Pursuit.handleAff env.vars.token)
159+
# Storage.interpret (if env.vars.readOnly then Storage.handleReadOnly env.cacheDir else Storage.handleS3 { s3: { key: env.vars.spacesKey, secret: env.vars.spacesSecret }, cache: env.cacheDir })
154160
# Source.interpret Source.handle
155161
# GitHub.interpret (GitHub.handle { octokit: env.octokit, cache: env.cacheDir, ref: env.githubCacheRef })
156162
# Cache.interpret _compilerCache (Cache.handleFs env.cacheDir)

app/test/App/Effect/PackageSets.purs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,45 @@ spec = do
9595
-- bar (dependency) must come before foo (dependent)
9696
names `Assert.shouldEqual` [ bar, foo ]
9797

98+
Spec.describe "extraneousPackageDirs" do
99+
Spec.it "Returns empty when packages/ matches the target set" do
100+
let
101+
target = Map.fromFoldable
102+
[ Tuple (Utils.unsafePackageName "aff") (Utils.unsafeVersion "7.0.0")
103+
, Tuple (Utils.unsafePackageName "prelude") (Utils.unsafeVersion "6.0.0")
104+
]
105+
existing = [ "aff@7.0.0", "prelude@6.0.0" ]
106+
PackageSets.extraneousPackageDirs target existing `Assert.shouldEqual` []
107+
108+
Spec.it "Identifies stale packages not in the target set" do
109+
let
110+
target = Map.fromFoldable
111+
[ Tuple (Utils.unsafePackageName "aff") (Utils.unsafeVersion "7.0.0")
112+
]
113+
existing = [ "aff@7.0.0", "stale@1.0.0", "old-pkg@2.0.0" ]
114+
PackageSets.extraneousPackageDirs target existing `Assert.shouldEqual` [ "stale@1.0.0", "old-pkg@2.0.0" ]
115+
116+
Spec.it "Identifies old versions of updated packages as extraneous" do
117+
let
118+
target = Map.fromFoldable
119+
[ Tuple (Utils.unsafePackageName "aff") (Utils.unsafeVersion "8.0.0")
120+
]
121+
existing = [ "aff@7.0.0" ]
122+
PackageSets.extraneousPackageDirs target existing `Assert.shouldEqual` [ "aff@7.0.0" ]
123+
124+
Spec.it "Returns all dirs when target set is empty" do
125+
let
126+
target = Map.empty :: Map PackageName Version
127+
existing = [ "aff@7.0.0", "prelude@6.0.0" ]
128+
PackageSets.extraneousPackageDirs target existing `Assert.shouldEqual` [ "aff@7.0.0", "prelude@6.0.0" ]
129+
130+
Spec.it "Returns empty when packages/ is empty" do
131+
let
132+
target = Map.fromFoldable
133+
[ Tuple (Utils.unsafePackageName "aff") (Utils.unsafeVersion "7.0.0")
134+
]
135+
PackageSets.extraneousPackageDirs target [] `Assert.shouldEqual` []
136+
98137
Spec.it "Processes updates before removals" do
99138
let
100139
foo = Utils.unsafePackageName "foo"

0 commit comments

Comments
 (0)