Skip to content

Commit a140be2

Browse files
authored
use named mutex instead of lock to lock cache file (#76)
1 parent 4002b96 commit a140be2

File tree

6 files changed

+161
-17
lines changed

6 files changed

+161
-17
lines changed

Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
33
<PropertyGroup>
4-
<CredentialProviderVersion>0.1.13</CredentialProviderVersion>
4+
<CredentialProviderVersion>0.1.14</CredentialProviderVersion>
55
</PropertyGroup>
66
</Project>

CredentialProvider.Microsoft.Tests/CredentialProviders/VstsBuildTaskServiceEndpoint/VstsBuildTaskServiceEndpointCredentialProviderTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,15 +115,15 @@ public async Task HandleRequestAsync_ReturnsErrorWhenMatchingEndpointIsNotFound(
115115
}
116116

117117
[TestMethod]
118-
public async Task HandleRequestAsync_ThrowsWithInvalidJson()
118+
public void HandleRequestAsync_ThrowsWithInvalidJson()
119119
{
120120
Uri sourceUri = new Uri(@"http://example.pkgs.vsts.me.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json");
121121
string feedEndPointJsonEnvVar = EnvUtil.BuildTaskExternalEndpoints;
122122
string invalidFeedEndPointJson = "this is not json";
123123

124124
Environment.SetEnvironmentVariable(feedEndPointJsonEnvVar, invalidFeedEndPointJson);
125125

126-
Action act = () => vstsCredentialProvider.HandleRequestAsync(new GetAuthenticationCredentialsRequest(sourceUri, false, false, false), CancellationToken.None);
126+
Func<Task> act = async () => await vstsCredentialProvider.HandleRequestAsync(new GetAuthenticationCredentialsRequest(sourceUri, false, false, false), CancellationToken.None);
127127
act.Should().Throw<Exception>();
128128
}
129129
}

CredentialProvider.Microsoft/RequestHandlers/GetAuthenticationCredentialsRequestHandler.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public GetAuthenticationCredentialsRequestHandler(ILogger logger, IReadOnlyColle
3939
public GetAuthenticationCredentialsRequestHandler(ILogger logger, IReadOnlyCollection<ICredentialProvider> credentialProviders)
4040
: this(logger, credentialProviders, null)
4141
{
42-
this.cache = GetSessionTokenCache(logger);
42+
this.cache = GetSessionTokenCache(logger, CancellationToken);
4343
}
4444

4545
public override async Task<GetAuthenticationCredentialsResponse> HandleRequestAsync(GetAuthenticationCredentialsRequest request)
@@ -122,12 +122,12 @@ protected override AutomaticProgressReporter GetProgressReporter(IConnection con
122122
return AutomaticProgressReporter.Create(connection, message, progressReporterTimeSpan, cancellationToken);
123123
}
124124

125-
private static ICache<Uri, string> GetSessionTokenCache(ILogger logger)
125+
private static ICache<Uri, string> GetSessionTokenCache(ILogger logger, CancellationToken cancellationToken)
126126
{
127127
if (EnvUtil.SessionTokenCacheEnabled())
128128
{
129129
logger.Verbose(string.Format(Resources.SessionTokenCacheLocation, EnvUtil.SessionTokenCacheLocation));
130-
return new SessionTokenCache(EnvUtil.SessionTokenCacheLocation, logger);
130+
return new SessionTokenCache(EnvUtil.SessionTokenCacheLocation, logger, cancellationToken);
131131
}
132132

133133
logger.Verbose(Resources.SessionTokenCacheDisabled);

CredentialProvider.Microsoft/Resources.Designer.cs

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CredentialProvider.Microsoft/Resources.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,4 +433,10 @@ Device Flow Authentication Timeout
433433
<data name="TimeElapsedAfterSendingResponse" xml:space="preserve">
434434
<value>Time elapsed in milliseconds after sending response '{0}' '{1}': {2}</value>
435435
</data>
436+
<data name="SessionTokenCacheMutexFail" xml:space="preserve">
437+
<value>Unable to gain a lock on the credential cache.</value>
438+
</data>
439+
<data name="SessionTokenCacheMutexMiss" xml:space="preserve">
440+
<value>Another instance of CredentialProvider is accessing the credential cache, waiting for it to become available.</value>
441+
</data>
436442
</root>

CredentialProvider.Microsoft/Util/SessionTokenCache.cs

Lines changed: 131 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System;
66
using System.Collections.Generic;
77
using System.IO;
8+
using System.Threading;
89
using Newtonsoft.Json;
910
using NuGetCredentialProvider.Logging;
1011

@@ -15,20 +16,63 @@ public class SessionTokenCache : ICache<Uri, string>
1516
private static readonly object FileLock = new object();
1617
private readonly string cacheFilePath;
1718
private ILogger logger;
19+
private CancellationToken cancellationToken;
20+
private readonly string mutexName;
1821

19-
public SessionTokenCache(string cacheFilePath, ILogger logger)
22+
public SessionTokenCache(string cacheFilePath, ILogger logger, CancellationToken cancellationToken)
2023
{
2124
this.cacheFilePath = cacheFilePath;
2225
this.logger = logger;
26+
this.mutexName = @"Global\" + cacheFilePath.Replace(Path.DirectorySeparatorChar, '_');
2327
}
2428

2529
private Dictionary<Uri, string> Cache
2630
{
2731
get
2832
{
29-
lock (FileLock)
33+
bool mutexHeld = false, dummy;
34+
using (Mutex mutex = new Mutex(false, mutexName, out dummy))
3035
{
31-
return Deserialize(ReadFileBytes());
36+
try
37+
{
38+
try
39+
{
40+
if (!mutex.WaitOne(0))
41+
{
42+
// We couldn't get the mutex on our first acquisition attempt. Log this so the user knows what we're
43+
// waiting on.
44+
logger.Verbose(Resources.SessionTokenCacheMutexMiss);
45+
46+
int index = WaitHandle.WaitAny(new WaitHandle[] { mutex, this.cancellationToken.WaitHandle }, -1);
47+
48+
if (index == 1)
49+
{
50+
logger.Verbose(Resources.CancelMessage);
51+
return new Dictionary<Uri, string>();
52+
}
53+
else if (index == WaitHandle.WaitTimeout)
54+
{
55+
logger.Verbose(Resources.SessionTokenCacheMutexFail);
56+
return new Dictionary<Uri, string>();
57+
}
58+
}
59+
}
60+
catch (AbandonedMutexException)
61+
{
62+
// If this is thrown, then we hold the mutex.
63+
}
64+
65+
mutexHeld = true;
66+
67+
return Deserialize(ReadFileBytes());
68+
}
69+
finally
70+
{
71+
if (mutexHeld)
72+
{
73+
mutex.ReleaseMutex();
74+
}
75+
}
3276
}
3377
}
3478
}
@@ -38,11 +82,49 @@ public string this[Uri key]
3882
get => Cache[key];
3983
set
4084
{
41-
lock (FileLock)
85+
bool mutexHeld = false, dummy;
86+
using (Mutex mutex = new Mutex(false, mutexName, out dummy))
4287
{
43-
var cache = Cache;
44-
cache[key] = value;
45-
WriteFileBytes(Serialize(cache));
88+
try
89+
{
90+
try
91+
{
92+
if (!mutex.WaitOne(0))
93+
{
94+
// We couldn't get the mutex on our first acquisition attempt. Log this so the user knows what we're
95+
// waiting on.
96+
logger.Verbose(Resources.SessionTokenCacheMutexMiss);
97+
98+
int index = WaitHandle.WaitAny(new WaitHandle[] { mutex, this.cancellationToken.WaitHandle }, -1);
99+
100+
if (index == 1)
101+
{
102+
logger.Verbose(Resources.CancelMessage);
103+
}
104+
else if (index == WaitHandle.WaitTimeout)
105+
{
106+
logger.Verbose(Resources.SessionTokenCacheMutexFail);
107+
}
108+
}
109+
}
110+
catch (AbandonedMutexException)
111+
{
112+
// If this is thrown, then we hold the mutex.
113+
}
114+
115+
mutexHeld = true;
116+
117+
var cache = Cache;
118+
cache[key] = value;
119+
WriteFileBytes(Serialize(cache));
120+
}
121+
finally
122+
{
123+
if (mutexHeld)
124+
{
125+
mutex.ReleaseMutex();
126+
}
127+
}
46128
}
47129
}
48130
}
@@ -75,11 +157,49 @@ public bool TryGetValue(Uri key, out string value)
75157

76158
public void Remove(Uri key)
77159
{
78-
lock (FileLock)
160+
bool mutexHeld = false, dummy;
161+
using (Mutex mutex = new Mutex(false, mutexName, out dummy))
79162
{
80-
var cache = Cache;
81-
cache.Remove(key);
82-
WriteFileBytes(Serialize(cache));
163+
try
164+
{
165+
try
166+
{
167+
if (!mutex.WaitOne(0))
168+
{
169+
// We couldn't get the mutex on our first acquisition attempt. Log this so the user knows what we're
170+
// waiting on.
171+
logger.Verbose(Resources.SessionTokenCacheMutexMiss);
172+
173+
int index = WaitHandle.WaitAny(new WaitHandle[] { mutex, this.cancellationToken.WaitHandle }, -1);
174+
175+
if (index == 1)
176+
{
177+
logger.Verbose(Resources.CancelMessage);
178+
}
179+
else if (index == WaitHandle.WaitTimeout)
180+
{
181+
logger.Verbose(Resources.SessionTokenCacheMutexFail);
182+
}
183+
}
184+
}
185+
catch (AbandonedMutexException)
186+
{
187+
// If this is thrown, then we hold the mutex.
188+
}
189+
190+
mutexHeld = true;
191+
192+
var cache = Cache;
193+
cache.Remove(key);
194+
WriteFileBytes(Serialize(cache));
195+
}
196+
finally
197+
{
198+
if (mutexHeld)
199+
{
200+
mutex.ReleaseMutex();
201+
}
202+
}
83203
}
84204
}
85205

0 commit comments

Comments
 (0)