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

jeanlrnt / MiniCron.Core / 20644200994

01 Jan 2026 07:22PM UTC coverage: 86.486% (-0.03%) from 86.519%
20644200994

Pull #58

github

web-flow
Merge ce6476fa6 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 %.

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

1 existing line in 1 file now uncovered.

353 of 412 relevant lines covered (85.68%)

26.05 hits per line

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

96.67
/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
        if (_locks.TryGetValue(jobId, out var existingExpiry) && existingExpiry <= now)
9✔
53
        {
54
            // Lock has expired, try to update it
55
            var refreshedExpiry = now.Add(ttl);
2✔
56
            if (_locks.TryUpdate(jobId, refreshedExpiry, existingExpiry))
2✔
57
            {
58
                return Task.FromResult(true);
2✔
59
            }
60
        }
61

62
        // Lock is held and valid - return false immediately
63
        return Task.FromResult(false);
7✔
64
    }
65

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

79
        _locks.TryRemove(jobId, out _);
22✔
80
        
81
        return Task.CompletedTask;
22✔
82
    }
83

84
    /// <summary>
85
    /// Disposes the provider and clears all locks. This method is idempotent.
86
    /// After disposal, all operations on this provider will throw <see cref="ObjectDisposedException"/>.
87
    /// </summary>
88
    public void Dispose()
89
    {
90
        if (_disposed)
3✔
91
        {
UNCOV
92
            return;
×
93
        }
94
        _disposed = true;
3✔
95
        _locks.Clear();
3✔
96
    }
3✔
97
}
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