< Summary

Information
Class: CounterpointCollective.Threading.CallerInfo
Assembly: CounterpointCollective.Threading
File(s): /builds/counterpointcollective/prestoprimitives/Threading/NamedLock.cs
Line coverage
55%
Covered lines: 5
Uncovered lines: 4
Coverable lines: 9
Total lines: 147
Line coverage: 55.5%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
get_CallerFilePath()100%210%
get_CallerMemberName()100%210%
get_CalledLineNumber()100%210%
.ctor(...)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{
 07    public sealed record CallerInfo
 8    {
 09        public string? CallerFilePath { get; }
 10
 011        public string? CallerMemberName { get; }
 012        public int CalledLineNumber { get; }
 13
 11414        public CallerInfo(string? callerFilePath, string? callerMemberName, int calledLineNumber)
 15        {
 11416            CallerFilePath = callerFilePath;
 11417            CallerMemberName = callerMemberName;
 11418            CalledLineNumber = calledLineNumber;
 11419        }
 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(
 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}