Skip to content

Handover safety, dusk-anchored cadence, and slots that can actually run - #3

Merged
wschoenell merged 6 commits into
mainfrom
fix/handover-and-slot-safety
Jul 29, 2026
Merged

Handover safety, dusk-anchored cadence, and slots that can actually run#3
wschoenell merged 6 commits into
mainfrom
fix/handover-and-slot-safety

Conversation

@wschoenell

Copy link
Copy Markdown
Contributor

Six unfixed scheduler/robobs items from lna40/docs/PENDING_ISSUES.md, all
seen live on opd-40. Each commit is independent and carries a discriminating
test (every new test was verified to fail without its fix).

Handover bookkeeping (issues #37, #38, and the robobs half of #41)

finished doubles as the "handed over" marker and the chimera program id is
a bare sqlite rowid. Three failure modes follow:

  • The queue clean deleted the row of the program being executed. The
    scheduler-program thread then dies on ObjectDeletedError as it logs the
    abort it just handled — every operator lock during a program does this
    (2026-07-28) — and the freed rowid goes to the next handover, so a
    completion is applied to whichever program inherited the id (2026-07-26:
    eta Car recorded as observed out of a hand-loaded bias run). The clean now
    asks the scheduler which program is running and leaves that row alone.
  • A completion could credit a run that never happened. Attribution now
    requires the program's own program_begin. A SUCCESS for a program that
    never began is either the mis-delivery above (still queued → the event is
    ignored) or a program the scheduler finished without running ("Program not
    valid anymore") → the handover is released and the program goes back on
    offer.
  • A handed-but-unrun program whose chimera row vanished was lost forever.
    Program.handed_at (new column, migrated in open_database) records the
    handover itself and is cleared when a completion is applied, so
    "handed but unrun" survives the death of the chimera row. BRUCH's 3.4 h
    block and four focus occurrences died that way on 2026-07-26.

An ABORTED completion is an operator decision, not a program failure: it no
longer counts towards max_consecutive_errors, and the program keeps its
open handover so the next clean re-offers it.

times: cadence anchored on dusk (issue #42)

times: [0, 3, 6, 10] means hours after the evening twilight, but the anchor
was obs_start — which --tonight sets to NOW. A 04:00 UT replan produced
04:00/07:00/10:00/14:00 instead of 21:58/00:58/03:58/07:58: two occurrences
past dawn and no further focus for the night, with nothing warning. Numeric
entries now count from the night's dusk, and an occurrence resolving outside
the window is dropped at generation time (as absolute UT entries already
were) instead of sitting in timeddb unrunnable and unexpirable.

Slots that cannot finish before dawn (issue #40)

The allocator bounded the slot START by the window, not the block END: with
slot_len (1500 s) longer than the block (1260 s), EXOPL's 19th slot on
2026-07-27 ended 3 min after the night did. It was rejected on every poll for
the rest of the night, the queue never legitimately emptied, and a
process-queue preview reported 0.00 h open shutter for a fully booked
night. Block length is known at allocation time, so it joins the per-slot
eligibility mask — a shorter block can still take the last slot.

Per-site block-length overheads (issue #43)

obsblock.length used two global constants (12 s readout/frame, 600 s focus
sweep) where opd-40 measures ~2.8 s and ~270 s — lengths ~4x long, and the
same wrong number lands in the night-end fit check, the derived slot length
and the process-queue simulation. The overhead belongs to the camera, its
binning and its compression, so add-observing-block /
add-project-inputs now take --readout-overhead /--autofocus-overhead /
--autoflat-frame-overhead, defaulting to today's values so nothing changes
silently.

Integration harness (issue #46)

astroufsc/chimera#271 made get_site() the only accessor and dropped the
Scheduler's site: option, so tests/integration/fast_night.py hung and the
test reported a bare 300 s subprocess timeout. The Manager now gets the Site
injected, and the script arms its own deadline that dumps thread stacks, so
the next hang diagnoses itself. The compressed-night run passes again,
which is also an end-to-end check of the handover changes above against a
real bus and scheduler.


Companion core-side fixes (issues #41 and #20) are pushed to
astroufsc/chimera#255.

Unit tests: 174 passed. Integration: test_compressed_night ALL PASS.

… not happen

Three failure modes of the handover bookkeeping, all seen live, share one
root: `finished` doubles as the "handed over" marker and the chimera
program id is a bare sqlite rowid.

* The queue clean deleted the row of the program the scheduler is
  EXECUTING. The scheduler-program thread then dies on ObjectDeletedError
  as soon as it logs the abort it just handled - every operator lock
  during a program does this (opd-40 2026-07-28, the abort landed 6 min
  after the delete) - and the freed rowid goes to the next handover, so a
  completion is applied to whichever program inherited the id (2026-07-26:
  eta Car recorded as observed out of a hand-loaded bias run). The clean
  now asks the scheduler which program is running and leaves that row (and
  its in-memory handover record) alone.

* A completion could credit an observation to a program that never
  started. Attribution now requires the program's own `program_begin`:
  a SUCCESS for a program that never began is either the reused-rowid
  mis-delivery above (the program is still queued, so the event is
  ignored) or a program the scheduler finished without running it
  ("Program not valid anymore"), in which case the handover is released
  and the program goes back on offer.

* A handed-but-unrun program whose chimera row vanished was lost for good:
  the recovery sweep only recognised a `chimera_id` still in the queue, and
  the dangling branch cleared the link but left `finished = True`. BRUCH's
  3.4 h block and four focus occurrences died that way on 2026-07-26.
  `Program.handed_at` now records the handover itself and is cleared when
  a completion is applied, so "handed but unrun" survives the death of the
  chimera row. A program whose row is finished still counts as run.

An ABORTED completion is an operator decision, not a program failure: it no
longer counts towards `max_consecutive_errors`, and the program keeps its
open handover so the next clean re-offers it.
`times: [0, 3, 6, 10]` is documented as hours after the evening twilight,
but parse_time_entry anchored on `obs_start` — which `--tonight` sets to
NOW. A 04:00 UT replan on 2026-07-28 therefore produced 04:00/07:00/10:00/
14:00 instead of the night's 21:58/00:58/03:58/07:58: two occurrences past
dawn, one expired unrun, and no further focus for the rest of the night.
Nothing warned; the rows look perfectly plausible in the database.

Numeric entries now count from the dusk that opened the night (the site
knows it; the window start no longer defines the cadence), and an
occurrence resolving outside the observing window is dropped at generation
time — the treatment absolute UT entries already got — instead of sitting
in `timeddb` unrunnable and, with nothing later to supersede it,
unexpirable.
The allocator lays slots on a fixed `slot_len` cadence and stops when the
slot START passes the window end; whether the BLOCK fits in what is left
of the night was only checked later, per poll, by check_conditions. With
slot_len (1500 s) longer than the block (1260 s), EXOPL's nineteenth slot
on 2026-07-27 carried a block ending 08:22:03 against a night end of
08:18:54. It was rejected on every poll for the rest of the night, so the
project's queue never legitimately emptied, night_status showed an
unfinished program that is unrunnable by construction, and a
`process-queue` preview reported 0.00 h open shutter for a night that was
in fact fully booked.

Block length is known at allocation time (`obsblock.length`, stored at
ingest), so it joins the per-slot eligibility mask: a block too long for
the remaining night is simply not a candidate, and a shorter one can
still take the slot.
`obsblock.length` was estimated with two global constants — 12 s of
readout per frame and 600 s per autofocus sweep. Measured on opd-40
(QHY600, 1x1, fits_rice) the real numbers are ~2.8 s/frame and ~270 s, so
stored lengths run about 4x long, and the same wrong number lands in three
places: the night-end fit check rejects blocks that would have finished
(EXOPL's last visit: 1260 s estimated, 985 s real, 3 min inside the
night), the derived slot length gives ~24 visits where ~37 fit, and
`process-queue` reports open-shutter and idle times off by the same
margin.

The overhead is a property of the camera, its binning and its compression,
so no constant can be right for every deployment. `add-observing-block`
and `add-project-inputs` now take --readout-overhead / --autofocus-overhead
/ --autoflat-frame-overhead, defaulting to today's values so nothing
changes silently, and the estimate is printed per block at ingest.
astroufsc/chimera#271 made `ChimeraObject.get_site()` the only way an
object reaches the site and dropped the Scheduler's `site:` option. The
harness built its Manager without a site and configured one on the
scheduler, so the scheduler-program thread raised ObjectNotFoundException
and the script hung short of its exit - reported as a 300 s subprocess
timeout, which reads as "slow" rather than "broken".

The Manager now gets the Site injected (as production does, and as the
core's own test fixture does), the scheduler's dead `site:` key is gone,
and the script arms a hard internal deadline that dumps every thread
stack before exiting non-zero, so the next hang diagnoses itself.
Takes main's stricter plot-log fallback (resolve an orphaned log entry by
name only when exactly one target carries it); this branch's own
cli/robobs.py changes are unaffected.
@wschoenell
wschoenell merged commit 34cd1a8 into main Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant