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

ImmediatePlatform / Immediate.Jobs / 30657938276

31 Jul 2026 07:08PM UTC coverage: 84.111% (+0.9%) from 83.228%
30657938276

push

github

web-flow
Retain job execution history (#82)

* Retain job execution history

* Address job retention review feedback

* Bound relational concurrency retries

723 of 843 new or added lines in 13 files covered. (85.77%)

14 existing lines in 4 files now uncovered.

8216 of 9768 relevant lines covered (84.11%)

2.7 hits per line

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

95.12
/src/Immediate.Jobs.Shared/JobExecutionModels.cs
1
namespace Immediate.Jobs.Shared;
2

3
/// <summary>The durable outcome of one acquired job execution.</summary>
4
public enum JobExecutionState
5
{
6
        /// <summary>The execution is currently owned by a worker.</summary>
7
        Active,
8
        /// <summary>The execution completed successfully.</summary>
9
        Succeeded,
10
        /// <summary>The execution ended with an error.</summary>
11
        Failed,
12
        /// <summary>The execution was explicitly cancelled.</summary>
13
        Cancelled,
14
        /// <summary>The worker lease expired before a terminal outcome was recorded.</summary>
15
        Interrupted,
16
}
17

18
/// <summary>Storage-neutral diagnostics retained for one acquired job execution.</summary>
19
public sealed record JobExecutionRecord
20
{
21
        /// <summary>Reconstructs the latest execution available in a legacy job row.</summary>
22
        /// <param name="job">The pre-execution-history job record.</param>
23
        /// <returns>A clearly marked synthetic execution, or <see langword="null"/> when the job was never acquired.</returns>
24
        public static JobExecutionRecord? CreateSynthetic(JobRecord job)
25
        {
26
                ArgumentNullException.ThrowIfNull(job);
1✔
27
                return JobExecutionRecords.CreateSynthetic(job);
1✔
28
        }
29

30
        /// <summary>The owning job identifier.</summary>
31
        public required string JobId { get; init; }
32

33
        /// <summary>The 1-based execution ordinal and ownership-fencing value.</summary>
34
        public required int Attempt { get; init; }
35

36
        /// <summary>The current or terminal execution state.</summary>
37
        public required JobExecutionState State { get; init; }
38

39
        /// <summary>The worker that acquired the execution, when known.</summary>
40
        public string? WorkerId { get; init; }
41

42
        /// <summary>The storage time at which the execution was acquired, when known.</summary>
43
        public DateTimeOffset? AcquiredAt { get; init; }
44

45
        /// <summary>The worker time immediately before handler execution began, when recorded.</summary>
46
        public DateTimeOffset? ExecutionStartedAt { get; init; }
47

48
        /// <summary>The storage time at which the execution reached its terminal state.</summary>
49
        public DateTimeOffset? CompletedAt { get; init; }
50

51
        /// <summary>The execution trace identifier, when an activity was created.</summary>
52
        public string? ExecutionTraceId { get; init; }
53

54
        /// <summary>The execution span identifier, when an activity was created.</summary>
55
        public string? ExecutionSpanId { get; init; }
56

57
        /// <summary>The complete exception text for a failed execution.</summary>
58
        public string? Error { get; init; }
59

60
        /// <summary>Whether this best-effort record was reconstructed from a pre-history job row.</summary>
61
        public bool IsSynthetic { get; init; }
62
}
63

64
/// <summary>Paging and exact-ordinal filters for retained job executions.</summary>
65
public sealed record JobExecutionQuery
66
{
67
        /// <summary>The largest execution-history page returned by a storage provider.</summary>
68
        public const int MaximumTake = 1000;
69

70
        /// <summary>Validates the job identifier, exact ordinal, and paging values.</summary>
71
        public void Validate() => JobExecutionRecords.ValidateQuery(this);
1✔
72

73
        /// <summary>The owning job identifier.</summary>
74
        public required string JobId { get; init; }
75

76
        /// <summary>An exact execution ordinal, or <see langword="null"/> for newest-first history.</summary>
77
        public int? Attempt { get; init; }
78

79
        /// <summary>The number of matching executions to skip.</summary>
80
        public int Skip { get; init; }
81

82
        /// <summary>The maximum number of executions to return.</summary>
83
        public int Take { get; init; } = 100;
5✔
84
}
85

86
internal static class JobExecutionRecords
87
{
88
        internal static void ValidateQuery(JobExecutionQuery query)
89
        {
90
                ArgumentNullException.ThrowIfNull(query);
5✔
91
                ArgumentException.ThrowIfNullOrWhiteSpace(query.JobId, nameof(query));
5✔
92
                if (query.Attempt is { } attempt)
5✔
93
                        ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(attempt, 0, nameof(query));
4✔
94
                ArgumentOutOfRangeException.ThrowIfNegative(query.Skip, nameof(query));
5✔
95
                ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(query.Take, 0, nameof(query));
5✔
96
        }
5✔
97

98
        internal static JobExecutionRecord? CreateSynthetic(JobRecord job)
99
        {
100
                if (job.Attempt <= 0)
5✔
101
                        return null;
4✔
102

103
                var state = job.State switch
5✔
104
                {
5✔
105
                        JobState.Active => JobExecutionState.Active,
5✔
106
                        JobState.Succeeded => JobExecutionState.Succeeded,
5✔
107
                        JobState.Failed => JobExecutionState.Failed,
5✔
NEW
108
                        JobState.Cancelled => JobExecutionState.Cancelled,
×
109
                        JobState.Pending or JobState.Scheduled when job.LastError is not null => JobExecutionState.Failed,
5✔
110
                        JobState.AwaitingContinuation or
5✔
111
                        JobState.AwaitingParameters or
5✔
112
                        JobState.Scheduled or
5✔
113
                        JobState.Pending or
5✔
114
                        JobState.Skipped => JobExecutionState.Interrupted,
5✔
NEW
115
                        _ => throw new ArgumentOutOfRangeException(nameof(job), job.State, "Unknown job state."),
×
116
                };
5✔
117

118
                return new()
5✔
119
                {
5✔
120
                        JobId = job.Id,
5✔
121
                        Attempt = job.Attempt,
5✔
122
                        State = state,
5✔
123
                        WorkerId = job.State == JobState.Active ? job.WorkerId : null,
5✔
124
                        ExecutionStartedAt = job.ExecutionStartedAt,
5✔
125
                        CompletedAt = IsTerminal(job.State) ? job.CompletedAt : null,
5✔
126
                        ExecutionTraceId = job.ExecutionTraceId,
5✔
127
                        ExecutionSpanId = job.ExecutionSpanId,
5✔
128
                        Error = state == JobExecutionState.Failed ? job.LastError : null,
5✔
129
                        IsSynthetic = true,
5✔
130
                };
5✔
131
        }
132

133
        private static bool IsTerminal(JobState state) =>
134
                state is JobState.Succeeded or JobState.Failed or JobState.Cancelled or JobState.Skipped;
5✔
135
}
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