5 Commits

Author SHA1 Message Date
9e58fd0c6d Merge pull request 'ci: clean up after image builds and demo wipes' (#237) from chore/ci-self-cleanup into main
All checks were successful
sync-project-tree / sync (push) Successful in 22s
Build container images / build (push) Successful in 2m49s
Build container images / deploy (push) Successful in 51s
SonarQube / analysis (push) Successful in 10m16s
Demo wipe / wipe (push) Successful in 6m31s
Reviewed-on: #237
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-11-04 12:16:02 +00:00
ebb8f870bb ci: clean up after image builds and demo wipes
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 53s
PR Checks / client-build (pull_request) Successful in 2m19s
PR Checks / server-tests (pull_request) Successful in 6m19s
The build job runs docker on the runner host's daemon, so every run left
four tagged images behind for good. Remove this run's tags once they are
pushed and cap BuildKit's cache at 5 GB, always, never failing the job.

The demo wipe pulls every two hours; drop the dangling image each pull
replaces. The uom-deploy-runner deploy job is deliberately untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-11-02 11:20:47 -06:00
27f7fe5d85 Merge pull request 'fix(events): core.lease keeps a module's retry: false on read/apply refusals' (#236) from fix/lease-retry-206 into main
Some checks failed
sync-project-tree / sync (push) Successful in 13s
Build container images / build (push) Successful in 1m47s
Build container images / deploy (push) Successful in 40s
SonarQube / analysis (push) Failing after 3m20s
Demo wipe / wipe (push) Successful in 7m23s
Reviewed-on: #236
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-11-02 11:21:52 +00:00
10efafbae5 Merge branch 'main' into fix/lease-retry-206
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 52s
PR Checks / client-build (pull_request) Successful in 1m10s
PR Checks / server-tests (pull_request) Successful in 6m14s
2026-11-02 11:15:17 +00:00
28d0b76f13 fix(events): core.lease keeps a module's retry: false on read/apply refusals
All checks were successful
PR Checks / client-build (pull_request) Successful in 59s
PR Checks / bot-tests (pull_request) Successful in 1m16s
PR Checks / server-tests (pull_request) Successful in 6m3s
core.lease rebuilt both failure envelopes as { ok: false, error }, so a
module's retry: false was dropped and the dispatcher retried a permanent
refusal (e.g. module-rust's events-disabled) three times. Both returns now
pass retry: false through and leave retry undefined otherwise, so the
dispatcher's default still applies. Three tests in eventModuleContract cover
read, apply (reservation still released) and the default.

Closes #206

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-11-01 02:53:17 -06:00
4 changed files with 119 additions and 1 deletions

View File

@@ -93,6 +93,22 @@ jobs:
docker push "${BOT_IMAGE}:latest"
docker push "${BOT_IMAGE}:${TAG}"
- name: Remove this run's images and cap the build cache
# The build runs on the HOST daemon, so whatever it leaves stays on the
# runner host for good: every run used to leave four tagged images, and
# each newer `latest` turned the previous one into a permanent `sha-` image
# until the host ran out of disk. Both tags are in the registry by now, so
# the local copies are dead weight. BuildKit keeps its layer cache apart
# from the images, so the next build stays fast; the cache is pruned to a
# cap instead of growing forever (`--reserved-space` is the same flag's
# name on newer Docker). Runs even after a failure, and never fails the job.
if: always()
run: |
docker image rm "${APP_IMAGE}:latest" "${APP_IMAGE}:${TAG}" \
"${BOT_IMAGE}:latest" "${BOT_IMAGE}:${TAG}" 2>/dev/null || true
docker builder prune -f --keep-storage 5GB \
|| docker builder prune -f --reserved-space 5GB || true
- name: Log out (clear cached credentials from the runner)
if: always()
run: docker logout "${REGISTRY}" || true

View File

@@ -110,6 +110,14 @@ jobs:
docker run --rm --network rg-demo_default -v "$DEMO_DIR:/demo" \
-e SITE_URL=http://app:3000 "$NODE_IMAGE" node /demo/rig.mjs smoke
- name: Remove the images this wipe replaced
# Each pull that finds a newer `latest` leaves the old image untagged on
# the demo host, every two hours. Untagged images are never used again, so
# drop them. Dangling images only: nothing tagged and nothing a container
# is using is touched.
if: always()
run: docker image prune -f || true
- name: Report the rig restore
if: steps.rig.outcome == 'failure'
run: |

View File

@@ -442,8 +442,12 @@ const ACTIONS = [
const baseline = await lease.read({ target })
if (!baseline || baseline.ok !== true) {
// A module that says `retry: false` has said a retry cannot help, so that
// verdict is passed through; anything else stays undefined so the
// dispatcher's own default (retry) still applies.
return {
ok: false,
retry: baseline && baseline.retry === false ? false : undefined,
error: baseline && baseline.error
? String(baseline.error)
: `could not read the current value of ${lease.label}`,
@@ -494,7 +498,11 @@ const ACTIONS = [
}
if (!applied || applied.ok !== true) {
await resourcesDb.markReverted(reserved.id)
return { ok: false, error: applied && applied.error ? String(applied.error) : `${lease.label} refused the new value` }
return {
ok: false,
retry: applied && applied.retry === false ? false : undefined,
error: applied && applied.error ? String(applied.error) : `${lease.label} refused the new value`,
}
}
await resourcesDb.confirm(reserved.id)

View File

@@ -813,6 +813,92 @@ test('a dry run of core.lease checks everything and takes nothing', async () =>
assert.equal(bad.outcome, 'terminal')
})
// ── A lease's refusal keeps its `retry: false` (website#206) ────────────────
//
// `core.lease` rebuilt both failure returns as `{ ok: false, error }`, so a module
// that said "a retry cannot help" (a shard that refuses the value outright, a
// target that does not exist) was dispatched as `retry: true` and the run paused
// and re-asked on a loop. The module's `retry: false` now passes through, and
// anything else is left undefined so the dispatcher's own default still applies.
//
// The reservation lands in MariaDB, which these tests do not have, so the two
// calls `core.lease` makes on it are swapped for recorders and put back after.
const REFUSING_LEASES = `module.exports = (ctx, api) => {
const lease = (id, read, apply) => ({
id, label: 'Gain rate', type: 'float', min: 0.5, max: 5, maxDurationMs: 3600000,
read, apply, async restore() { return { ok: true } },
})
const fine = async () => ({ ok: true, value: 1 })
const accepts = async () => ({ ok: true })
api.registerEventLeases([
lease('demo.read.final', async () => ({ ok: false, retry: false, error: 'no such spawner' }), accepts),
lease('demo.read.soft', async () => ({ ok: false, error: 'shard is restarting' }), accepts),
lease('demo.apply.final', fine, async () => ({ ok: false, retry: false, error: 'value refused' })),
lease('demo.apply.soft', fine, async () => ({ ok: false, error: 'shard is restarting' })),
])
}`
/** Run `fn` with the lease reservation calls recorded rather than sent to a database. */
async function withStubbedReservations(fn) {
// eslint-disable-next-line global-require
const resourcesDb = require('../src/model/events/eventRunResources.db')
const original = { reserve: resourcesDb.reserve, markReverted: resourcesDb.markReverted }
const calls = { reserved: 0, reverted: [] }
resourcesDb.reserve = async () => { calls.reserved += 1; return { ok: true, id: 77 } }
resourcesDb.markReverted = async (id) => { calls.reverted.push(id) }
try {
return await fn(calls)
} finally {
Object.assign(resourcesDb, original)
}
}
const leaseStep = (lease) => step('core.lease', { lease, value: '3', minutes: 10 })
test('a lease read refusal that says retry: false is terminal, not retried', async () => {
assertRegistered(loadModule('demo', REFUSING_LEASES))
registries.registerCore()
await withStubbedReservations(async (calls) => {
const result = await dispatch.dispatchStep(leaseStep('demo.read.final'), { run: RUN })
assert.equal(result.outcome, 'terminal')
assert.match(result.error, /no such spawner/)
// Nothing was reserved, so there is nothing to give back.
assert.equal(calls.reserved, 0)
})
})
test('a lease apply refusal that says retry: false is terminal, and the reservation is still released', async () => {
assertRegistered(loadModule('demo', REFUSING_LEASES))
registries.registerCore()
await withStubbedReservations(async (calls) => {
const result = await dispatch.dispatchStep(leaseStep('demo.apply.final'), { run: RUN })
assert.equal(result.outcome, 'terminal')
assert.match(result.error, /value refused/)
assert.equal(calls.reserved, 1)
assert.deepEqual(calls.reverted, [77])
})
})
test('a lease refusal that does not say retry: false is still retried', async () => {
assertRegistered(loadModule('demo', REFUSING_LEASES))
registries.registerCore()
await withStubbedReservations(async (calls) => {
const read = await dispatch.dispatchStep(leaseStep('demo.read.soft'), { run: RUN })
assert.equal(read.outcome, 'retry')
assert.match(read.error, /shard is restarting/)
const apply = await dispatch.dispatchStep(leaseStep('demo.apply.soft'), { run: RUN })
assert.equal(apply.outcome, 'retry')
assert.match(apply.error, /shard is restarting/)
// The apply refusal released its reservation exactly as before.
assert.deepEqual(calls.reverted, [77])
})
})
// ── Targeted leases and searchable sources (Phase 12b) ──────────────────────
test('a targeted lease is a family of values, and the target rides in the ref', async () => {