Skip to content

Commit 3b28998

Browse files
authored
Prevent double cloning of git repos when fetching them in parallel (#1291)
1 parent 6d2f796 commit 3b28998

File tree

3 files changed

+60
-16
lines changed

3 files changed

+60
-16
lines changed

src/Spago/Command/Fetch.purs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ import Data.Either as Either
2727
import Data.FunctorWithIndex (mapWithIndex)
2828
import Data.HTTP.Method as Method
2929
import Data.Int as Int
30+
import Data.List as List
3031
import Data.Map as Map
3132
import Data.Newtype (wrap)
3233
import Data.Set as Set
3334
import Data.String (joinWith)
34-
import Data.Traversable (sequence)
35+
import Data.Traversable (sequence, traverse_)
3536
import Effect.Aff as Aff
37+
import Effect.Aff.AVar as AVar
3638
import Effect.Ref as Ref
3739
import Node.Buffer as Buffer
3840
import Node.Encoding as Encoding
@@ -204,11 +206,23 @@ run { packages: packagesRequestedToInstall, ensureRanges, isTest, isRepl } = do
204206
fetchPackagesToLocalCache :: a. Map PackageName Package -> Spago (FetchEnv a) Unit
205207
fetchPackagesToLocalCache packages = do
206208
{ offline } <- ask
209+
-- Before starting to fetch packages we build a Map of AVars to act as locks for each git location.
210+
-- This is so we don't have two threads trying to clone the same repo at the same time.
211+
gitLocks <- liftAff $ map (Map.fromFoldable <<< List.catMaybes) $ for (Map.values packages) case _ of
212+
GitPackage gitPackage -> (Just <<< Tuple gitPackage.git) <$> AVar.new unit
213+
_ -> pure Nothing
207214
parallelise $ packages # Map.toUnfoldable <#> \(Tuple name package) -> do
208215
let localPackageLocation = Config.getPackageLocation name package
209216
-- first of all, we check if we have the package in the local cache. If so, we don't even do the work
210217
unlessM (FS.exists localPackageLocation) case package of
211-
GitPackage gitPackage -> getGitPackageInLocalCache name gitPackage
218+
GitPackage gitPackage -> do
219+
-- for git repos it's a little more involved since cloning them takes a while and we risk race conditions
220+
-- and possibly cloning the same repo multiple times - so we use a lock on the git url to prevent that
221+
let lock = Map.lookup gitPackage.git gitLocks
222+
-- Take the lock, do the git thing, release the lock
223+
liftAff $ AVar.take `traverse_` lock
224+
getGitPackageInLocalCache name gitPackage
225+
liftAff $ AVar.put unit `traverse_` lock
212226
RegistryVersion v -> do
213227
-- if the version comes from the registry then we have a longer list of things to do
214228
let versionString = Registry.Version.print v
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Reading Spago workspace configuration...
2+
3+
✓ Selecting package to build: consumer
4+
5+
Downloading dependencies...
6+
Cloning <library-repo-path>
7+
Building...
8+
purs compile...
9+
purs compile...
10+
purs compile...
11+
purs compile...
12+
purs compile...
13+
Src Lib All
14+
Warnings 0 0 0
15+
Errors 0 0 0
16+
17+
✓ Build succeeded.

test/Spago/Build/Monorepo.purs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import Test.Prelude
55
import Data.Array as Array
66
import Data.String (Pattern(..))
77
import Data.String as String
8+
import Data.String.Regex as Regex
9+
import Data.String.Regex.Flags as Regex.Flags
810
import Effect.Aff (bracket)
911
import Node.Path as Path
1012
import Node.Process as Process
@@ -254,19 +256,20 @@ spec = Spec.describe "monorepo" do
254256

255257
Spec.it "#1208: clones a monorepo only once, even if multiple packages from it are needed" \{ spago, fixture, testCwd } -> do
256258
-- A local file system Git repo to use as a remote for Spago to clone from
257-
let createLibraryRepo = do
258-
let libRepo = Path.concat [ Paths.paths.temp, "spago-1208" ]
259-
whenM (FS.exists libRepo) $ rmRf libRepo
260-
FS.copyTree { src: fixture "monorepo/1208-no-double-cloning/library", dst: libRepo }
261-
git_ libRepo [ "init" ]
262-
git_ libRepo [ "add", "." ]
263-
git_ libRepo [ "config", "--global", "core.longpaths", "true" ]
264-
git_ libRepo [ "config", "user.name", "test-user" ]
265-
git_ libRepo [ "config", "user.email", "test-user@aol.com" ]
266-
git_ libRepo [ "commit", "-m", "Initial commit" ]
267-
git_ libRepo [ "tag", "v1" ]
268-
git_ libRepo [ "tag", "v2" ]
269-
pure libRepo
259+
let
260+
createLibraryRepo = do
261+
let libRepo = Path.concat [ Paths.paths.temp, "spago-1208" ]
262+
whenM (FS.exists libRepo) $ rmRf libRepo
263+
FS.copyTree { src: fixture "monorepo/1208-no-double-cloning/library", dst: libRepo }
264+
git_ libRepo [ "init" ]
265+
git_ libRepo [ "add", "." ]
266+
git_ libRepo [ "config", "--global", "core.longpaths", "true" ]
267+
git_ libRepo [ "config", "user.name", "test-user" ]
268+
git_ libRepo [ "config", "user.email", "test-user@aol.com" ]
269+
git_ libRepo [ "commit", "-m", "Initial commit" ]
270+
git_ libRepo [ "tag", "v1" ]
271+
git_ libRepo [ "tag", "v2" ]
272+
pure libRepo
270273

271274
bracket createLibraryRepo rmRf \libRepo -> do
272275
let
@@ -302,7 +305,11 @@ spec = Spec.describe "monorepo" do
302305
{ stdoutFile: Nothing
303306
, stderrFile: Just $ fixture expectedFixture
304307
, result
305-
, sanitize: String.trim >>> String.replaceAll (String.Pattern libRepo) (String.Replacement "<library-repo-path>")
308+
, sanitize:
309+
String.replaceAll (String.Pattern "\r\n") (String.Replacement "\n")
310+
>>> String.replaceAll (String.Pattern libRepo) (String.Replacement "<library-repo-path>")
311+
>>> Regex.replace (unsafeFromRight $ Regex.regex "^purs compile: .*$" (Regex.Flags.global <> Regex.Flags.multiline)) "purs compile..."
312+
>>> String.trim
306313
}
307314

308315
-- First run `spago install` to make sure global cache is populated,
@@ -348,6 +355,12 @@ spec = Spec.describe "monorepo" do
348355
shouldBeSuccessErr' "monorepo/1208-no-double-cloning/expected-stderr/four-deps.txt"
349356
assertRefCheckedOut "lib4" "v4"
350357

358+
-- Lockfile test: when it's up to date but the cache is not populated (i.e. a fresh clone)
359+
-- then there are no double clones. This is a regression test for #1206
360+
spago [ "build" ] >>= shouldBeSuccess
361+
rmRf ".spago"
362+
spago [ "build" ] >>= shouldBeSuccessErr' "monorepo/1208-no-double-cloning/expected-stderr/lockfile-up-to-date.txt"
363+
351364
where
352365
git_ cwd = void <<< git cwd
353366

0 commit comments

Comments
 (0)