< Summary

Information
Class: CounterpointCollective.Threading.NamedLock
Assembly: CounterpointCollective.Threading
File(s): /builds/counterpointcollective/prestoprimitives/Threading/NamedLock.cs
Line coverage
80%
Covered lines: 17
Uncovered lines: 4
Coverable lines: 21
Total lines: 147
Line coverage: 80.9%
Branch coverage
50%
Covered branches: 1
Total branches: 2
Branch coverage: 50%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.ctor(...)100%210%
get_CallerInfo()100%210%
get_DebugInfo()100%210%
.ctor(...)100%11100%
get_LifeTime()100%210%
get_Key()100%11100%
get_CallerInfo()100%11100%
get_DebugInfo()50%22100%
get_Timestamp()100%11100%
Dispose()100%11100%
Describe()100%11100%

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
 11422    public sealed class NamedLock(
 11423        string key,
 11424        NamedLockHandler rkdLockHandler,
 11425        CallerInfo callerInfo,
 11426        Func<string>? fDebugInfo
 11427    ) : IDisposable
 28    {
 29#pragma warning disable CA1034 // Nested types should not be visible
 030        public sealed record Description
 31#pragma warning restore CA1034 // Nested types should not be visible
 32        {
 033            public CallerInfo CallerInfo { get; }
 34
 35            [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
 036            public string? DebugInfo { get; }
 37
 10038            public Description(NamedLock namedLock)
 39            {
 10040                CallerInfo = namedLock.CallerInfo;
 10041                LifeTime = DateTime.Now - namedLock.Timestamp;
 10042                DebugInfo = namedLock.DebugInfo;
 10043            }
 44
 045            public TimeSpan LifeTime { get; }
 46        }
 47
 34248        public string Key { get; private set; } = key;
 49
 21450        public CallerInfo CallerInfo { get; } = callerInfo;
 51
 10052        public string? DebugInfo => fDebugInfo?.Invoke();
 53
 21454        public DateTime Timestamp { get; } = DateTime.Now;
 55
 11256        public void Dispose() => rkdLockHandler.Unlock(this);
 57
 10058        public Description Describe() => new(this);
 59    }
 60
 61    public sealed class NamedLockHandler
 62    {
 63        private readonly record struct LockState(
 64            NamedLock CurrentLock,
 65            BlockingCollection<(NamedLock NamedLock, TaskCompletionSource Tcs)> Queue,
 66            int QueueCount
 67        );
 68
 69        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        {
 80            var callerInfo = new CallerInfo(callerFilePath, callerMemberName, callerLineNumber);
 81            var res = new NamedLock(key, this, callerInfo, fDebugInfo);
 82
 83            //Ensure correct QueueCount atomically, even before we write into the queue.
 84            var v = _locks.AddOrUpdate(key,
 85                _ => new LockState(res, [], 0),
 86                (_, existing) => existing with { QueueCount = existing.QueueCount + 1 }
 87            );
 88
 89            if (v.CurrentLock != res)
 90            {
 91                //We didn't get the lock immediately
 92                var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
 93                using var ctr = cancellationToken.Register(() => tcs.TrySetCanceled());
 94                v.Queue.Add((res, tcs), CancellationToken.None); //Now we actually write into the queue, synching QueueC
 95                await tcs.Task; //May throw OperationCanceledException
 96            }
 97            return res;
 98        }
 99
 100        public void Unlock(NamedLock currLock)
 101        {
 102            var lockState = _locks[currLock.Key];
 103            if (lockState.CurrentLock != currLock)
 104            {
 105                throw new ArgumentException(
 106                    "Illegal locking state. Different current lock holder"
 107                );
 108            }
 109
 110            while (true)
 111            {
 112                if (lockState.QueueCount > 0)
 113                {
 114                    var nextLock = lockState.Queue.Take(); //This may block very shortly when the QueueCount and actual 
 115                    lockState = _locks.Update(
 116                        currLock.Key,
 117                        (_, lockState) => lockState with { CurrentLock = nextLock.NamedLock, QueueCount = lockState.Queu
 118                        lockState
 119                    );
 120                    if (nextLock.Tcs.TrySetResult())
 121                    {
 122                        break;
 123                    }
 124                } else if (_locks.TryRemove(new(currLock.Key, lockState)))
 125                {
 126                    break;
 127                }
 128                lockState = _locks[currLock.Key];
 129            }
 130        }
 131
 132        public bool IsLocked(string key) => _locks.ContainsKey(key);
 133
 134        public (string Name, NamedLock.Description[] Descriptions)[] DescribeLocks() => _locks
 135                .Select(
 136                    kv =>
 137                        (
 138                            kv.Key,
 139                            new[] { kv.Value.CurrentLock.Describe() }
 140                                .Concat(kv.Value.Queue.Select(q => q.NamedLock.Describe()))
 141                                .ToArray()
 142                        )
 143                )
 144                .OrderByDescending(d => d.Item2.First().LifeTime)
 145                .ToArray();
 146    }
 147}