Skip to content

Commit 31a3653

Browse files
authored
Lock conflicts occur when multiple sites (with ExceptionLess.net) are deployed on a server (#305)
1 parent 3b6047c commit 31a3653

File tree

4 files changed

+93
-20
lines changed

4 files changed

+93
-20
lines changed

src/Exceptionless/Extensions/StringExtensions.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Security.Cryptography;
45
using System.Text;
56

67
namespace Exceptionless.Extensions {
@@ -90,5 +91,25 @@ internal static bool IsValidIdentifier(this string value) {
9091

9192
return true;
9293
}
94+
95+
/// <summary>Compute hash on input string</summary>
96+
/// <param name="input">The string to compute hash on.</param>
97+
/// <param name="algorithm"> </param>
98+
/// <returns>The hash as a hexadecimal String.</returns>
99+
internal static string ComputeHash(this string input, HashAlgorithm algorithm) {
100+
if (String.IsNullOrEmpty(input))
101+
throw new ArgumentNullException(nameof(input));
102+
103+
byte[] data = algorithm.ComputeHash(Encoding.Unicode.GetBytes(input));
104+
105+
return ToHex(data);
106+
}
107+
108+
/// <summary>Compute SHA256 hash on input string</summary>
109+
/// <param name="input">The string to compute hash on.</param>
110+
/// <returns>The hash as a hexadecimal String.</returns>
111+
internal static string ToSHA256(this string input) {
112+
return ComputeHash(input, SHA256.Create());
113+
}
93114
}
94115
}

src/Exceptionless/Logging/FileExceptionlessLog.cs

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
using System;
22
using System.Collections.Concurrent;
33
using System.IO;
4+
using System.Security;
5+
#if NET45
6+
using System.Security.AccessControl;
7+
using System.Security.Principal;
8+
#endif
49
using System.Text;
510
using System.Threading;
11+
using Exceptionless.Extensions;
612
using Exceptionless.Utility;
713

814
namespace Exceptionless.Logging {
915
public class FileExceptionlessLog : IExceptionlessLog, IDisposable {
10-
private static Mutex _flushLock = new Mutex(false, nameof(FileExceptionlessLog));
11-
16+
private Mutex _flushMutex;
1217
private Timer _flushTimer;
1318
private readonly bool _append;
1419
private bool _firstWrite = true;
15-
private bool _isFlushing = false;
16-
private bool _isCheckingFileSize = false;
20+
private bool _isFlushing;
21+
private bool _isCheckingFileSize;
1722

1823
public FileExceptionlessLog(string filePath, bool append = false) {
1924
if (String.IsNullOrEmpty(filePath))
@@ -25,6 +30,7 @@ public FileExceptionlessLog(string filePath, bool append = false) {
2530

2631
Initialize();
2732

33+
_flushMutex = CreateSystemFileMutex(FilePath);
2834
// flush the log every 3 seconds instead of on every write
2935
_flushTimer = new Timer(OnFlushTimer, null, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3));
3036
}
@@ -57,7 +63,7 @@ protected internal virtual string GetFileContents() {
5763
if (File.Exists(FilePath))
5864
return File.ReadAllText(FilePath);
5965
} catch (IOException ex) {
60-
System.Diagnostics.Trace.WriteLine("Exceptionless: Error getting size of file: {0}", ex.Message);
66+
System.Diagnostics.Trace.WriteLine("Exceptionless: Error getting size of file: {0}", ex.ToString());
6167
}
6268

6369
return String.Empty;
@@ -68,7 +74,7 @@ protected internal virtual long GetFileSize() {
6874
if (File.Exists(FilePath))
6975
return new FileInfo(FilePath).Length;
7076
} catch (Exception ex) {
71-
System.Diagnostics.Trace.WriteLine("Exceptionless: Error getting size of file: {0}", ex.Message);
77+
System.Diagnostics.Trace.WriteLine("Exceptionless: Error getting size of file: {0}", ex.ToString());
7278
}
7379

7480
return -1;
@@ -132,7 +138,7 @@ public void Flush() {
132138
_isFlushing = true;
133139

134140
Run.WithRetries(() => {
135-
if (!_flushLock.WaitOne(TimeSpan.FromSeconds(5)))
141+
if (!_flushMutex.WaitOne(TimeSpan.FromSeconds(5)))
136142
return;
137143

138144
hasFlushLock = true;
@@ -155,10 +161,10 @@ public void Flush() {
155161
}
156162
});
157163
} catch (Exception ex) {
158-
System.Diagnostics.Trace.WriteLine("Exceptionless: Error flushing log contents to disk: {0}", ex.Message);
164+
System.Diagnostics.Trace.WriteLine("Exceptionless: Error flushing log contents to disk: {0}", ex.ToString());
159165
} finally {
160166
if (hasFlushLock)
161-
_flushLock.ReleaseMutex();
167+
_flushMutex.ReleaseMutex();
162168
_isFlushing = false;
163169
}
164170
}
@@ -217,15 +223,14 @@ internal void CheckFileSize() {
217223
string lastLines = String.Empty;
218224
try {
219225
Run.WithRetries(() => {
220-
if (!_flushLock.WaitOne(TimeSpan.FromSeconds(5)))
226+
if (!_flushMutex.WaitOne(TimeSpan.FromSeconds(5)))
221227
return;
222228

223-
lastLines = GetLastLinesFromFile(FilePath);
224-
225-
_flushLock.ReleaseMutex();
229+
lastLines = GetLastLinesFromFile();
230+
_flushMutex.ReleaseMutex();
226231
});
227232
} catch (Exception ex) {
228-
System.Diagnostics.Trace.WriteLine("Exceptionless: Error getting last X lines from the log file: {0}", ex.Message);
233+
System.Diagnostics.Trace.WriteLine("Exceptionless: Error getting last X lines from the log file: {0}", ex.ToString());
229234
}
230235

231236
if (String.IsNullOrEmpty(lastLines)) {
@@ -236,16 +241,16 @@ internal void CheckFileSize() {
236241
// overwrite the log file and initialize it with the last X lines it had
237242
try {
238243
Run.WithRetries(() => {
239-
if (!_flushLock.WaitOne(TimeSpan.FromSeconds(5)))
244+
if (!_flushMutex.WaitOne(TimeSpan.FromSeconds(5)))
240245
return;
241246

242247
using (var writer = GetWriter(true))
243248
writer.Value.Write(lastLines);
244249

245-
_flushLock.ReleaseMutex();
250+
_flushMutex.ReleaseMutex();
246251
});
247252
} catch (Exception ex) {
248-
System.Diagnostics.Trace.WriteLine("Exceptionless: Error rewriting the log file after trimming it: {0}", ex.Message);
253+
System.Diagnostics.Trace.WriteLine("Exceptionless: Error rewriting the log file after trimming it: {0}", ex.ToString());
249254
}
250255

251256
_isCheckingFileSize = false;
@@ -263,9 +268,14 @@ public virtual void Dispose() {
263268
}
264269

265270
Flush();
271+
272+
if (_flushMutex != null) {
273+
_flushMutex.Close();
274+
_flushMutex = null;
275+
}
266276
}
267277

268-
protected string GetLastLinesFromFile(string path, int lines = 100) {
278+
protected string GetLastLinesFromFile(int lines = 100) {
269279
byte[] buffer = Encoding.ASCII.GetBytes("\n");
270280

271281
using (var fs = GetReader()) {
@@ -298,6 +308,49 @@ protected string GetLastLinesFromFile(string path, int lines = 100) {
298308
}
299309
}
300310

311+
/// <summary>
312+
/// Allows file based locking across processes
313+
/// </summary>
314+
private Mutex CreateSystemFileMutex(string fileNameOrPath) {
315+
#if NET45
316+
var security = new MutexSecurity();
317+
var allowEveryoneRule = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.FullControl, AccessControlType.Allow);
318+
security.AddAccessRule(allowEveryoneRule);
319+
320+
string name = GetFileBasedMutexName(fileNameOrPath);
321+
322+
try {
323+
return new Mutex(false, name, out bool _, security);
324+
} catch (Exception ex) {
325+
if (ex is SecurityException || ex is UnauthorizedAccessException || ex is NotSupportedException || ex is NotImplementedException) {
326+
System.Diagnostics.Trace.WriteLine("Exceptionless: Error creating global mutex falling back to previous implementation: {0}", ex.ToString());
327+
return new Mutex(false, nameof(FileExceptionlessLog));
328+
}
329+
330+
System.Diagnostics.Trace.WriteLine("Exceptionless: Error creating global mutex: {0}", ex.ToString());
331+
throw;
332+
}
333+
#else
334+
System.Diagnostics.Trace.WriteLine("Exceptionless: This platform does not support taking out a global mutex");
335+
return new Mutex(false, nameof(FileExceptionlessLog));
336+
#endif
337+
}
338+
339+
private string GetFileBasedMutexName(string fileNameOrPath) {
340+
const string prefix = "Global\\Exceptionless-Log-";
341+
string name = fileNameOrPath.ToLowerInvariant().Replace('\\', '_').Replace('/', '_');
342+
343+
const int maxLength = 260;
344+
if ((prefix.Length + name.Length) <= maxLength) {
345+
return $"{prefix}{name}";
346+
}
347+
348+
// If name exceeds max length hash it and append part of the file name.
349+
string hash = name.ToSHA256();
350+
int startIndex = name.Length - (maxLength - prefix.Length - hash.Length);
351+
return $"{prefix}{hash}{name.Substring(startIndex)}";
352+
}
353+
301354
private class LogEntry {
302355
public LogEntry(LogLevel level, string message) {
303356
LogLevel = level;

src/Exceptionless/Logging/IsolatedStorageFileExceptionlessLog.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ protected internal override long GetFileSize() {
5151
if (File.Exists(fullPath))
5252
return new FileInfo(fullPath).Length;
5353
} catch (IOException ex) {
54-
System.Diagnostics.Trace.WriteLine("Exceptionless: Error getting size of file: {0}", ex.Message);
54+
System.Diagnostics.Trace.WriteLine("Exceptionless: Error getting size of file: {0}", ex.ToString());
5555
}
5656
}
5757

test/Exceptionless.Tests/Storage/FileStorageTestsBase.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ public void CanConcurrentlyManageFiles() {
156156
Reset();
157157

158158
var storage = GetStorage();
159-
IJsonSerializer serializer = new DefaultJsonSerializer();
160159
const string queueName = "test";
161160

162161
Parallel.For(0, 25, i => {

0 commit comments

Comments
 (0)