ci_concurrency.go RawHistoryFolder
1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain
4
5import (
6 "strconv"
7 "unicode"
8 "unicode/utf16"
9)
10
11// CIConcurrencyState is where a run or a job is in its concurrency group.
12type CIConcurrencyState string
13
14// Concurrency states.
15const (
16 // CIConcurrencyIdle: the group is evaluated but the entry has not asked for
17 // its turn yet.
18 CIConcurrencyIdle CIConcurrencyState = ""
19 // CIConcurrencyPending: the group record names this entry as the waiting
20 // one, behind the running entry.
21 CIConcurrencyPending CIConcurrencyState = "pending"
22 // CIConcurrencyHolding: this entry is the running one of its group.
23 CIConcurrencyHolding CIConcurrencyState = "holding"
24 // CIConcurrencyReplaced: only in a mark. A newer entry replaced this one:
25 // it is owed a cancellation and never acquires the group.
26 CIConcurrencyReplaced CIConcurrencyState = "replaced"
27)
28
29// CIMaxConcurrencyGroup is the longest concurrency group name GitHub accepts
30// (actions/runner WorkflowTemplateConverter.cs:679-687, "must be less than 400
31// characters").
32const CIMaxConcurrencyGroup = 400
33
34// CIConcurrency is the evaluated "concurrency:" of a run (workflow level) or of
35// a job.
36type CIConcurrency struct {
37 // Group is the evaluated group as written; "" for none. Groups are compared
38 // without regard to case (ConcurrencyKey).
39 Group string
40 // Cancel is the evaluated cancel-in-progress.
41 Cancel bool
42 // State is written to the record of the entry only after the group record
43 // named it (acquire committed): an entry that waits in a group that does
44 // not name it any more was replaced, and is cancelled.
45 State CIConcurrencyState
46 // Evaluated is set once the expressions were evaluated, whatever the result
47 // (a run starts unevaluated, and a re-run evaluates again).
48 Evaluated bool
49 // FromRun is set on a job that shows the group of its run: the entry is the
50 // run's, the job holds nothing of its own.
51 FromRun bool
52 // Preempted is set when a newer entry of the group cancelled this one.
53 Preempted bool
54}
55
56// ConcurrencyKey is the identity of a group within a repository. GitHub compares
57// groups with .NET's OrdinalIgnoreCase, which compares strings upper-cased
58// character by character with the simple case mapping (OrdinalCasing in
59// dotnet/runtime, System.Private.CoreLib/src/System/Globalization/
60// OrdinalCasing.cs; the source was not available here, so this follows its
61// documented rule). A character that is not ASCII and whose upper case is ASCII
62// keeps itself: ı (U+0131) and ſ (U+017F) are neither I nor S. So σ, ς and Σ are
63// one letter, ı and İ are distinct from I, and i equals I. The key is upper
64// case and is never lowered back.
65func ConcurrencyKey(group string) string {
66 out := make([]rune, 0, len(group))
67 for _, r := range group {
68 if u := unicode.ToUpper(r); r <= unicode.MaxASCII || u > unicode.MaxASCII {
69 r = u
70 }
71 out = append(out, r)
72 }
73 return string(out)
74}
75
76// ConcurrencyGroupLength is the length of a group as C#'s String.Length counts
77// it: in UTF-16 code units.
78func ConcurrencyGroupLength(group string) int { return len(utf16.Encode([]rune(group))) }
79
80// ConcurrencyCancelMessage is GitHub's annotation for an entry cancelled by a
81// newer one (docs.github.com, "Control the concurrency of workflows and jobs").
82func ConcurrencyCancelMessage(group string) string {
83 return "Canceling since a higher priority waiting request for '" + group + "' exists"
84}
85
86// CIConcurrencyEntry is a run (JobID empty) or a job waiting for or holding a
87// concurrency group.
88type CIConcurrencyEntry struct {
89 RunID int64
90 Attempt int32
91 JobID string
92 // Cancel is the cancel-in-progress of the entry: while it waits, the
93 // running entry of the group is cancelled.
94 Cancel bool
95 // Canceling is set on a running entry that a waiting entry with
96 // cancel-in-progress asked to stop; it stays named until it has ended, and
97 // the reconciliation asks it again.
98 Canceling bool
99}
100
101// Key identifies the entry in the marks of its group.
102func (e CIConcurrencyEntry) Key() string {
103 return strconv.FormatInt(e.RunID, 10) + "/" + strconv.FormatInt(int64(e.Attempt), 10) + "/" + e.JobID
104}
105
106// Same reports whether two entries are the same run or job.
107func (e CIConcurrencyEntry) Same(o CIConcurrencyEntry) bool {
108 return e.RunID == o.RunID && e.Attempt == o.Attempt && e.JobID == o.JobID
109}
110
111// CIConcurrencyRecord is the state of a group: at most one running entry and
112// one pending entry (queue: single).
113type CIConcurrencyRecord struct {
114 Running *CIConcurrencyEntry
115 Pending *CIConcurrencyEntry
116}
117
118// IsEmpty reports whether nothing runs or waits in the group.
119func (r CIConcurrencyRecord) IsEmpty() bool { return r.Running == nil && r.Pending == nil }
120
121// Names reports whether the record names the entry as the running or the
122// pending one.
123func (r CIConcurrencyRecord) Names(e CIConcurrencyEntry) bool {
124 return (r.Running != nil && r.Running.Same(e)) || (r.Pending != nil && r.Pending.Same(e))
125}
126
127// CIConcurrencyMark says where an entry stands in its group. The marks of a group
128// are written in the same batch as its record: an entry is marked pending or
129// holding if and only if the record names it as the pending or the running
130// entry, and an entry that a newer one replaced is marked replaced in the batch
131// that unnames it. So a replaced entry is found from the marks alone, whatever
132// failed after that batch.
133type CIConcurrencyMark struct {
134 Entry CIConcurrencyEntry
135 // Group is the group as the newest request wrote it (the cancel annotation
136 // names it).
137 Group string
138 State CIConcurrencyState
139}
140
141// CIConcurrencyGroupState is a group as one transaction sees it: its record and
142// the marks of the entries that belong to it, by CIConcurrencyEntry.Key.
143type CIConcurrencyGroupState struct {
144 Record CIConcurrencyRecord
145 Marks map[string]CIConcurrencyMark
146}
147
148// IsEmpty reports whether the group has no entry and no mark left.
149func (s CIConcurrencyGroupState) IsEmpty() bool { return s.Record.IsEmpty() && len(s.Marks) == 0 }