Skip to content

Commit d3fe4f4

Browse files
committed
Reworked FileLock to fit our needs
@ejsmith please reivew this as we can revert if you don't like these changes. I tried to get rid of cases were we were doing extra io calls (https://blogs.msdn.microsoft.com/jaredpar/2009/12/10/the-file-system-is-unpredictable/)
1 parent 711963d commit d3fe4f4

File tree

3 files changed

+147
-162
lines changed

3 files changed

+147
-162
lines changed

src/Exceptionless/Storage/LockBase.cs

Lines changed: 0 additions & 145 deletions
This file was deleted.

src/Exceptionless/Storage/LockFile.cs

Lines changed: 144 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,166 @@
11
#if !PORTABLE && !NETSTANDARD1_2
22
using System;
3+
using System.Collections.Concurrent;
4+
using System.Diagnostics;
35
using System.IO;
6+
using System.Threading;
7+
using Exceptionless.Utility;
48

59
namespace Exceptionless.Storage {
6-
public sealed class LockFile : LockBase<LockFile> {
10+
internal sealed class LockFile : IDisposable {
11+
private static readonly ConcurrentDictionary<string, bool> _lockStatus = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
12+
713
/// <summary>
814
/// Initializes a new instance of the <see cref="LockFile"/> class.
915
/// </summary>
10-
/// <param name="fileName">The file.</param>
11-
public LockFile(string fileName) : base(fileName) {
12-
FileName = fileName;
16+
/// <param name="path"></param>
17+
/// <param name="defaultTimeOutInSeconds"></param>
18+
public LockFile(string path, int defaultTimeOutInSeconds = 30) {
19+
if (String.IsNullOrEmpty(path))
20+
throw new ArgumentNullException(nameof(path));
21+
22+
FileName = path;
23+
DefaultTimeOutInSeconds = defaultTimeOutInSeconds;
1324
}
1425

15-
public override string GetLockFilePath() {
16-
return FileName;
26+
/// <summary>
27+
/// Acquires a lock while waiting with the default timeout value.
28+
/// </summary>
29+
public void AcquireLock() {
30+
AcquireLock(TimeSpan.FromSeconds(DefaultTimeOutInSeconds));
1731
}
1832

1933
/// <summary>
20-
/// The file that is being locked.
34+
/// Acquires a lock in a specific amount of time.
2135
/// </summary>
22-
public string FileName { get; private set; }
36+
/// <param name="timeout">The time to wait for when trying to acquire a lock.</param>
37+
public void AcquireLock(TimeSpan timeout) {
38+
CreateLock(FileName, timeout);
39+
}
2340

2441
/// <summary>
25-
/// Checks to see if the specific path is locked.
42+
/// Acquires a lock in a specific amount of time.
2643
/// </summary>
27-
/// <param name="path"></param>
28-
/// <returns>Returns true if the lock has expired or the lock exists.</returns>
29-
public static bool IsLocked(string path) {
44+
/// <param name="path">The path to acquire a lock on.</param>
45+
/// <param name="timeout">The time to wait for when trying to acquire a lock.</param>
46+
/// <returns>A lock instance.</returns>
47+
public static LockFile Acquire(string path, TimeSpan? timeout = null) {
48+
var lockInstance = new LockFile(path);
49+
lockInstance.AcquireLock(timeout ?? TimeSpan.FromSeconds(lockInstance.DefaultTimeOutInSeconds));
50+
return lockInstance;
51+
}
52+
53+
/// <summary>
54+
/// Creates a lock file.
55+
/// </summary>
56+
/// <param name="path">The place to create the lock file.</param>
57+
/// <param name="timeout">The amount of time to wait before a TimeoutException is thrown.</param>
58+
private void CreateLock(string path, TimeSpan timeout) {
59+
DateTime expire = DateTime.UtcNow.Add(timeout);
60+
61+
Retry:
62+
while (File.Exists(path)) {
63+
if (expire < DateTime.UtcNow)
64+
throw new TimeoutException($"The lock '{path}' timed out.");
65+
66+
if (IsLockExpired(path)) {
67+
Debug.WriteLine($"The lock '{path}' has expired on '{GetCreationTimeUtc(path)}' and wasn't cleaned up properly.");
68+
ReleaseLock();
69+
} else {
70+
Debug.WriteLine($"Waiting for lock: {path}");
71+
Thread.Sleep(500);
72+
}
73+
}
74+
75+
// create file
76+
try {
77+
using (var fs = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None))
78+
fs.Dispose();
79+
80+
_lockStatus[path] = true;
81+
} catch (IOException) {
82+
Debug.WriteLine($"Error creating lock: {path}");
83+
goto Retry;
84+
}
85+
86+
Debug.WriteLine(String.Format($"Created lock: {path}"));
87+
}
88+
89+
/// <summary>
90+
/// Releases the lock.
91+
/// </summary>
92+
public void ReleaseLock() {
93+
RemoveLock(FileName);
94+
}
95+
96+
/// <summary>
97+
/// Releases the lock.
98+
/// </summary>
99+
/// <param name="path">The path to the lock file.</param>
100+
private void RemoveLock(string path) {
101+
Run.WithRetries(() => {
102+
try {
103+
DeleteFile(path);
104+
_lockStatus[path] = false;
105+
Debug.WriteLine($"Deleted lock: {path}");
106+
} catch (Exception) {
107+
Debug.WriteLine($"Error deleting lock: {path}");
108+
throw;
109+
}
110+
}, 5);
111+
}
112+
113+
private void DeleteFile(string path) {
114+
Run.WithRetries(() => {
115+
try {
116+
File.Delete(path);
117+
#if !NETSTANDARD
118+
} catch (DriveNotFoundException) {
119+
#endif
120+
} catch (DirectoryNotFoundException) {
121+
} catch (FileNotFoundException) { }
122+
});
123+
}
124+
125+
private DateTime GetCreationTimeUtc(string path) {
126+
return Run.WithRetries(() => {
127+
try {
128+
return File.GetCreationTimeUtc(path);
129+
#if !NETSTANDARD
130+
} catch (DriveNotFoundException) {
131+
return DateTime.MinValue;
132+
#endif
133+
} catch (DirectoryNotFoundException) {
134+
return DateTime.MinValue;
135+
} catch (FileNotFoundException) {
136+
return DateTime.MinValue;
137+
}
138+
});
139+
}
140+
141+
private bool IsLockExpired(string path) {
30142
if (String.IsNullOrEmpty(path))
31-
return false;
143+
throw new ArgumentNullException(nameof(path));
32144

33-
return !IsLockExpired(path) && File.Exists(path);
145+
return (!_lockStatus.ContainsKey(path) || !_lockStatus[path])
146+
&& GetCreationTimeUtc(path) < DateTime.UtcNow.Subtract(TimeSpan.FromSeconds(DefaultTimeOutInSeconds * 10));
147+
}
148+
149+
/// <summary>
150+
/// The file that is being locked.
151+
/// </summary>
152+
public string FileName { get; }
153+
154+
/// <summary>
155+
/// The default time to wait when trying to acquire a lock.
156+
/// </summary>
157+
public double DefaultTimeOutInSeconds { get; }
158+
159+
/// <summary>
160+
/// Releases the lock.
161+
/// </summary>
162+
public void Dispose() {
163+
ReleaseLock();
34164
}
35165
}
36166
}

tests/Exceptionless.Tests/Storage/LockFileTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ public void AcquireTimeOut() {
2222
Assert.Throws<TimeoutException>(() => LockFile.Acquire("test.lock", TimeSpan.FromSeconds(1)));
2323
}
2424

25-
[Fact(Skip = "Test requires 20 seconds to run.")]
25+
[Fact]
2626
public void Acquire() {
2727
var thread1 = new Thread(s => {
2828
_writer.WriteLine("[Thread: {0}] Lock 1 Entry", Thread.CurrentThread.ManagedThreadId);
2929
using (var lock1 = LockFile.Acquire("Acquire.lock")) {
3030
_writer.WriteLine("[Thread: {0}] Lock 1", Thread.CurrentThread.ManagedThreadId);
31-
Thread.Sleep(TimeSpan.FromSeconds(10));
31+
Thread.Sleep(TimeSpan.FromSeconds(2));
3232
}
3333
});
3434
thread1.Start();
@@ -41,7 +41,7 @@ public void Acquire() {
4141
});
4242

4343
thread2.Start();
44-
Thread.Sleep(TimeSpan.FromSeconds(20));
44+
Thread.Sleep(TimeSpan.FromSeconds(5));
4545
}
4646
}
4747
}

0 commit comments

Comments
 (0)