watchpr: start the conflict window at the arm
The window ran from the first non-mergeable poll even while the rule was disarmed, so the poll that armed it confirmed a run it played no part in. Measure from the later of the run's start and the arm; re-arming stays a no-op so a base moving under every poll still confirms.
This commit is contained in:
+45
-14
@@ -161,20 +161,36 @@ const conflictWindow = 2 * time.Minute
|
||||
// head or base SHA moved. A bare false inherited from the baseline says nothing
|
||||
// -- it is equally a conflict the operator is already waiting on and a recompute
|
||||
// in flight -- so it arms nothing. Arming and the run of falses are independent:
|
||||
// movement only arms, because a base branch that moves under the PR on every
|
||||
// push to main would otherwise restart the run forever.
|
||||
// movement only arms and never resets the run, because a base branch that moves
|
||||
// under the PR on every push to main would otherwise restart it forever. The
|
||||
// window is instead measured from the later of the run's start and the arm, so
|
||||
// an arming poll confirms nothing it has not itself outlasted.
|
||||
type prWatch struct {
|
||||
prev PRState
|
||||
armed bool
|
||||
// armedAt is when the rule armed mid-watch; nil while disarmed, and nil when
|
||||
// the baseline armed it, since then there is no transition to measure from.
|
||||
armedAt *time.Time
|
||||
// conflictSince is when the current unbroken run of non-mergeable polls
|
||||
// began; zero when no run is in progress.
|
||||
conflictSince time.Time
|
||||
// began; nil when no run is in progress.
|
||||
conflictSince *time.Time
|
||||
}
|
||||
|
||||
func newPRWatch(baseline PRState) *prWatch {
|
||||
return &prWatch{prev: baseline, armed: baseline.Mergeable == MergeYes}
|
||||
}
|
||||
|
||||
// arm records the disarmed->armed transition and when it happened. Re-arming is
|
||||
// a no-op, so a base branch moving under every poll advances nothing.
|
||||
func (w *prWatch) arm(now time.Time) {
|
||||
if w.armed {
|
||||
return
|
||||
}
|
||||
w.armed = true
|
||||
at := now
|
||||
w.armedAt = &at
|
||||
}
|
||||
|
||||
// mergeInputsChanged reports whether the commits Gitea merges have moved, which
|
||||
// starts a fresh merge computation whose result is attributable to this watch.
|
||||
func mergeInputsChanged(prev, cur PRState) bool {
|
||||
@@ -187,28 +203,43 @@ func mergeInputsChanged(prev, cur PRState) bool {
|
||||
func (w *prWatch) track(st PRState, now time.Time) {
|
||||
switch st.Mergeable {
|
||||
case MergeNo:
|
||||
if w.conflictSince.IsZero() {
|
||||
w.conflictSince = now
|
||||
if w.conflictSince == nil {
|
||||
at := now
|
||||
w.conflictSince = &at
|
||||
}
|
||||
case MergeYes:
|
||||
w.armed = true
|
||||
w.conflictSince = time.Time{}
|
||||
w.arm(now)
|
||||
w.conflictSince = nil
|
||||
default:
|
||||
w.conflictSince = time.Time{}
|
||||
w.conflictSince = nil
|
||||
}
|
||||
}
|
||||
|
||||
// missed records a poll that never produced a snapshot; the run of adjacent
|
||||
// non-mergeable observations does not survive the gap.
|
||||
func (w *prWatch) missed() {
|
||||
w.conflictSince = time.Time{}
|
||||
w.conflictSince = nil
|
||||
}
|
||||
|
||||
// windowStart is the later of the run's first observation and the arm, so the
|
||||
// window always covers observations this watch can attribute a merge
|
||||
// computation to. A baseline arm records no time and leaves the run governing.
|
||||
func (w *prWatch) windowStart() *time.Time {
|
||||
if w.conflictSince == nil {
|
||||
return nil
|
||||
}
|
||||
if w.armedAt != nil && w.armedAt.After(*w.conflictSince) {
|
||||
return w.armedAt
|
||||
}
|
||||
return w.conflictSince
|
||||
}
|
||||
|
||||
// confirmed reports whether the run of non-mergeable observations has spanned
|
||||
// the recompute window. The run starts at its first observation, so a lone
|
||||
// non-mergeable poll never confirms anything whatever the interval.
|
||||
// the recompute window since it started counting, so neither a lone
|
||||
// non-mergeable poll nor the poll that armed the rule confirms anything.
|
||||
func (w *prWatch) confirmed(now time.Time) bool {
|
||||
return !w.conflictSince.IsZero() && now.Sub(w.conflictSince) >= conflictWindow
|
||||
start := w.windowStart()
|
||||
return start != nil && now.Sub(*start) >= conflictWindow
|
||||
}
|
||||
|
||||
// observe folds in the newest snapshot, taken at now, and reports whether the
|
||||
@@ -216,7 +247,7 @@ func (w *prWatch) confirmed(now time.Time) bool {
|
||||
func (w *prWatch) observe(cur PRState, now time.Time) (bool, string) {
|
||||
changed, reason := MeaningfulChange(w.prev, cur)
|
||||
if mergeInputsChanged(w.prev, cur) {
|
||||
w.armed = true
|
||||
w.arm(now)
|
||||
}
|
||||
w.prev = cur
|
||||
w.track(cur, now)
|
||||
|
||||
Reference in New Issue
Block a user