Skip to content

Commit 220884b

Browse files
Switched to a file lock
1 parent dcec5e0 commit 220884b

File tree

2 files changed

+81
-30
lines changed

2 files changed

+81
-30
lines changed

src/Sentry/Internal/CacheDirectoryCoordinator.cs

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,48 +6,85 @@ namespace Sentry.Internal;
66
internal class CacheDirectoryCoordinator : IDisposable
77
{
88
private readonly IDiagnosticLogger? _logger;
9-
private readonly Semaphore _semaphore;
9+
private readonly object _gate = new();
10+
11+
private FileStream? _lockStream;
12+
private readonly string _lockFilePath;
13+
1014
private bool _acquired;
1115
private bool _disposed;
12-
private readonly Lock _gate = new();
1316

14-
public CacheDirectoryCoordinator(string cacheDir, IDiagnosticLogger? logger)
17+
public CacheDirectoryCoordinator(string cacheDir, IDiagnosticLogger? logger, IFileSystem fileSystem)
1518
{
1619
_logger = logger;
17-
var mutexName = BuildMutexName(cacheDir);
18-
_semaphore = new Semaphore(1, 1, mutexName); // Named mutexes allow interprocess locks on all platforms
19-
}
20+
_lockFilePath = $"{cacheDir}.lock";
2021

21-
private static string BuildMutexName(string path)
22-
{
23-
var hash = path.GetHashString();
24-
// Global\ prefix allows cross-session visibility on Windows Terminal Services
25-
return $"Global\\SentryCache{hash}";
22+
try
23+
{
24+
var baseDir = Path.GetDirectoryName(_lockFilePath);
25+
if (!string.IsNullOrWhiteSpace(baseDir))
26+
{
27+
// Not normally necessary, but just in case
28+
fileSystem.CreateDirectory(baseDir);
29+
}
30+
}
31+
catch (Exception ex)
32+
{
33+
_logger?.LogError("Failed to ensure lock directory exists for cache coordinator.", ex);
34+
}
2635
}
2736

28-
/// <summary>
29-
/// Try to own this cache directory.
30-
/// </summary>
31-
/// <param name="timeout">How long to wait for the lock.</param>
32-
/// <returns>True if acquired; false otherwise.</returns>
3337
public bool TryAcquire(TimeSpan timeout)
3438
{
3539
if (_acquired)
3640
{
3741
return true;
3842
}
43+
3944
lock (_gate)
4045
{
4146
if (_acquired)
4247
{
4348
return true;
4449
}
45-
if (!_semaphore.WaitOne(timeout))
50+
51+
if (_disposed)
4652
{
4753
return false;
4854
}
49-
_acquired = true;
50-
return true;
55+
56+
try
57+
{
58+
// Note that FileShare.None is implemented via advisory locks only on macOS/Linux... so it will stop
59+
// other .NET processes from accessing the file but not other non-.NET processes. This should be fine
60+
// in our case - we just want to avoid multiple instances of the SDK concurrently accessing the cache
61+
_lockStream = new FileStream(
62+
_lockFilePath,
63+
FileMode.OpenOrCreate,
64+
FileAccess.ReadWrite,
65+
FileShare.None);
66+
67+
_acquired = true;
68+
return true;
69+
}
70+
catch (Exception ex)
71+
{
72+
_logger?.LogDebug("Unable to acquire cache directory lock", ex);
73+
}
74+
finally
75+
{
76+
if (!_acquired && _lockStream is not null)
77+
{
78+
try { _lockStream.Dispose(); }
79+
catch
80+
{
81+
// Ignore
82+
}
83+
_lockStream = null;
84+
}
85+
}
86+
87+
return false;
5188
}
5289
}
5390

@@ -56,16 +93,33 @@ public void Dispose()
5693
lock (_gate)
5794
{
5895
if (_disposed)
96+
{
5997
return;
98+
}
6099

61100
_disposed = true;
101+
62102
if (_acquired)
63103
{
64104
try
65-
{ _semaphore.Release(); }
66-
catch (Exception ex) { _logger?.LogError("Error releasing the cache directory semaphore.", ex); }
105+
{
106+
_lockStream?.Close();
107+
}
108+
catch (Exception ex)
109+
{
110+
_logger?.LogError("Error releasing the cache directory file lock.", ex);
111+
}
112+
}
113+
114+
try
115+
{
116+
_lockStream?.Dispose();
117+
}
118+
catch (Exception ex)
119+
{
120+
_logger?.LogError("Error disposing cache lock stream.", ex);
67121
}
68-
_semaphore.Dispose();
122+
_lockStream = null;
69123
}
70124
}
71125
}
@@ -79,7 +133,7 @@ internal static class CacheDirectoryHelper
79133
? null
80134
: Path.Combine(options.CacheDirectoryPath, "Sentry");
81135

82-
internal static string? GetIsolatedFolderName(this SentryOptions options)
136+
private static string? GetIsolatedFolderName(this SentryOptions options)
83137
{
84138
var stringBuilder = new StringBuilder(IsolatedCacheDirectoryPrefix);
85139
#if IOS || ANDROID

src/Sentry/Internal/Http/CachingTransport.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ private CachingTransport(ITransport innerTransport, SentryOptions options, bool
8181
_isolatedCacheDirectoryPath =
8282
options.TryGetIsolatedCacheDirectoryPath() ??
8383
throw new InvalidOperationException("Cache directory or DSN is not set.");
84-
_cacheCoordinator = new CacheDirectoryCoordinator(_isolatedCacheDirectoryPath, options.DiagnosticLogger);
84+
_cacheCoordinator = new CacheDirectoryCoordinator(_isolatedCacheDirectoryPath, options.DiagnosticLogger, options.FileSystem);
8585
if (!_cacheCoordinator.TryAcquire(TimeSpan.FromMilliseconds(100)))
8686
{
8787
throw new InvalidOperationException("Cache directory already locked.");
@@ -99,18 +99,15 @@ private void Initialize(bool startWorker)
9999
// Restore any abandoned files from a previous session
100100
MoveUnprocessedFilesBackToCache();
101101

102-
// Ensure directories exist
103-
_fileSystem.CreateDirectory(_isolatedCacheDirectoryPath);
104102
_fileSystem.CreateDirectory(_processingDirectoryPath);
105103

106-
// Start a worker, if one is needed
107104
if (startWorker)
108105
{
109106
_options.LogDebug("Starting CachingTransport worker.");
110107
_worker = Task.Run(CachedTransportBackgroundTaskAsync);
111108

112109
// Start a recycler in the background so as not to block initialisation
113-
// _recycler = Task.Run(SalvageAbandonedCacheSessions);
110+
_recycler = Task.Run(SalvageAbandonedCacheSessions);
114111
}
115112
else
116113
{
@@ -206,7 +203,7 @@ private void SalvageAbandonedCacheSessions()
206203
}
207204

208205
_options.LogDebug("found abandoned cache: {0}", dir);
209-
using var coordinator = new CacheDirectoryCoordinator(dir, _options.DiagnosticLogger);
206+
using var coordinator = new CacheDirectoryCoordinator(dir, _options.DiagnosticLogger, _options.FileSystem);
210207
if (coordinator.TryAcquire(TimeSpan.FromMilliseconds(100)))
211208
{
212209
_options.LogDebug("Salvaging abandoned cache: {0}", dir);
@@ -324,7 +321,7 @@ private async Task ProcessCacheAsync(CancellationToken cancellation)
324321

325322
// Make sure no files got stuck in the processing directory
326323
// See https://github.com/getsentry/sentry-dotnet/pull/3438#discussion_r1672524426
327-
if (_options.NetworkStatusListener is { Online: false } listener)
324+
if (_options.NetworkStatusListener is { Online: false })
328325
{
329326
MoveUnprocessedFilesBackToCache();
330327
}

0 commit comments

Comments
 (0)