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

jeanlrnt / MiniCron.Core / 20619965690

31 Dec 2025 01:27PM UTC coverage: 86.519%. First build
20619965690

push

github

web-flow
Merge pull request #26 from jeanlrnt/develop

Develop

64 of 70 branches covered (91.43%)

Branch coverage included in aggregate %.

216 of 269 new or added lines in 8 files covered. (80.3%)

366 of 427 relevant lines covered (85.71%)

25.49 hits per line

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

93.48
/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();
25✔
17
    private volatile bool _disposed;
18
    private readonly SemaphoreSlim _lockReleasedSignal = new(0, 1);
25✔
19

20
    /// <summary>
21
    /// Attempts to acquire a lock for the specified job with a time-to-live (TTL).
22
    /// Uses a combination of event-based signaling and progressive backoff for efficiency.
23
    /// </summary>
24
    /// <param name="jobId">The unique identifier of the job to lock.</param>
25
    /// <param name="ttl">The time-to-live for the lock.</param>
26
    /// <param name="cancellationToken">Token to cancel the acquisition attempt.</param>
27
    /// <returns>
28
    /// True if the lock was successfully acquired; false if the operation was cancelled
29
    /// or the lock could not be acquired within the cancellation period.
30
    /// When cancelled, the method returns false rather than throwing an exception.
31
    /// </returns>
32
    public async Task<bool> TryAcquireAsync(Guid jobId, TimeSpan ttl, CancellationToken cancellationToken)
33
    {
34
        if (_disposed)
30✔
35
        {
36
            throw new ObjectDisposedException(nameof(InMemoryJobLockProvider));
2✔
37
        }
38
        var backoffDelay = 10; // Start with 10ms
28✔
39
        const int maxBackoffDelay = 500; // Cap at 500ms
40

41
        while (!cancellationToken.IsCancellationRequested)
31✔
42
        {
43
            // Check disposed state before each operation to prevent race conditions
44
            if (_disposed)
31✔
45
            {
NEW
46
                throw new ObjectDisposedException(nameof(InMemoryJobLockProvider));
×
47
            }
48

49
            var now = DateTimeOffset.UtcNow;
31✔
50
            var expiry = now.Add(ttl);
31✔
51

52
            // Try add new lock
53
            if (_locks.TryAdd(jobId, expiry))
31✔
54
            {
55
                return true;
25✔
56
            }
57

58
            // If existing lock expired, try to replace it
59
            if (_locks.TryGetValue(jobId, out var existingExpiry))
6✔
60
            {
61
                var refreshedExpiry = DateTimeOffset.UtcNow.Add(ttl);
6✔
62
                if (existingExpiry <= DateTimeOffset.UtcNow
6✔
63
                    && _locks.TryUpdate(jobId, refreshedExpiry, existingExpiry))
6✔
64
                {
65
                    return true;
2✔
66
                }
67
            }
68

69
            // Wait for either a lock release signal or backoff delay
70
            // This combines event-based signaling with progressive backoff
71
            try
72
            {
73
                await _lockReleasedSignal.WaitAsync(backoffDelay, cancellationToken);
4✔
74
            }
3✔
75
            catch (OperationCanceledException)
1✔
76
            {
77
                // Cancellation requested, return false as documented
78
                return false;
1✔
79
            }
80

81
            // Increase backoff delay with integer arithmetic (backoff * 3 / 2), capped at maxBackoffDelay
82
            // Note: Integer division truncates (e.g., 15 * 3 / 2 = 22, truncated from 22.5), providing consistent progression
83
            backoffDelay = Math.Min(backoffDelay * 3 / 2, maxBackoffDelay);
3✔
84
        }
85

NEW
86
        return false;
×
87
    }
28✔
88

89
    /// <summary>
90
    /// Releases the lock for the specified job ID.
91
    /// </summary>
92
    /// <param name="jobId">The unique identifier of the job.</param>
93
    /// <returns>A completed task.</returns>
94
    /// <exception cref="ObjectDisposedException">Thrown if this provider has been disposed.</exception>
95
    public Task ReleaseAsync(Guid jobId)
96
    {
97
        if (_disposed)
24✔
98
        {
99
            throw new ObjectDisposedException(nameof(InMemoryJobLockProvider));
2✔
100
        }
101

102
        _locks.TryRemove(jobId, out _);
22✔
103
        
104
        // Signal waiting threads that a lock has been released
105
        // Use try-catch because concurrent releases may cause SemaphoreFullException
106
        // This is the correct pattern for signaling without race conditions
107
        try
108
        {
109
            _lockReleasedSignal.Release();
22✔
110
        }
19✔
111
        catch (SemaphoreFullException)
3✔
112
        {
113
            // Already at max count (1), no action needed
114
            // This can occur when multiple threads release locks concurrently
115
        }
3✔
116
        
117
        return Task.CompletedTask;
22✔
118
    }
119

120
    /// <summary>
121
    /// Disposes the provider and clears all locks. This method is idempotent.
122
    /// After disposal, all operations on this provider will throw <see cref="ObjectDisposedException"/>.
123
    /// </summary>
124
    public void Dispose()
125
    {
126
        if (_disposed)
3✔
127
        {
NEW
128
            return;
×
129
        }
130
        _disposed = true;
3✔
131
        _locks.Clear();
3✔
132
        _lockReleasedSignal.Dispose();
3✔
133
    }
3✔
134
}
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