fix(ci): count the acknowledgement timeout from the start of the server when it is later
A runner that could not reach a server that was down is not punished for it, as for the heartbeat deadline.
- commit
- 5df470be37beaf70d3561ccd6bf0a1b5b5f2f5a8
- author
- brice · · 2026-10-12 18:40 UTC
- parent
- 1769e4f
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +13 -2
M internal/usecase/ci_watch.go +13 -2
| @@ -130,13 +130,24 @@ func (c *CI) lastBeat(j domain.CIJob) time.Time { | |||
| 130 | 130 | func (c *CI) due(j domain.CIJob, now time.Time) bool { | |
| 131 | 131 | switch { | |
| 132 | 132 | case j.Phase == domain.CIPhaseAssigned: | |
| 133 | - | return now.Sub(j.AssignedAt) > c.cfg.AckTimeout | |
| 133 | + | return now.Sub(c.assignedSince(j)) > c.cfg.AckTimeout | |
| 134 | 134 | case j.CancelReason != domain.CICancelNone: | |
| 135 | 135 | return now.Sub(j.CancelRequestedAt) > c.cfg.CancelGrace | |
| 136 | 136 | } | |
| 137 | 137 | return now.Sub(c.lastBeat(j)) > c.cfg.HeartbeatDeadline || now.After(c.deadline(j)) | |
| 138 | 138 | } | |
| 139 | 139 | ||
| 140 | + | // assignedSince is when the acknowledgement of an assignment starts to count: | |
| 141 | + | // its assignment, or the start of this process when that is later, so that a | |
| 142 | + | // runner that could not reach a server that was down is not punished for it | |
| 143 | + | // (the same grace as the heartbeat deadline). | |
| 144 | + | func (c *CI) assignedSince(j domain.CIJob) time.Time { | |
| 145 | + | if c.boot.After(j.AssignedAt) { | |
| 146 | + | return c.boot | |
| 147 | + | } | |
| 148 | + | return j.AssignedAt | |
| 149 | + | } | |
| 150 | + | ||
| 140 | 151 | // deadline is when the job's time budget ends for the server: timeout-minutes | |
| 141 | 152 | // from the assignment, plus the slack that lets the runner's own timeout win. | |
| 142 | 153 | func (c *CI) deadline(j domain.CIJob) time.Time { | |
| @@ -147,7 +158,7 @@ func (c *CI) deadline(j domain.CIJob) time.Time { | |||
| 147 | 158 | // reports whether it changed. | |
| 148 | 159 | func (c *CI) enforce(ctx context.Context, rs *runState, j *domain.CIJob, now time.Time) bool { | |
| 149 | 160 | switch { | |
| 150 | - | case j.Phase == domain.CIPhaseAssigned && now.Sub(j.AssignedAt) > c.cfg.AckTimeout: | |
| 161 | + | case j.Phase == domain.CIPhaseAssigned && now.Sub(c.assignedSince(*j)) > c.cfg.AckTimeout: | |
| 151 | 162 | c.cancelAssignment(ctx, rs, j) | |
| 152 | 163 | return true | |
| 153 | 164 | case j.CancelReason != domain.CICancelNone && now.Sub(j.CancelRequestedAt) > c.cfg.CancelGrace: | |
| @@ -130,13 +130,24 @@ func (c *CI) lastBeat(j domain.CIJob) time.Time { | |||||
| 130 | func (c *CI) due(j domain.CIJob, now time.Time) bool { | 130 | func (c *CI) due(j domain.CIJob, now time.Time) bool { | ||
| 131 | switch { | 131 | switch { | ||
| 132 | case j.Phase == domain.CIPhaseAssigned: | 132 | case j.Phase == domain.CIPhaseAssigned: | ||
| 133 | - | return now.Sub(j.AssignedAt) > c.cfg.AckTimeout | 133 | + | return now.Sub(c.assignedSince(j)) > c.cfg.AckTimeout |
| 134 | case j.CancelReason != domain.CICancelNone: | 134 | case j.CancelReason != domain.CICancelNone: | ||
| 135 | return now.Sub(j.CancelRequestedAt) > c.cfg.CancelGrace | 135 | return now.Sub(j.CancelRequestedAt) > c.cfg.CancelGrace | ||
| 136 | } | 136 | } | ||
| 137 | return now.Sub(c.lastBeat(j)) > c.cfg.HeartbeatDeadline || now.After(c.deadline(j)) | 137 | return now.Sub(c.lastBeat(j)) > c.cfg.HeartbeatDeadline || now.After(c.deadline(j)) | ||
| 138 | } | 138 | } | ||
| 139 | 139 | ||||
| 140 | + | // assignedSince is when the acknowledgement of an assignment starts to count: | |||
| 141 | + | // its assignment, or the start of this process when that is later, so that a | |||
| 142 | + | // runner that could not reach a server that was down is not punished for it | |||
| 143 | + | // (the same grace as the heartbeat deadline). | |||
| 144 | + | func (c *CI) assignedSince(j domain.CIJob) time.Time { | |||
| 145 | + | if c.boot.After(j.AssignedAt) { | |||
| 146 | + | return c.boot | |||
| 147 | + | } | |||
| 148 | + | return j.AssignedAt | |||
| 149 | + | } | |||
| 150 | + | ||||
| 140 | // deadline is when the job's time budget ends for the server: timeout-minutes | 151 | // deadline is when the job's time budget ends for the server: timeout-minutes | ||
| 141 | // from the assignment, plus the slack that lets the runner's own timeout win. | 152 | // from the assignment, plus the slack that lets the runner's own timeout win. | ||
| 142 | func (c *CI) deadline(j domain.CIJob) time.Time { | 153 | func (c *CI) deadline(j domain.CIJob) time.Time { | ||
| @@ -147,7 +158,7 @@ func (c *CI) deadline(j domain.CIJob) time.Time { | |||||
| 147 | // reports whether it changed. | 158 | // reports whether it changed. | ||
| 148 | func (c *CI) enforce(ctx context.Context, rs *runState, j *domain.CIJob, now time.Time) bool { | 159 | func (c *CI) enforce(ctx context.Context, rs *runState, j *domain.CIJob, now time.Time) bool { | ||
| 149 | switch { | 160 | switch { | ||
| 150 | - | case j.Phase == domain.CIPhaseAssigned && now.Sub(j.AssignedAt) > c.cfg.AckTimeout: | 161 | + | case j.Phase == domain.CIPhaseAssigned && now.Sub(c.assignedSince(*j)) > c.cfg.AckTimeout: |
| 151 | c.cancelAssignment(ctx, rs, j) | 162 | c.cancelAssignment(ctx, rs, j) | ||
| 152 | return true | 163 | return true | ||
| 153 | case j.CancelReason != domain.CICancelNone && now.Sub(j.CancelRequestedAt) > c.cfg.CancelGrace: | 164 | case j.CancelReason != domain.CICancelNone && now.Sub(j.CancelRequestedAt) > c.cfg.CancelGrace: | ||