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

ImmediatePlatform / Immediate.Jobs / 30394116826

28 Jul 2026 07:56PM UTC coverage: 74.382%. First build
30394116826

Pull #46

github

web-flow
Merge a20371fc5 into c324de21a
Pull Request #46: Address PR comments

570 of 684 new or added lines in 10 files covered. (83.33%)

5807 of 7807 relevant lines covered (74.38%)

2.4 hits per line

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

92.68
/src/Immediate.Jobs.Shared/JobBatchModels.cs
1
namespace Immediate.Jobs.Shared;
2

3
/// <summary>An opaque reference to a durable job invocation.</summary>
4
public readonly struct JobHandle : IEquatable<JobHandle>
5
{
6
        /// <summary>Creates a handle for an existing invocation identifier.</summary>
7
        public JobHandle(string id)
8
        {
9
                ArgumentException.ThrowIfNullOrWhiteSpace(id);
4✔
10
                Id = id;
4✔
11
        }
4✔
12

13
        internal JobHandle(string id, JobBatch? batch)
14
                : this(id) => Batch = batch;
4✔
15

16
        /// <summary>The opaque invocation identifier.</summary>
17
        public string Id { get; }
18

19
        internal JobBatch? Batch { get; }
20

21
        /// <inheritdoc />
22
        public bool Equals(JobHandle other) => string.Equals(Id, other.Id, StringComparison.Ordinal);
4✔
23

24
        /// <inheritdoc />
25
        public override bool Equals(object? obj) => obj is JobHandle other && Equals(other);
×
26

27
        /// <inheritdoc />
28
        public override int GetHashCode() => Id is null ? 0 : StringComparer.Ordinal.GetHashCode(Id);
×
29

30
        /// <summary>Compares two handles by their opaque invocation identifier.</summary>
31
        public static bool operator ==(JobHandle left, JobHandle right) => left.Equals(right);
×
32

33
        /// <summary>Compares two handles by their opaque invocation identifier.</summary>
34
        public static bool operator !=(JobHandle left, JobHandle right) => !left.Equals(right);
×
35

36
        /// <inheritdoc />
37
        public override string ToString() => Id ?? string.Empty;
×
38
}
39

40
/// <summary>An opaque reference to a committed atomic batch.</summary>
41
public sealed record BatchHandle
42
{
43
        /// <summary>Creates a handle for an existing batch identifier.</summary>
44
        public BatchHandle(string id)
4✔
45
        {
46
                ArgumentException.ThrowIfNullOrWhiteSpace(id);
4✔
47
                Id = id;
4✔
48
        }
4✔
49

50
        /// <summary>The opaque batch identifier.</summary>
51
        public string Id { get; }
52
}
53

54
/// <summary>Determines how a continuation evaluates its parents.</summary>
55
public enum ContinuationTrigger
56
{
57
        /// <summary>Run only when every parent succeeds; otherwise cancel the continuation.</summary>
58
        Success,
59

60
        /// <summary>Run only when every parent is terminal and at least one parent failed.</summary>
61
        Failure,
62

63
        /// <summary>Run after every parent reaches any terminal state.</summary>
64
        Complete,
65
}
66

67
/// <summary>Determines how work scheduled by a running job joins its workflow.</summary>
68
public enum ContinuationOptions
69
{
70
        /// <summary>Schedule outside the current batch and do not modify existing continuations.</summary>
71
        Detached,
72

73
        /// <summary>Add the job to the current batch as a parallel branch.</summary>
74
        BesideContinuations,
75

76
        /// <summary>Add the job to the current batch and make current waiters depend on it too.</summary>
77
        BeforeContinuations,
78
}
79

80
/// <summary>The durable lifecycle state of an atomic batch.</summary>
81
public enum BatchState
82
{
83
        /// <summary>At least one member has not reached a terminal state.</summary>
84
        Executing,
85
        /// <summary>Every member succeeded.</summary>
86
        Succeeded,
87
        /// <summary>At least one member failed.</summary>
88
        Failed,
89
        /// <summary>No member failed and at least one member was cancelled.</summary>
90
        Cancelled,
91
}
92

93
/// <summary>A durable atomic-batch header.</summary>
94
public sealed record JobBatchRecord
95
{
96
        /// <summary>The opaque batch identifier.</summary>
97
        public required string Id { get; init; }
98
        /// <summary>UTC time at which the batch was created.</summary>
99
        public required DateTimeOffset CreatedAt { get; init; }
100
        /// <summary>Total members added to the batch.</summary>
101
        public required int TotalJobs { get; init; }
102
        /// <summary>Members that have not reached a terminal state.</summary>
103
        public required int PendingCount { get; init; }
104
        /// <summary>Members that completed successfully.</summary>
105
        public int SucceededCount { get; init; }
106
        /// <summary>Members that exhausted their attempts.</summary>
107
        public int FailedCount { get; init; }
108
        /// <summary>Members cancelled by an explicit action or dependency violation.</summary>
109
        public int CancelledCount { get; init; }
110
        /// <summary>UTC time at which the first member was acquired.</summary>
111
        public DateTimeOffset? StartedAt { get; init; }
112
        /// <summary>UTC terminal completion time.</summary>
113
        public DateTimeOffset? CompletedAt { get; init; }
114
        /// <summary>The aggregate lifecycle state.</summary>
115
        public required BatchState State { get; init; }
116
}
117

118
/// <summary>A durable dependency edge from a job or batch to a child job.</summary>
119
public sealed record JobContinuationEdge
120
{
121
        /// <summary>The waiting child invocation.</summary>
122
        public required string ChildJobId { get; init; }
123
        /// <summary>The parent invocation for a job-to-job dependency.</summary>
124
        public string? ParentJobId { get; init; }
125
        /// <summary>The parent batch for a batch-to-job dependency.</summary>
126
        public string? ParentBatchId { get; init; }
127
        /// <summary>The condition under which the edge is satisfied.</summary>
128
        public ContinuationTrigger Trigger { get; init; } = ContinuationTrigger.Success;
129
}
130

131
/// <summary>A continuation buffered during a running job attempt and committed only on success.</summary>
132
public sealed record JobContinuationAddition
133
{
134
        /// <summary>The fully serialized new invocation.</summary>
135
        public required JobRecord Job { get; init; }
136
        /// <summary>How the new invocation joins and modifies the current workflow.</summary>
137
        public required ContinuationOptions Options { get; init; }
138
        /// <summary>The dependency trigger from the running job to the new invocation.</summary>
139
        public ContinuationTrigger Trigger { get; init; } = ContinuationTrigger.Success;
140
}
141

142
/// <summary>Per-attempt buffer used by generated schedulers during job execution.</summary>
143
public sealed class JobExecutionBuffer
144
{
145
        private readonly Lock _gate = new();
4✔
146
        private readonly List<JobContinuationAddition> _additions = [];
4✔
147
        private bool _sealed;
148

149
        internal void Add(JobContinuationAddition addition)
150
        {
4✔
151
                lock (_gate)
152
                {
153
                        if (_sealed)
4✔
154
                                throw new ImmediateJobException("The execution buffer is sealed and cannot accept additional continuations.");
4✔
155
                        _additions.Add(addition);
4✔
156
                }
4✔
157
        }
4✔
158

159
        internal IReadOnlyList<JobContinuationAddition> SealAndSnapshot()
160
        {
4✔
161
                lock (_gate)
162
                {
163
                        if (_sealed)
4✔
NEW
164
                                throw new ImmediateJobException("The execution buffer has already been sealed.");
×
165
                        _sealed = true;
4✔
166
                        return _additions.Count == 0
4✔
167
                                ? Array.Empty<JobContinuationAddition>()
4✔
168
                                : Array.AsReadOnly(_additions.ToArray());
4✔
169
                }
170
        }
4✔
171
}
172

173
/// <summary>Aggregate progress for an atomic batch.</summary>
174
public sealed record BatchStatus(
5✔
175
        string Id,
5✔
176
        BatchState State,
5✔
177
        int Total,
5✔
178
        int Succeeded,
5✔
179
        int Failed,
5✔
180
        int Cancelled,
5✔
181
        int Remaining,
5✔
182
        DateTimeOffset CreatedAt,
5✔
183
        DateTimeOffset? StartedAt,
5✔
184
        DateTimeOffset? CompletedAt,
5✔
185
        double FractionSettled
5✔
186
)
5✔
187
{
188
        /// <summary>Calculates settled progress, treating an empty batch as fully settled.</summary>
189
        public static double CalculateFractionSettled(int total, int remaining) =>
190
                total == 0 ? 1d : (double)(total - remaining) / total;
5✔
191
}
192

193
/// <summary>Filters members returned from a batch.</summary>
194
public sealed record BatchMemberQuery
195
{
196
        /// <summary>Optional lifecycle-state filter.</summary>
197
        public JobState? State { get; init; }
198
        /// <summary>Number of members to skip.</summary>
199
        public int Skip { get; init; }
200
        /// <summary>Maximum members to return.</summary>
201
        public int Take { get; init; } = 100;
4✔
202
}
203

204
/// <summary>Filters batches for dashboard presentation.</summary>
205
public sealed record JobBatchQuery
206
{
207
        /// <summary>Optional aggregate-state filter.</summary>
208
        public BatchState? State { get; init; }
209
        /// <summary>Number of batches to skip.</summary>
210
        public int Skip { get; init; }
211
        /// <summary>Maximum batches to return.</summary>
212
        public int Take { get; init; } = 100;
4✔
213
}
214

215
/// <summary>Monitoring data for one batch member.</summary>
216
public sealed record BatchMemberStatus(
4✔
217
        string JobId,
4✔
218
        string JobName,
4✔
219
        string QueueName,
4✔
220
        JobState State,
4✔
221
        int Attempt,
4✔
222
        DateTimeOffset CreatedAt,
4✔
223
        DateTimeOffset? CompletedAt,
4✔
224
        string? LastError
4✔
225
);
4✔
226

227
/// <summary>A batch dependency graph.</summary>
228
public sealed record BatchGraph(
4✔
229
        string BatchId,
4✔
230
        IReadOnlyList<BatchGraphNode> Nodes,
4✔
231
        IReadOnlyList<BatchGraphEdge> Edges
4✔
232
);
4✔
233

234
/// <summary>A job node in a batch graph.</summary>
235
public sealed record BatchGraphNode(string JobId, string JobName, JobState State);
4✔
236

237
/// <summary>A dependency edge in a batch graph.</summary>
238
public sealed record BatchGraphEdge(
5✔
239
        string ChildJobId,
5✔
240
        string? ParentJobId,
5✔
241
        string? ParentBatchId,
5✔
242
        ContinuationTrigger Trigger
5✔
243
);
5✔
244

245
/// <summary>Monitoring data for one job.</summary>
246
public sealed record JobStatus(
5✔
247
        string JobId,
5✔
248
        string JobName,
5✔
249
        string QueueName,
5✔
250
        JobState State,
5✔
251
        int Attempt,
5✔
252
        int? MaxAttempts,
5✔
253
        DateTimeOffset CreatedAt,
5✔
254
        DateTimeOffset DueAt,
5✔
255
        DateTimeOffset? CompletedAt,
5✔
256
        string? LastError,
5✔
257
        string? BatchId,
5✔
258
        IReadOnlyList<BatchGraphEdge> DependsOn
5✔
259
);
5✔
260

261
/// <summary>Read-only batch monitoring.</summary>
262
public interface IJobBatchMonitor
263
{
264
        /// <summary>Gets aggregate batch progress.</summary>
265
        ValueTask<BatchStatus?> GetStatusAsync(string batchId, CancellationToken cancellationToken = default);
266
        /// <summary>Queries batch members.</summary>
267
        ValueTask<IReadOnlyList<BatchMemberStatus>> QueryMembersAsync(
268
                string batchId,
269
                BatchMemberQuery query,
270
                CancellationToken cancellationToken = default
271
        );
272
        /// <summary>Gets the persisted dependency graph.</summary>
273
        ValueTask<BatchGraph?> GetGraphAsync(string batchId, CancellationToken cancellationToken = default);
274
}
275

276
/// <summary>Read-only single-job monitoring.</summary>
277
public interface IJobMonitor
278
{
279
        /// <summary>Gets one job and its incoming dependencies.</summary>
280
        ValueTask<JobStatus?> GetJobAsync(string jobId, CancellationToken cancellationToken = default);
281
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc