• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jeanlrnt / MiniCron.Core / 20645034988

01 Jan 2026 08:25PM UTC coverage: 86.515% (-0.004%) from 86.519%
20645034988

Pull #58

github

web-flow
Merge 873f9d4b7 into 69c94982a
Pull Request #58: Merge pull request #57 from jeanlrnt/copilot/fix-inmemoryjoblockprovider-issue

63 of 69 branches covered (91.3%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

354 of 413 relevant lines covered (85.71%)

26.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.77
/src/MiniCron.Core/Services/InMemoryJobLockProvider.cs
1
using System.Collections.Concurrent;
2

3
namespace MiniCron.Core.Services;
4

5
/// <summary>
6
/// Simple in-memory job lock provider. Suitable for single-node scenarios or tests.
7
/// Not suitable for multi-process distributed locking.
8
/// </summary>
9
/// <remarks>
10
/// Once <see cref="Dispose"/> is called, this provider should not be used for any operations.
11
/// Attempting to call <see cref="TryAcquireAsync"/> or <see cref="ReleaseAsync"/> after disposal
12
/// will throw <see cref="ObjectDisposedException"/>.
13
/// </remarks>
14
public class InMemoryJobLockProvider : IJobLockProvider, IDisposable
15
{
16
    private readonly ConcurrentDictionary<Guid, DateTimeOffset> _locks = new();
28✔
17
    private volatile bool _disposed;
18

19
    /// <summary>
20
    /// Attempts to acquire a lock for the specified job with a time-to-live (TTL).
21
    /// Returns immediately with the result - does not wait or block if the lock is held.
22
    /// </summary>
23
    /// <param name="jobId">The unique identifier of the job to lock.</param>
24
    /// <param name="ttl">The time-to-live for the lock.</param>
25
    /// <param name="cancellationToken">Token to cancel the acquisition attempt.</param>
26
    /// <returns>
27
    /// True if the lock was successfully acquired; false if the lock is currently held by another execution
28
    /// or if cancellation was requested.
29
    /// </returns>
30
    public Task<bool> TryAcquireAsync(Guid jobId, TimeSpan ttl, CancellationToken cancellationToken)
31
    {
32
        if (_disposed)
39✔
33
        {
34
            throw new ObjectDisposedException(nameof(InMemoryJobLockProvider));
2✔
35
        }
36

37
        if (cancellationToken.IsCancellationRequested)
37✔
38
        {
39
            return Task.FromResult(false);
1✔
40
        }
41

42
        var now = DateTimeOffset.UtcNow;
36✔
43
        var expiry = now.Add(ttl);
36✔
44

45
        // Try to add new lock
46
        if (_locks.TryAdd(jobId, expiry))
36✔
47
        {
48
            return Task.FromResult(true);
27✔
49
        }
50

51
        // If existing lock expired, try to replace it
52
        // Capture a fresh timestamp to improve expiry accuracy and reduce the race window;
53
        // correctness is enforced by TryUpdate's compare-and-swap semantics below.
54
        var nowForExpiry = DateTimeOffset.UtcNow;
9✔
55
        if (_locks.TryGetValue(jobId, out var existingExpiry) && existingExpiry <= nowForExpiry)
9✔
56
        {
57
            // Lock has expired based on the value we observed; TryUpdate will only succeed
58
            // if the lock's expiry is still 'existingExpiry' (i.e., no other thread refreshed it).
59
            var refreshedExpiry = nowForExpiry.Add(ttl);
2✔
60
            if (_locks.TryUpdate(jobId, refreshedExpiry, existingExpiry))
2✔
61
            {
62
                return Task.FromResult(true);
2✔
63
            }
64
        }
65

66
        // Lock is held and valid - return false immediately
67
        return Task.FromResult(false);
7✔
68
    }
69

70
    /// <summary>
71
    /// Releases the lock for the specified job ID.
72
    /// </summary>
73
    /// <param name="jobId">The unique identifier of the job.</param>
74
    /// <returns>A completed task.</returns>
75
    /// <exception cref="ObjectDisposedException">Thrown if this provider has been disposed.</exception>
76
    public Task ReleaseAsync(Guid jobId)
77
    {
78
        if (_disposed)
24✔
79
        {
80
            throw new ObjectDisposedException(nameof(InMemoryJobLockProvider));
2✔
81
        }
82

83
        _locks.TryRemove(jobId, out _);
22✔
84
        
85
        return Task.CompletedTask;
22✔
86
    }
87

88
    /// <summary>
89
    /// Disposes the provider and clears all locks. This method is idempotent.
90
    /// After disposal, all operations on this provider will throw <see cref="ObjectDisposedException"/>.
91
    /// </summary>
92
    public void Dispose()
93
    {
94
        if (_disposed)
3✔
95
        {
UNCOV
96
            return;
×
97
        }
98
        _disposed = true;
3✔
99
        _locks.Clear();
3✔
100
    }
3✔
101
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc