Repository navigation
[hosting] Hosted progress events: the events plug implementation and its emit points - #2138
Conversation
CloudEvents 1.0 lines in rolling segment files with a resumed, strictly increasing id, fsync at most once a second, one writer per directory taken from the host inputs. The legacy flow emits phase_changed, candidate_settled, best_updated, budget and warning through the events socket; the local implementation still only logs at debug. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Code metrics gate: PASSEDJudged: the 18 files this change touches. Files it does not touch never fail it; anything found there is listed under Outside this change for information.
20 findings in files this change does not touchOutside this change (informational): 20
Thresholds and sources: |
Name repeated literals, keep the budget throttle on an object, lower the writeback.py baseline to its new length, and create segments without world read permission. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
writeback.py was split into per-job modules on main (#2132); re-apply the winner_vetoed warning and best_updated emit in _current_best_lift.py. Co-Authored-By: Claude <noreply@anthropic.com>
…eback test Co-Authored-By: Claude <noreply@anthropic.com>
…tion hosted-cpu no longer names an environment variable and 'events=hosted' now resolves, so the unset-variable case uses a manifest and the not-installed case is refused at the measurement socket. Co-Authored-By: Claude <noreply@anthropic.com>
chaojhou
left a comment
There was a problem hiding this comment.
PR #2138 -- [hosting] Hosted progress events: the events plug implementation and its emit points
What it does: adds the hosted implementation of the events plug socket (src/hyperloom/hosted/events.py). It writes CloudEvents 1.0 JSON lines into rolling 4 MiB segment files under HostInfo.events_dir:
- event ids rise strictly and continue after a restart;
- a half-written last line is cut off;
- fsync runs at most once a second.
It also adds a never-raising emit facade (src/hyperloom/plugs/progress.py). The legacy flow calls it at its existing write points: phase_changed, candidate_settled, best_updated, a budget event throttled to one per 5 minutes, and three warning events. The PR ships a v1 data schema, switches the hosted-cpu and hosted-gpu profiles to events=hosted, and documents the stream format.
Blocking issues: 2
-
[free:roll-failure-wedges-stream] One failed
os.openduring a segment roll breaks the events writer for the rest of the run [verified]
Problem:_roll_if_full(src/hyperloom/hosted/events.py:148-154) closes the current fd, setsself._fd = Noneand incrementsself._segmentbefore it calls_open_segment(). Ifos.openraises there (EMFILE, ENOSPC, or EIO on the shared events dir),_fdstaysNonewhile_sizeis still above the cap. Every lateremit()takes the roll branch again and callsos.close(None), which raisesTypeError.emit()logs that each time, but nothing ever reopens a segment.
Impact: after one transient filesystem error at a 4 MiB boundary, every later progress event in the run is lost. That includesphase_changed,best_updated, and thecampaign_finished/campaign_failedevents the hosted entry sends at the end. The platform tailing the directory sees the run stop reporting partway. This breaks the PR's own contract that a failed write is logged and the stream carries on;test_a_failed_write_is_logged_not_raised_and_leaves_no_partial_linepins that contract only for the write path.
Action: open the next segment before giving up the current one, or increment_segmentand replace_fdonly afteros.opensucceeds, so the next emit retries the roll. Add a test in whichos.openfails once during a roll and the following emit lands in the new segment with the next id. -
[X2] The description names the wrong file for
best_updatedand claims a writeback.py baseline change that is not in the diff [verified]
Problem: the description saysbest_updatedis emitted inorchestrator/loop/writeback.py_write_current_best. It also says writeback.py shrinks by one line, that its code-metrics baseline entry drops to 4155, and that two function-local imports were removed. At head 82f548f the emit is at src/hyperloom/orchestrator/loop/_current_best_lift.py:424, because #2132 moved_write_current_bestthere. The diff does not touch writeback.py or the baseline, and only one function-local import was removed (shared_state.py).
Impact: the release is built from this description, and reviewers use it to find the emit sites. As written, it points to a file the PR does not change and to a baseline edit that does not exist.
Action: name_current_best_lift.py_write_current_best(and_refuses_graded_winnerfor thewinner_vetoedwarning) as the emit sites. Remove the writeback.py / 4155 / two-imports sentence, or restate it as what the diff does.
The bot findings are addressed at this head: the segment files open with mode 0o600 (events.py:139), and the unused _budget_last global is gone. CI at 82f548f is green, and the branch merges cleanly with current main. Note that this implementation receives HostInfo through PlugContext.host_info, which open_plug_host does not yet supply on main; see the second finding on #2134.
Checked: hosted/events.py, plugs/progress.py, plugs/host.py, plugs/socket.py, plugs/events_local.py, hosted/host_info.py, the emit call sites (machine.py _commit_transition, _current_best_lift.py, shared_state.py), the schema, the env-var doc, the bot review threads | Ran: direct-call probes of the events writer on an exported head tree; git log -- writeback.py; git diff --stat base..head; git merge-tree --write-tree origin/main (clean) | Head: 82f548f
A failed os.open during a roll no longer leaves the writer without a descriptor; the next emit retries the roll. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: zoroyihan7 <Yihan.Wang@amd.com>
|
Addressed the CHANGES_REQUESTED review:
|
The legacy flow does not call measurement, agent runner or GPU token through plug sockets; those belong to the multibranch hosted entry, which declares them in its own profile. A socket the profile leaves out is not started, and PlugHost.get raises if something asks for it. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: zoroyihan7 <Yihan.Wang@amd.com>
|
hosted-gpu profile now declares only |
eventsplug (src/hyperloom/hosted/events.py) and the emit points in the legacy flow. The plug kernel and the local implementation are already on main (feat(plugs): add the plug kernel, built-in profiles and the events socket #2110). The hosted implementation writes CloudEvents 1.0 structured JSON, one event per line, to<events dir>/<10-digit segment>.jsonl:idis a decimal string, strictly increasing within the run, resumed from the last complete line of the last segment on start; a half-written last line (a crashed write) is cut off first, so the next event never glues onto it.write, and ends with a newline, so a reader that tails by byte offset can tell an unfinished line from an event.fsyncat most once a second and onflush()/close; one event is at most 16 KiB (larger content is replaced by{"oversized": true, "bytes", "keys"}with a warning); thread-safe; a failed write is logged, the partial bytes are removed, and the run continues.HostInfothrough the plug context, never fromos.environ, whichtake_host_infohas already cleared. A child process therefore has nothing to write to.hyperloom/plugs/progress.py, which never raises):phase_changedinorchestrator/phases/machine.py;candidate_settledinbreakdown/recorder/phase_event.pyrecord_settle;best_updatedinorchestrator/loop/_current_best_lift.py_write_current_best;budgetafterSharedState.save(), at most every 5 minutes (tokensandgpu_secare null, the legacy flow keeps no total of either);warningat three degradation paths (state save failure at a phase boundary, a vetoed winner (_refuses_graded_winnerin the same file), a recorder write failure).plugs_lock_mismatchis already emitted by the plug host (feat(plugs): add the plug kernel, built-in profiles and the events socket #2110) and is reused.src/hyperloom/contracts/hyperloom_events.v1.schema.json(v1 type set only, shipped as package data); a test validates what the emitters send against it.measurement_*andnode_quarantinedare defined in the schema only; their emitters come with hosted measurement.hosted-cpuandhosted-gpuselectevents=hostedand no longer carryconfig.dir = ${HYPERLOOM_EVENTS_DIR}(that variable is removed from the environment before the plug host expands it; the directory comes fromHostInfo). The built-in registry gainsevents/hosted. The otherhosted-*sockets still have no implementation on main, so those profiles are refused exactly as before.campaign_started,campaign_finishedandcampaign_failedare emitted by the entry (not here). Aftertake_host_infoand building thePlugHostwithhost_info=<HostInfo>, the entry runs insideactivated(host)and callshyperloom.plugs.progress.emit("hyperloom.campaign_started", {"session_dir", "profile", "plugs_fingerprint"}), and likewisecampaign_finished(stop_reason,best,report_path) orcampaign_failed(error, optionalstop_reason), thenhost.get("events").flush()beforehost.close()(close also syncs).emitnever raises.hosted/tests/test_hosted_events.py(CloudEvents attributes on every type, id resume across a restart including a half-written last line and an empty last segment, rollover at 4 MiB, 8 threads x 150 events with unique increasing ids in file order, oversized event, fsync rate bound with a fake clock, failed and partial write leaves no partial line, a failedos.openduring a segment roll is retried by the next emit (the next id, in the new segment), a real child process has no events directory variable, the directory comes from the host inputs and not from a decoy in the environment);plugs/tests/test_progress.py(emitters match the schema, schema covers exactly the v1 types, the plug host's lock mismatch event matches the schema, an emit that raises does not reach the caller, budget throttle);orchestrator/phases/tests/test_progress_emit_points.py(phase switch, best_updated, no event for a refused winner); one test added totest_sbd_v6_phase_timeline.py(candidate_settled). Reverse-proofs run once each: resume ignoring the last id fails 2 tests; reading the directory from the environment fails the decoy test;take_host_infoleaving the variable in place fails the child test; writing without the newline fails 8 tests; closing the current segment before the next one opens fails the roll-retry test. Ran ruff check and format,lint-imports, and the phases, loop, breakdown, plugs, hosted, state and inference_optimizer test trees.shared_state.pyis over 1200 lines and does not grow: one function-local import of a name already imported at module level was removed to pay for the added lines. This PR does not touchwriteback.pyor the code-metrics baseline. No function gets longer than its recorded value. Segment files are created with mode 0600 (owner only).docs/reference/environment-variables.mddocuments the format.hosted-*profile files dropevents.config.dir; those profiles were not usable on main.🤖 Generated with Claude Code