Task: Carry the host on the heartbeat

Table of Contents

This page documents a task in the Implement the operations journeys in ores.web story. It captures the goal, current status, acceptance, and any notes or results.

1. Goal

Every service heartbeat carries the service name, the instance id and the release, and no host. The compute side is the mirror: each node sample carries the host id, and no instance id. Nothing joins an instance to the machine it runs on, or a compute wrapper to its node (The service heartbeat carries no host). This task adds host_id to service_heartbeat_message and stores it on service_sample beside instance_id, so the grid's wrapper panel and the services roster can name the machine behind an instance.

2. Status

Field Value
State DONE
Parent story Implement the operations journeys in ores.web
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-10-04

3. Acceptance

  • service_heartbeat_message carries a host_id, and service_sample stores it beside instance_id.
  • The publishing side fills it wherever it already holds the id, the compute wrapper first: the wrapper already stamps its node samples with it.
  • The capture The service heartbeat carries no host moves out of the inbox with this task's evidence.
  • The change regenerates cleanly, and the telemetry and compute tests pass.

4. Plan

The field travels one way: a publisher stamps it, the telemetry service copies it, and the database stores it. The work follows that path. Both messages keep one field order — service_name, instance_id, host_id, version — so the model, the entity and the stored row agree.

  1. Add host_id to service_heartbeat_message and service_sample in projects/ores.telemetry/modeling/ores.telemetry.service_samples.org. The field sits beside instance_id, and it is empty when the publisher does not know the host.
  2. Regenerate the protocol from the model, and check the C++ and TypeScript diffs carry the new field and nothing else.
  3. Fill the field where the value exists. heartbeat_publisher takes host_id as a trailing defaulted constructor argument and stamps every message with it, and the compute wrapper passes cfg.host_id — the value it already stamps on its node samples. Every other service holds no host id, so its beats carry the empty string.
  4. Copy the field at the receiving end: telemetry_handler::service_heartbeat writes hb->host_id onto the service_sample it builds.
  5. Store it. service_sample_entity gains std::string host_id;, the mapper carries it both ways, and the create script adds host_id text not null default ''.
  6. Prove it. Add a repository test that inserts a sample with a host id and reads it back through list_service_samples, then build and run the telemetry and compute tests against a recreated database.
  7. Close the loop. Move capture A1D7FC12 out of the inbox with the evidence, and record the measurements on this task.

5. Measurements

Every number this task claims about a tree, with the command that produced it and the commit it was measured at. A number without both is not a measurement, because a clean-up record describes a tree and trunk moves under a branch: a consumer count, a gate's component count and a suite's size all change with no code changing, and the reader of a bare number cannot tell which tree it came from. Where the raw output matters, put it in an evidence/ file beside the task and name that file in the Command column.

What Value Command Commit
Telemetry suites 3/3 pass: core 0.90 s, database 1.84 s, service 0.81 s compass test run -- -R 'ores\.telemetry\.' fd05c11807
Compute suites 3/3 pass: api 0.20 s, core 3.63 s, service 0.74 s compass test run -- -R 'ores\.compute\.' fd05c11807
Database recreate fingerprint 348f2fc9f8d6d570, 0 errors, 0 m 42 s compass db recreate -y -k fd05c11807
Drift sweep no file this branch touched would change check_component_drift.py --sweep fd05c11807
ER diagram check 430 tables, 0 warnings, puml up to date plantuml_er_generate.sh --check fd05c11807
TypeScript typecheck no errors npm run typecheck (wire-protocol) fd05c11807
Live demo wrapper beats carry the host: 17/17; every other service sends the empty string compass db sql (queries in PR #2626 comment) 3a8ca6538e

6. Notes

The design is settled on The service heartbeat carries no host: one field, not a new entity. The alternative — a release and an instance id on the node sample — answers the grid's join only, and leaves the services screen blind.

7. Test Scenarios

Manual QA scenarios (scaffolded via compass add test_scenario) that verify this task. Link new ones here as they're created; the scenario doc itself links back via its "Verifies task" field.

Scenario State Notes
     

8. PRs

PR Title
#2626 [telemetry] Carry the host on the service heartbeat

9. Review

# Comment summary File Decision Notes
1 Existing databases: create table if not exists does not add the column projects/ores.sql/create/telemetry/service_samples_create.sql Accepted The project has no migration mechanism: environments are created by compass db recreate, and the fleet starts only on a matching fingerprint. No long-lived database exists to alter.
2 Mixed-version rollout: a missing field may fail to decode build/.../rfl/parsing/NamedTupleParser.hpp (rfl 0.25.0) Accepted Verified in the vendored source: a plain std::string field is required (line 289), so an old publisher's beat fails to decode and telemetry logs a warning and drops it (telemetry_handler.hpp:136). An unknown field is ignored, so a new publisher is safe on an old telemetry. The fleet starts from one build, so no mixed window exists; the failure is bounded and self-heals on publisher restart.
3 Primary key: host_id is not in the key projects/ores.sql/create/telemetry/service_samples_create.sql:35 Accepted The key stays (sampled_at, service_name, instance_id); instance ids are unique per process.
4 Bundled merge in the six-commit history — Accepted House flow merges main into the branch before review; the review surface is the implementation commit.

10. Result

service_heartbeat_message and service_sample carry host_id, and the one field answers both joins the capture named: a wrapper's row can name its node, and an instance's row its host.

The field travels the whole path. The model carries it on both messages, the regenerated C++ and TypeScript protocols carry it, and heartbeat_publisher takes it as a trailing defaulted argument and stamps every beat. The compute wrapper passes cfg.host_id, the value it already stamps on its node samples; every other service sends the empty string. telemetry_handler copies it onto the sample, and the entity, the mapper and the host_id text not null default '' column store it.

Verification. The repository test create_and_list_service_sample writes a sample with a host id and reads it back through list_service_samples. The telemetry suites (3/3) and the compute suites (3/3) pass against a recreated database. See the measurements above.

Emacs 29.3 (Org mode 9.6.15)