< Summary

Information
Class: CounterpointCollective.Threading.NamedLockHandler
Assembly: CounterpointCollective.Threading
File(s): /builds/counterpointcollective/prestoprimitives/Threading/NamedLock.cs
Line coverage
91%
Covered lines: 45
Uncovered lines: 4
Coverable lines: 49
Total lines: 147
Line coverage: 91.8%
Branch coverage
90%
Covered branches: 9
Total branches: 10
Branch coverage: 90%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_CurrentLock()100%11100%
get_Queue()100%11100%
get_QueueCount()100%11100%
.ctor()100%11100%
LockAsync()100%22100%
Unlock(...)87.5%8883.33%
IsLocked(...)100%11100%
DescribeLocks()100%1191.66%

File(s)

/builds/counterpointcollective/prestoprimitives/Threading/NamedLock.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using System.Runtime.CompilerServices;
 3using System.Text.Json.Serialization;
 4
 5namespace CounterpointCollective.Threading
 6{
 7    public sealed record CallerInfo
 8    {
 9        public string? CallerFilePath { get; }
 10
 11        public string? CallerMemberName { get; }
 12        public int CalledLineNumber { get; }
 13
 14        public CallerInfo(string? callerFilePath, string? callerMemberName, int calledLineNumber)
 15        {
 16            CallerFilePath = callerFilePath;
 17            CallerMemberName = callerMemberName;
 18            CalledLineNumber = calledLineNumber;
 19        }
 20    }
 21
 22    public sealed class NamedLock(
 23        string key,
 24        NamedLockHandler rkdLockHandler,
 25        CallerInfo callerInfo,
 26        Func<string>? fDebugInfo
 27    ) : IDisposable
 28    {
 29#pragma warning disable CA1034 // Nested types should not be visible
 30        public sealed record Description
 31#pragma warning restore CA1034 // Nested types should not be visible
 32        {
 33            public CallerInfo CallerInfo { get; }
 34
 35            [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
 36            public string? DebugInfo { get; }
 37
 38            public Description(NamedLock namedLock)
 39            {
 40                CallerInfo = namedLock.CallerInfo;
 41                LifeTime = DateTime.Now - namedLock.Timestamp;
 42                DebugInfo = namedLock.DebugInfo;
 43            }
 44
 45            public TimeSpan LifeTime { get; }
 46        }
 47
 48        public string Key { get; private set; } = key;
 49
 50        public CallerInfo CallerInfo { get; } = callerInfo;
 51
 52        public string? DebugInfo => fDebugInfo?.Invoke();
 53
 54        public DateTime Timestamp { get; } = DateTime.Now;
 55
 56        public void Dispose() => rkdLockHandler.Unlock(this);
 57
 58        public Description Describe() => new(this);
 59    }
 60
 61    public sealed class NamedLockHandler
 62    {
 63        private readonly record struct LockState(
 33264            NamedLock CurrentLock,
 21165            BlockingCollection<(NamedLock NamedLock, TaskCompletionSource Tcs)> Queue,
 53466            int QueueCount
 67        );
 68
 669        private readonly ConcurrentDictionary<string, LockState> _locks = [];
 70
 71        public async Task<NamedLock> LockAsync(
 72            string key,
 73            Func<string>? fDebugInfo = null,
 74            [CallerFilePath] string? callerFilePath = null,
 75            [CallerMemberName] string? callerMemberName = null,
 76            [CallerLineNumber] int callerLineNumber = 0,
 77            CancellationToken cancellationToken = default
 78        )
 79        {
 11480            var callerInfo = new CallerInfo(callerFilePath, callerMemberName, callerLineNumber);
 11481            var res = new NamedLock(key, this, callerInfo, fDebugInfo);
 82
 83            //Ensure correct QueueCount atomically, even before we write into the queue.
 11484            var v = _locks.AddOrUpdate(key,
 985                _ => new LockState(res, [], 0),
 10586                (_, existing) => existing with { QueueCount = existing.QueueCount + 1 }
 11487            );
 88
 11489            if (v.CurrentLock != res)
 90            {
 91                //We didn't get the lock immediately
 10592                var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
 10793                using var ctr = cancellationToken.Register(() => tcs.TrySetCanceled());
 10594                v.Queue.Add((res, tcs), CancellationToken.None); //Now we actually write into the queue, synching QueueC
 10595                await tcs.Task; //May throw OperationCanceledException
 10396            }
 11297            return res;
 11298        }
 99
 100        public void Unlock(NamedLock currLock)
 101        {
 112102            var lockState = _locks[currLock.Key];
 112103            if (lockState.CurrentLock != currLock)
 104            {
 0105                throw new ArgumentException(
 0106                    "Illegal locking state. Different current lock holder"
 0107                );
 108            }
 109
 2110            while (true)
 111            {
 114112                if (lockState.QueueCount > 0)
 113                {
 105114                    var nextLock = lockState.Queue.Take(); //This may block very shortly when the QueueCount and actual 
 105115                    lockState = _locks.Update(
 105116                        currLock.Key,
 105117                        (_, lockState) => lockState with { CurrentLock = nextLock.NamedLock, QueueCount = lockState.Queu
 105118                        lockState
 105119                    );
 105120                    if (nextLock.Tcs.TrySetResult())
 121                    {
 103122                        break;
 123                    }
 9124                } else if (_locks.TryRemove(new(currLock.Key, lockState)))
 125                {
 126                    break;
 127                }
 2128                lockState = _locks[currLock.Key];
 129            }
 9130        }
 131
 10132        public bool IsLocked(string key) => _locks.ContainsKey(key);
 133
 1134        public (string Name, NamedLock.Description[] Descriptions)[] DescribeLocks() => _locks
 1135                .Select(
 1136                    kv =>
 1137                        (
 1138                            kv.Key,
 1139                            new[] { kv.Value.CurrentLock.Describe() }
 99140                                .Concat(kv.Value.Queue.Select(q => q.NamedLock.Describe()))
 1141                                .ToArray()
 1142                        )
 1143                )
 0144                .OrderByDescending(d => d.Item2.First().LifeTime)
 1145                .ToArray();
 146    }
 147}