chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
A release waits after the deploy and runs on the runner handed over to
.changeset/run-after-deploy-hands-over.mdnew+7−0
| @@ -0,0 +1,7 @@ | ||
--- | ||
default: patch | ||
--- | ||
| ||
# A run waiting after a deploy runs on the runner it hands over to | ||
| ||
A runner stopping keeps a run at the same commit as a deploy going, to run itself, since the deploy may wait for it. A run whose workflow waits `after` that deploy is now dropped instead, and the runner started next runs it once the deploy has passed, rather than the stopping one starting it and cutting it off past `--stop-wait`. | ||
.knotless/workflows/release.yml+7−0
| @@ -11,10 +11,17 @@ | ||
# main alone in the runner's secrets file. Asked for on any other branch, | ||
# `knotless rerun <repo> --ref refs/heads/<branch> --workflow release`, it | ||
# builds everything and publishes nothing, to try the build. | ||
# | ||
# It runs after deploy.yml at its commit. A version bump changes every | ||
# binary, so the deploy hands the runner over to a new one, and the old | ||
# one cuts off what it still runs 15 minutes after (--stop-wait), less | ||
# than this build takes. Waiting after the deploy, it is dropped by the | ||
# old runner and run by the new one. | ||
on: | ||
push: {refs: [refs/heads/main], paths: [Cargo.toml]} | ||
asked: {refs: [refs/heads/*]} | ||
superseded: finish | ||
after: [deploy] | ||
image: rust:1.98 | ||
cache: [target, /usr/local/cargo/registry, /usr/local/rustup, /opt/tools] | ||
secrets: [RELEASE_PASSWORD] | ||
crates/knotless-run/src/lib.rs+19−10
| @@ -1028,8 +1028,8 @@ pub struct Runner { | ||
/// checks' keys: any other of this runner's is left over, and removed. | ||
sessions: Mutex<HashSet<String>>, | ||
/// Where each run going of a workflow whose runs finish is at, as | ||
/// [`Runner::dropping`] names it. | ||
finishing: tokio::sync::watch::Sender<Vec<String>>, | ||
/// [`Runner::dropping`] names it, and its workflow. | ||
finishing: tokio::sync::watch::Sender<Vec<(String, String)>>, | ||
/// The appview runs' output goes to as it is written, and writes in | ||
/// private repositories' spaces come from ([`live`]). | ||
appview: Option<Arc<live::Appview>>, | ||
| @@ -1083,13 +1083,14 @@ impl Drop for Deciding { | ||
/// A run going of a workflow whose runs finish, noted in | ||
/// [`Runner::finishing`] until it is dropped. | ||
struct Finishing<'a> { | ||
finishing: &'a tokio::sync::watch::Sender<Vec<String>>, | ||
of: String, | ||
finishing: &'a tokio::sync::watch::Sender<Vec<(String, String)>>, | ||
of: (String, String), | ||
} | ||
| ||
impl<'a> Finishing<'a> { | ||
fn new(runner: &'a Runner, of: String) -> Self { | ||
fn new(runner: &'a Runner, of: String, workflow: &str) -> Self { | ||
let finishing = &runner.finishing; | ||
let of = (of, workflow.to_string()); | ||
finishing.send_modify(|all| all.push(of.clone())); | ||
Finishing { finishing, of } | ||
} | ||
| @@ -2316,7 +2317,7 @@ impl Runner { | ||
let waits = self.waits(rank, watch, subject, wf, on_ref, &mut cancelled); | ||
let waited = tokio::select! { | ||
biased; | ||
_ = self.dropping(&of, batch, &mut deciding) => { | ||
_ = self.dropping(&of, batch, &wf.after, &mut deciding) => { | ||
telemetry::run("run dropped, to run on the next start", watch, subject, &wf.name, None, None); | ||
return self.drop_run(watch, subject, wf, rkey, check).await; | ||
} | ||
| @@ -2356,7 +2357,8 @@ impl Runner { | ||
let began = std::time::Instant::now(); | ||
telemetry::run("run started", watch, subject, &wf.name, None, None); | ||
let started = Started { | ||
finishing: (wf.superseded == Superseded::Finish).then(|| Finishing::new(self, of)), | ||
finishing: (wf.superseded == Superseded::Finish) | ||
.then(|| Finishing::new(self, of, &wf.name)), | ||
job, | ||
cancelled, | ||
said, | ||
| @@ -2992,11 +2994,18 @@ impl Runner { | ||
/// stopping, unless it is a `batch`'s, or a run going whose runs | ||
/// finish, a deploy, is at the same place, which may wait for its | ||
/// commit's checks. Those are kept, `deciding` so, to run here: the | ||
/// runner handed over to knows them as this one's. | ||
async fn dropping(&self, of: &str, batch: bool, deciding: &mut Deciding) { | ||
/// runner handed over to knows them as this one's. A run that waits | ||
/// `after` that deploy is dropped all the same: the deploy is what | ||
/// stops this runner, so the one handed over to runs it, rather than | ||
/// this one cutting it off past its stop's wait. | ||
async fn dropping(&self, of: &str, batch: bool, after: &[String], deciding: &mut Deciding) { | ||
let mut stopping = self.stopping.subscribe(); | ||
let _ = stopping.wait_for(|stopping| *stopping).await; | ||
let kept = self.finishing.borrow().iter().any(|f| f == of); | ||
let kept = { | ||
let finishing = self.finishing.borrow(); | ||
let mut here = finishing.iter().filter(|(at, _)| at == of); | ||
here.any(|(_, wf)| !after.contains(wf)) | ||
}; | ||
if !batch && !kept { | ||
return; | ||
} | ||
crates/knotless-run/tests/run.rs+12−1
| @@ -1516,8 +1516,14 @@ fn a_stop_drops_what_has_not_started() { | ||
"on: {{push: {{refs: [refs/heads/main]}}, pull: {{base: [refs/heads/main]}}}}\nsteps:\n - run: |\n echo \"$KNOTLESS_RUN_SHA ${{KNOTLESS_RUN_PULL:-push}}\" >> {go}/thens\n until [ -f {go}/then ]; do sleep 0.1; done\n", | ||
go = go.display() | ||
); | ||
// `release` waits for the deploy, which is what stops a runner. | ||
let release = format!( | ||
"superseded: finish\non: {{push: {{refs: [refs/heads/main]}}}}\nafter: [deploy]\nsteps:\n - run: echo $KNOTLESS_RUN_SHA >> {go}/releases\n", | ||
go = go.display() | ||
); | ||
std::fs::write(dir.join("deploy.yml"), deploy).unwrap(); | ||
std::fs::write(dir.join("then.yml"), then).unwrap(); | ||
std::fs::write(dir.join("release.yml"), release).unwrap(); | ||
owner.git(&work, &["add", "."]); | ||
owner.git(&work, &["commit", "-q", "-m", "workflows"]); | ||
owner.git(&work, &["push", "-q", "origin", "HEAD:block"]); | ||
| @@ -1554,15 +1560,18 @@ fn a_stop_drops_what_has_not_started() { | ||
of(&pull, "then")["status"] == "queued" | ||
}); | ||
assert_eq!(of("refs/heads/main", "then")["status"], "queued"); | ||
assert_eq!(of("refs/heads/main", "release")["status"], "queued"); | ||
| ||
// Stopped, it drops the pull's run and reads no more, but waits for | ||
// the deploy going, and keeps the push's run of its commit, which a | ||
// deploy may wait for, to run here once the deploy ends. | ||
// deploy may wait for, to run here once the deploy ends. The run | ||
// waiting after the deploy it drops, for the next runner to run. | ||
let stopping = rt.spawn({ | ||
let run = run.clone(); | ||
async move { run.stop().await } | ||
}); | ||
wait_until(|| of(&pull, "then")["status"] == "cancelled"); | ||
wait_until(|| of("refs/heads/main", "release")["status"] == "cancelled"); | ||
rt.block_on(run.reconcile(&watch)).unwrap(); | ||
std::thread::sleep(Duration::from_millis(500)); | ||
assert!(!stopping.is_finished()); | ||
| @@ -1582,6 +1591,8 @@ fn a_stop_drops_what_has_not_started() { | ||
assert_eq!(of(&pull, "then")["status"], "success"); | ||
assert_eq!(ran("thens"), format!("{main} push\n{topic} {pull}\n")); | ||
assert_eq!(ran("deploys"), format!("{main}\n")); | ||
assert_eq!(of("refs/heads/main", "release")["status"], "success"); | ||
assert_eq!(ran("releases"), format!("{main}\n")); | ||
} | ||
| ||
/// A runner stopping hands over to the one started beside it: the new one | ||
docs/design/runner.md+5−1
| @@ -514,7 +514,11 @@ check is filed as `cancelled`, and the state file records it. Two kinds | ||
are kept instead, to start here as jobs free: a merge-queue batch's, | ||
since dropping one of a batch's runs has the whole batch checked again, | ||
and one at the same commit and base as a running `superseded: finish` | ||
run, since a deploy may wait for its own commit's other checks. | ||
run, since a deploy may wait for its own commit's other checks. A run | ||
that waits `after` that one is dropped all the same: a deploy is what | ||
stops the runner, so a run after it is left to the next runner, which | ||
starts it once the deploy has passed, rather than started here and cut | ||
off past `--stop-wait`. | ||
| ||
On the next start, a dropped run runs again for only those workflows, and | ||
only if its ref or pull is still where it was. If it moved on, the newer | ||