|
1 | 1 | #if !PORTABLE && !NETSTANDARD1_2
|
2 | 2 | using System;
|
| 3 | +using System.Collections.Concurrent; |
| 4 | +using System.Diagnostics; |
3 | 5 | using System.IO;
|
| 6 | +using System.Threading; |
| 7 | +using Exceptionless.Utility; |
4 | 8 |
|
5 | 9 | 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 | + |
7 | 13 | /// <summary>
|
8 | 14 | /// Initializes a new instance of the <see cref="LockFile"/> class.
|
9 | 15 | /// </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; |
13 | 24 | }
|
14 | 25 |
|
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)); |
17 | 31 | }
|
18 | 32 |
|
19 | 33 | /// <summary>
|
20 |
| - /// The file that is being locked. |
| 34 | + /// Acquires a lock in a specific amount of time. |
21 | 35 | /// </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 | + } |
23 | 40 |
|
24 | 41 | /// <summary>
|
25 |
| - /// Checks to see if the specific path is locked. |
| 42 | + /// Acquires a lock in a specific amount of time. |
26 | 43 | /// </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) { |
30 | 142 | if (String.IsNullOrEmpty(path))
|
31 |
| - return false; |
| 143 | + throw new ArgumentNullException(nameof(path)); |
32 | 144 |
|
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(); |
34 | 164 | }
|
35 | 165 | }
|
36 | 166 | }
|
|
0 commit comments