Skip to content

Lock store creation against concurrent processes#12114

Open
crtschin wants to merge 4 commits into
haskell:masterfrom
crtschin:crtschin/fix-concurrent-store-create
Open

Lock store creation against concurrent processes#12114
crtschin wants to merge 4 commits into
haskell:masterfrom
crtschin:crtschin/fix-concurrent-store-create

Conversation

@crtschin

@crtschin crtschin commented Jul 14, 2026

Copy link
Copy Markdown

Fixes #11329.

Used the hypothesis in #12111 (comment) to create a reproducer. Extracted a helper from the code that used fd locks to make concurrent store additions safe, and use that when initializing the store as well.

Template Α: This PR modifies behaviour or interface

Include the following checklist in your PR:

crtschin added 2 commits July 14, 2026 21:55
When several cabal processes share one `--store-dir` and that store is
cold, they all race `createPackageDBIfMissing`. Precisely hitting the
warning above it, noting that it is not thread-safe.

Fix this by using a fd-based lock, prior to attempting to create the
index.
@Bodigrim

Copy link
Copy Markdown
Collaborator

Thanks a lot for working on this!

I was thinking about the issue in the background and came up with a different approach. Namely,

  • Distribution.Simple.Program.HcPkg.init should read stderr of ghc-pkg and raise alreadyExistsErrorType if ghc-pkg says so.
  • Then createPackageDBIfMissing can catch the error from createPackageDB. If the error isAlreadyExistsError then return success, otherwise rethrow.

Theoretically this is slightly better in a sense that it works regardless on who else is trying to create package DB (which could be an older Cabal version, Stack, Shake, anyone) and there is no need to agree on the name of the lock file, because there is no locking.

But I understand if you prefer your implementation, which is already written :)

@crtschin

Copy link
Copy Markdown
Author

Appreciate you taking a look! Nice idea, taking more of a optimistic approach to avoid locking.

It's probably benign, but I'm not too big of a fan of the idea of logic depending on reading stderr. So I do prefer this as is 😄, but I'm open to changing the PR to your approach though if that's generally preferred.


-- | Hold an exclusive cross-process lock on @lockPath@ for the duration of
-- @action@, creating the lock file if it does not exist.
withFileLock :: Verbosity -> FilePath -> String -> IO a -> IO a

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think withFileLock should go into Distribution.Simple.Utils or another utility module.

Should not we remove the lock file afterwards?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think withFileLock should go into Distribution.Simple.Utils or another utility module.

Will do.

Should not we remove the lock file afterwards?

I think in this case of the store-init, it would be fine. As the check falls back to the existence of the store directory. I haven't taken a good look into withIncomingUnitIdLock to judge if that's safe as well.

I don't think this is safe in the general sense. Deleting the lockfile would disjoint visibility of the lock. Consider the following sequence of events.

- A acquires lock 1.
- B tries to acquire lock 1 and get's blocked.
- A finishes deletes lock 1.
- B proceeds holding lock 1.
- C sees no file, creates a new lockfile and acquires lock 2.
* B and C run concurrently

@crtschin crtschin marked this pull request as ready for review July 14, 2026 21:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Possible race condition when creating .cabal/store/ghc-....

2 participants