Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add global named mutex to prevent concurrent use #2669

Merged
merged 2 commits into from
Apr 28, 2021
Merged

Conversation

bording
Copy link
Contributor

@bording bording commented Apr 21, 2021

When using GitVersion.MsBuild in multiple projects, you can get concurrent invocations of the GitVersion app. This can result in exceptions when normalization is run. This PR attempts to fix this problem by introducing a global mutex to ensure only a single instance of GitVersion is running at a time, per working directory.

Description

I have added a named mutex in the GitVersionExecutor.RunGitVersionTool method, using the WorkingDirectory value as the name. Giving it a name along with the Global\ prefix means that the mutex will be shared across all processes on the machine.
This ensures that for a given working directory, only one instance of GitVersion will be executing at a time.

NOTE: Mutex names cannot have a backslash in them, so that is why I am removing them before using it as the name.

Related Issues

#1381
#1031

Motivation and Context

This change is required to prevent the various exceptions that can occur when the normalization process is run concurrently, which is likely to happen when more than one project in a solution is using GitVersion.MsBuild.

These errors include:

  • LibGit2Sharp.LockedFileException: failed to lock file
  • LibGit2Sharp.NameConflictException: failed to write reference

How Has This Been Tested?

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

@jetersen
Copy link
Contributor

@bording a previous PR already did a lot of work: #2410

@bording
Copy link
Contributor Author

bording commented Apr 21, 2021

@jetersen I saw that, but what I'm proposing is much simpler. There's no need for lock files, just need a named mutex to prevent concurrent usage.

@bording
Copy link
Contributor Author

bording commented Apr 21, 2021

I see that there are some build failures, but I think that they are unrelated to my changes. They appear to be related to some sort of cake problem?

I have built the code locally with dotnet build on Windows and Linux, and it works just fine.

@dazinator
Copy link
Member

dazinator commented Apr 21, 2021

Thanks @bording - a named mutex would also be my preferred solution to this, over lock files. File locking is difficult to do correctly with certain file systems throwing edge cases etc. Named mutexes are simpler (specifically designed for this use case) and should work on all supported platforms! So if this fixes the issue that would be a nice step forward imho!

Notes on .net (core / 5) underlying mutex implementation:

On Windows the native objects are used (CreateMutex, etc.), on Linuxes where pthread robust mutexes are supported, they are used, and on OSX file locks are used.

@bording
Copy link
Contributor Author

bording commented Apr 21, 2021

I would not recommend to use named mutex. What I've heard it is not well supported on linux.

On the other hand, I have used these before on both macOS and Linux, and they've worked just fine. If there is a problem with them, then that's a bug to file with Microsoft to fix in .NET.

I don't really see the point of reinventing synchronization primitives when the framework and OS provides what we need here.

@@ -56,8 +57,16 @@ public int Execute(GitVersionOptions gitVersionOptions)

private int RunGitVersionTool(GitVersionOptions gitVersionOptions)
{
var mutexName = gitVersionOptions.WorkingDirectory.Replace("\\", "");
Copy link
Contributor

Choose a reason for hiding this comment

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

If there is no need to make them readable of some sort you could also just hash the path into a guid

@asbjornu
Copy link
Member

Since the build is failing on the Clean step, I wonder whether the mutex may be at fault, causing some sort of file system lock preventing stuff from being deleted?

@bording
Copy link
Contributor Author

bording commented Apr 24, 2021

Since the build is failing on the Clean step, I wonder whether the mutex may be at fault, causing some sort of file system lock preventing stuff from being deleted?

@asbjornu I don't really understand Cake, so I can't really follow what the build log failures mean. To me, it just looks like some unrelated failure.

Can you point me to something that shows what's actually failing and maybe I can try to figure it out?

@asbjornu
Copy link
Member

@gep13 and @arturcic are you able to assist @bording with debugging this?

@arturcic
Copy link
Member

@gep13 and @arturcic are you able to assist @bording with debugging this?

I will try to repro locally

@bording
Copy link
Contributor Author

bording commented Apr 28, 2021

I took another look at the failing build logs, and finally found the error message lurking in there.

I have pushed up another commit that fixes the problem, and all the GitHub Action checks are green now. I see there is an Azure DevOps failure, but are those still relevant?

@arturcic
Copy link
Member

No, Azure Pipelines are failing pretty often. I was considering to switch to only GitHub Actions, but for now keeping Azure Pipelines

@arturcic arturcic merged commit 92f3d5b into GitTools:main Apr 28, 2021
@mergify
Copy link
Contributor

mergify bot commented Apr 28, 2021

Thank you @bording for your contribution!

@arturcic arturcic linked an issue Apr 28, 2021 that may be closed by this pull request
@bording bording deleted the mutex branch April 28, 2021 19:58
@bording
Copy link
Contributor Author

bording commented Apr 28, 2021

@arturcic Thanks for merging this. Do you have an ETA for this being released? We're in the process of converting our repos to GitHub Actions and have been hitting the problem this PR solves quite a lot!

@asbjornu asbjornu modified the milestone: 5.6.9 Apr 28, 2021
@arturcic
Copy link
Member

@bording version 5.6.9 was released

@joaorosado
Copy link

joaorosado commented Apr 30, 2021

Hi,

This looks like it is still causing concurrency problems. Now creating a lock file.

2021-04-30T20:06:29.2006077Z ERROR [04/30/21 20:06:27:40] An unexpected error occurred:
2021-04-30T20:06:29.2006843Z LibGit2Sharp.LockedFileException: failed to create locked file 'C:/agent/_work/1/s/.git/refs/heads/release/11.11.x.lock': The file exists.
2021-04-30T20:06:29.2007433Z
2021-04-30T20:06:29.2007765Z at LibGit2Sharp.Core.Ensure.HandleError(Int32 result)
2021-04-30T20:06:29.2008240Z at LibGit2Sharp.Core.Ensure.ZeroResult(Int32 result)
2021-04-30T20:06:29.2008920Z at LibGit2Sharp.Core.Proxy.git_reference_create(RepositoryHandle repo, String name, ObjectId targetId, Boolean allowOverwrite, String logMessage)
2021-04-30T20:06:29.2009702Z at LibGit2Sharp.ReferenceCollection.Add(String name, ObjectId targetId, String logMessage, Boolean allowOverwrite)
2021-04-30T20:06:29.2010443Z at LibGit2Sharp.ReferenceCollection.Add(String name, String canonicalRefNameOrObjectish, String logMessage, Boolean allowOverwrite)
2021-04-30T20:06:29.2011197Z at LibGit2Sharp.ReferenceCollection.Add(String name, String canonicalRefNameOrObjectish, Boolean allowOverwrite)
2021-04-30T20:06:29.2012217Z at GitVersion.ReferenceCollection.Add(String name, String canonicalRefNameOrObjectish, Boolean allowOverwrite) in D:\a\GitVersion\GitVersion\src\GitVersion.LibGit2Sharp\Git\ReferenceCollection.cs:line 20
2021-04-30T20:06:29.2013470Z at GitVersion.GitPreparer.CreateOrUpdateLocalBranchesFromRemoteTrackingOnes(String remoteName) in D:\a\GitVersion\GitVersion\src\GitVersion.Core\Core\GitPreparer.cs:line 343
2021-04-30T20:06:29.2015907Z at GitVersion.GitPreparer.NormalizeGitDirectory(Boolean noFetch, String currentBranchName, Boolean isDynamicRepository) in D:\a\GitVersion\GitVersion\src\GitVersion.Core\Core\GitPreparer.cs:line 191
2021-04-30T20:06:29.2017103Z at GitVersion.GitPreparer.NormalizeGitDirectory(String targetBranch, Boolean isDynamicRepository) in D:\a\GitVersion\GitVersion\src\GitVersion.Core\Core\GitPreparer.cs:line 147
2021-04-30T20:06:29.2018320Z at GitVersion.GitPreparer.PrepareInternal(Boolean normalizeGitDirectory, String currentBranch, Boolean shouldCleanUpRemotes) in D:\a\GitVersion\GitVersion\src\GitVersion.Core\Core\GitPreparer.cs:line 79
2021-04-30T20:06:29.2019373Z at GitVersion.GitPreparer.Prepare() in D:\a\GitVersion\GitVersion\src\GitVersion.Core\Core\GitPreparer.cs:line 60
2021-04-30T20:06:29.2020347Z at GitVersion.GitVersionCalculateTool.CalculateVersionVariables() in D:\a\GitVersion\GitVersion\src\GitVersion.Core\Core\GitVersionCalculateTool.cs:line 45
2021-04-30T20:06:29.2021892Z at GitVersion.GitVersionExecutor.RunGitVersionTool(GitVersionOptions gitVersionOptions) in D:\a\GitVersion\GitVersion\src\GitVersion.App\GitVersionExecutor.cs:line 70

I updated to 5.6.9 and removed the workaround to enabled the parallelism in msbuild.

Did I miss something?
Tried it in 2 repositories and both failed. One of them is pretty simple with only 2 source projects and 1 test project.
My builds are running on Azure DevOps on a Windows agent. One is on a managed build agent and another is on a cloud agent, so that does not appear to make a difference.

The stack on the other one is a little different, so here it goes:

LibGit2Sharp.LockedFileException: failed to lock file 'D:/a/1/s/.git/refs/heads/pull/80/merge.lock' for writing:
at LibGit2Sharp.Core.Ensure.HandleError(Int32 result)
at LibGit2Sharp.Core.Ensure.ZeroResult(Int32 result)
at LibGit2Sharp.Core.Proxy.git_reference_create(RepositoryHandle repo, String name, ObjectId targetId, Boolean allowOverwrite, String logMessage)
at LibGit2Sharp.ReferenceCollection.Add(String name, ObjectId targetId, String logMessage, Boolean allowOverwrite)
at LibGit2Sharp.ReferenceCollection.Add(String name, String canonicalRefNameOrObjectish, String logMessage, Boolean allowOverwrite)
at LibGit2Sharp.ReferenceCollection.Add(String name, String canonicalRefNameOrObjectish, Boolean allowOverwrite)
at GitVersion.ReferenceCollection.Add(String name, String canonicalRefNameOrObjectish, Boolean allowOverwrite) in D:\a\GitVersion\GitVersion\src\GitVersion.LibGit2Sharp\Git\ReferenceCollection.cs:line 20
at GitVersion.GitPreparer.EnsureLocalBranchExistsForCurrentBranch(IRemote remote, String currentBranch) in D:\a\GitVersion\GitVersion\src\GitVersion.Core\Core\GitPreparer.cs:line 382
at GitVersion.GitPreparer.NormalizeGitDirectory(Boolean noFetch, String currentBranchName, Boolean isDynamicRepository) in D:\a\GitVersion\GitVersion\src\GitVersion.Core\Core\GitPreparer.cs:line 189
at GitVersion.GitPreparer.NormalizeGitDirectory(String targetBranch, Boolean isDynamicRepository) in D:\a\GitVersion\GitVersion\src\GitVersion.Core\Core\GitPreparer.cs:line 147
at GitVersion.GitPreparer.PrepareInternal(Boolean normalizeGitDirectory, String currentBranch, Boolean shouldCleanUpRemotes) in D:\a\GitVersion\GitVersion\src\GitVersion.Core\Core\GitPreparer.cs:line 79
at GitVersion.GitPreparer.Prepare() in D:\a\GitVersion\GitVersion\src\GitVersion.Core\Core\GitPreparer.cs:line 60
at GitVersion.GitVersionCalculateTool.CalculateVersionVariables() in D:\a\GitVersion\GitVersion\src\GitVersion.Core\Core\GitVersionCalculateTool.cs:line 45
at GitVersion.GitVersionExecutor.RunGitVersionTool(GitVersionOptions gitVersionOptions) in D:\a\GitVersion\GitVersion\src\GitVersion.App\GitVersionExecutor.cs:line 70

@bording
Copy link
Contributor Author

bording commented Apr 30, 2021

@joaorosado Hmm, that is a bit surprising, and I'm not quite sure how that might be possible. I've rolled out the update on all of my repos, and it has indeed fixed the problem as I was expecting it to.

I think I would need some way to repro it myself before I could understand how it's not working for you.

@joaorosado
Copy link

joaorosado commented Apr 30, 2021

What value does the gitVersionOptions.WorkingDirectory have?

The command that fails looks like this, but the actual project that fails ("MyProjectName" here) is not always the same:

C:\Users\VssAdministrator.nuget\packages\gitversion.msbuild\5.6.9\tools\GitVersion.MsBuild.targets(9,9): error MSB3073: The command "dotnet --roll-forward Major "C:\Users\VssAdministrator.nuget\packages\gitversion.msbuild\5.6.9\tools\netcoreapp3.1/gitversion.dll" "D:\a\1\s\src\MyProjectName" -output file -outputfile obj\gitversion.json" exited with code 1. [D:\a\1\s\src\MyProjectName\MyProjectName.csproj]

Could it be that gitVersionOptions.WorkingDirectory is different for each project in the solution?
I did a search and in GitVersionTaskExecutor.cs it seems that it comes from task.IntermediateOutputPath.GetFileWriteInfo that sets it from a temp path Path.Combine(Path.GetTempPath(), "GitVersionTask"). and could make it different on multiple parallel executions?

(I don't have much context on these tasks, just did a quick search ..so can be totally off target)

@bording
Copy link
Contributor Author

bording commented Apr 30, 2021

WorkingDirectory should be the root folder of the repo where the .git folder is. If you've got something configured in your solution in a way that is causing that value to be different per project, then yes I would expect to see the behavior you're experiencing because that would mean each instance is acquiring a different mutex.

I'm not sure what you'd be doing in your project to make that happen though.

@joaorosado
Copy link

I don't see anything special on my solution.
The only thing similar would be this line on the Directory.Build.props, but since it's only one at the top level it wouldn't make a different file: <SourceRoot Include="$(MSBuildThisFileDirectory)/"/>

I'll try to build a version of GitVersion locally to attempt to debug.

@joaorosado
Copy link

joaorosado commented Apr 30, 2021

@bording just confirmed and the gitVersionOptions.WorkingDirectory is being called for each project with the folder of the project.
It is read directly from the arguments of that command line I posted above.

WorkingDirectory is the project folder (passed as first argument)
and then OutputFile is "obj\gitversion.json", os it writes to "\ProjectFolder\obj\gitversion.json" ..so it looks correct, but breaks the assumption that the WorkingDirectory is always the top level folder.

As for a repro, I just created a new Console app in visual studio with the default values.
It created the following structure,I just added a default GitVersion.yml and added the same dir to git:

C:\Users\jmr\source\repos\ConsoleApp1
C:\Users\jmr\source\repos\ConsoleApp1\.git
C:\Users\jmr\source\repos\ConsoleApp1\ConsoleApp1.sln
C:\Users\jmr\source\repos\ConsoleApp1\ConsoleApp1
C:\Users\jmr\source\repos\ConsoleApp1\ConsoleApp1\ConsoleApp1.csproj
C:\Users\jmr\source\repos\ConsoleApp1\GitVersion.yml

Added gitversion to the project and compiled inside visual studio:

Result:
image

@bording
Copy link
Contributor Author

bording commented Apr 30, 2021

Something sounds wrong there. Per the documentation, the path parameter, which is what the WorkingDirectory value is supposed to be:

The directory containing .git. If not defined current directory is used. (Must be first argument)

It should not be getting a per-project value for that.

@arturcic Any insight on this? Why would the wrong value be passed in there? Is the documentation wrong? I based this PR on that understanding, and nothing I saw in the code made me expect the documentation was wrong.

@joaorosado
Copy link

By the way the repositoryInfo object does have the correct information at this point:

image

@bording
Copy link
Contributor Author

bording commented May 1, 2021

@joaorosado The thing I want to make sure we avoid is actually creating any LibGit2Sharp types too early, outside of the mutex, to ensure there's no chance of any stale info being used when repository normalization occurs.

I'll take a look at the code path you're showing to see if that looks safe to use.

Otherwise, if WorkingDirectory actually isn't guaranteed to be the working directory of the repo from a git perspective, then another option would be to just make the mutex name something like Global\GitVersion and ensure that there are not two instances running concurrently anywhere on the machine.

I originally chose to use the WorkingDirectory because it seemed like a good value to scope the mutex down, since two instances running against different repos wouldn't conflict.

@dazinator
Copy link
Member

dazinator commented May 1, 2021

Otherwise, if WorkingDirectory actually isn't guaranteed to be the working directory of the repo from a git perspective, then another option would be to just make the mutex name something like Global\GitVersion and ensure that there are not two instances running concurrently anywhere on the machine.

@bording if you go that route it might be nice to detect the build Id provided by the build agent if such thing is available, otherwise falling back to the current branch name, last fallback Global\GitVersion

@bording bording mentioned this pull request May 1, 2021
5 tasks
@bording
Copy link
Contributor Author

bording commented May 1, 2021

Looks like IGitRepositoryInfo.DotGitDirectory should do what I want, and what I thought WorkingDirectory was giving me, without introducing any too-early initialization of the LibGit2Sharp Repository, so here's a follow-up PR: #2676

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.

GitVersionTask Parallel restore issue when multi targeting
7 participants