Skip to content

feat(rollout): release channel schema, queries and store - #1015

Draft
rl-block wants to merge 1 commit into
mainfrom
rollout/01b-release-channel-schema
Draft

feat(rollout): release channel schema, queries and store#1015
rl-block wants to merge 1 commit into
mainfrom
rollout/01b-release-channel-schema

Conversation

@rl-block

@rl-block rl-block commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Reviewable diff: +1226/-0 across 5 files (excludes generated, test, story and fixture files).

Summary

Adds the storage layer for release channels: tables for channels, their scope selectors, per-pair firmware assignments identified by payload checksum, rollouts (with generation, revision, actors and lineage), per-miner rollout progress and per-device deployment provenance, plus the views that answer "which channel does this miner belong to right now" and "how is each overlap resolved" from live fleet placement. The sqlc queries the domain layer uses (scope preview with overlap detection, rollout snapshots, per-miner halts and retries, telemetry evidence) ship with their generated Go. Nothing reads these tables yet.

Stack. #1014 (merged: API contract) -> #1015 (this PR) -> #1016 (domain: channels and scopes) -> #1017 (rollout engine) -> #1018 (API wiring) -> #1019 -> #1020 -> #1021 -> #1022 -> #1023. Diff is relative to main. Upstream context: the proto in #1014 defines the vocabulary the columns mirror (method, order, stage, status, cancel reason, halt reason, thresholds). Out of scope: the transactional rules that use these queries (advisory-locked overlap rejection in #1016, stage transitions and dispatch in #1017) and the activity-label migration for rollout events (#1022).

How it works

Channel definition. release_channel holds the name, description, behaviour columns (method, order_by, batch_size, pilot_size, wait_between_batches_seconds, review_after_each_batch, auto_continue, stabilization_seconds, the four nullable thresholds, max_concurrent_offline); the name is unique per org (UNIQUE (org_id, name)). release_channel_target stores one row per scope selector (target_type in site/building/rack/group/miner plus target_id). release_channel_firmware stores one row per (channel, manufacturer, model) pair (unique under an ASCII fold): the assigned payload checksum, version and snapshotted target keys, and an assignment_generation that advances on every change including clearing, which empties the checksum but keeps the row.

Membership. release_channel_match unions five queries, one per selector kind, each joining the selector to the devices it currently covers through the existing placement tables (device.site_id, device.building_id, device_set_membership for racks and groups, device_set_rack for a rack's building) and tagging the row with a specificity (miner 1, group 2, rack 3, building 4, site 5). release_channel_member collapses that to one row per miner with DISTINCT ON (device_id) ordered by specificity then channel id, and sets conflicted when more than one channel matched. Every read of "who is in this channel" goes through this view, so moving a miner changes its channel immediately.

Rollouts. firmware_rollout records one run per pair: the target checksum and version, the lineage for rollback, the assignment generation, a trigger-maintained revision/updated_at, the starting and last-acting actor, a copy of the behaviour it started with, status, cancel_reason, stage, batch_count/current_batch, stage_changed_at, paused_at, finished_at, and a partial unique index guaranteeing at most one active rollout per (channel, model). firmware_rollout_device holds the snapshot of miners in a run: batch_index (NULL for the rest stage), position, attempts, first_sent_at/last_sent_at, last_error, halted_at/halt_reason (failed or canceled), excluded_at, added_at, and the baseline captured when the miner was snapshotted (baseline_status, baseline_hash_rate_hs, baseline_power_w, baseline_efficiency_jh, baseline_temp_c, baseline_open_errors, baseline_at).

Queries (release_channel.sql): channel CRUD and target replacement; ListDeviceIDsByIdentifiers to turn operator-supplied identifiers into ids; ResolveReleaseChannelScope, which evaluates an unsaved scope with the same joins as the view and reports, per miner, the model and the owning channel if another one already covers it; LockReleaseChannelScopes, a per-org transaction-scoped advisory lock; ListReleaseChannelMismatchedMembers (members whose reported version differs from the assignment); ListFirmwareRollouts with optional channel and status filters and keyset paging on (created_at, id) (the caller passes the last row's key and a limit); GetFirmwareRolloutWithChannel; ListReleaseChannelMinersPage (one channel's members, optional model, keyset paging on (device_identifier, device_id)); SnapshotFirmwareRolloutDevices, ListFirmwareRolloutDevices joined to current status and latest telemetry; RequeueFirmwareRolloutDevices and ReleaseFirmwareRolloutDeviceHalts for retry; and the stage/status update statements.

Diagrams

erDiagram
    release_channel ||--o{ release_channel_target : "scope selectors"
    release_channel ||--o{ release_channel_firmware : "one per model"
    release_channel ||--o{ firmware_rollout : "runs"
    firmware_rollout ||--o{ firmware_rollout_device : "snapshot"
    release_channel_target }o--o{ device : "resolved via release_channel_match"
Loading
flowchart LR
    T["release_channel_target (site / building / rack / group / miner)"] --> M["release_channel_match: one row per (channel, miner) with specificity"]
    P["device placement: device.site_id, building_id, device_set_membership, device_set_rack"] --> M
    M --> R["release_channel_member: DISTINCT ON miner, lowest specificity wins, conflicted flag"]
    R --> Q["ListReleaseChannelMembers / ListReleaseChannelMismatchedMembers"]
Loading

Areas of the code involved

Area / file What changed Why it matters for review
server/migrations/000148_release_channels.up.sql (new) Five tables, indexes (lookup by target, one-active-rollout partial unique, org/created ordering, device lookup), two views The data model. Focus on the release_channel_match joins (a wrong join silently changes who gets firmware) and the DISTINCT ON ordering in release_channel_member
server/migrations/000148_release_channels.down.sql (new) Drops views then tables in dependency order Round-tripped by the test
server/sqlc/queries/release_channel.sql (new) All queries listed above ResolveReleaseChannelScope must agree with the view; LockReleaseChannelScopes is what #1016 relies on for exclusivity
server/internal/domain/stores/sqlstores/release_channel.go (new) SQLReleaseChannelStore (transaction-aware Queries(ctx)) and IsUniqueViolation Small adapter following the other stores
server/internal/infrastructure/db/migration_bridges_test.go Derives the latest migration version from the embedded files instead of hardcoding it (main moved from 145 to 147 while this stack was open) Test-only; keeps the bridge test from breaking on every new migration
server/migrations/release_channels_test.go (new) Applies 148 up, down, up Test-only
server/generated/sqlc/** sqlc output (release_channel.sql.go, models.go, querier.go, retrying_querier.gen.go, db.go) generated — skip

Key technical decisions & trade-offs

  • Membership is a view over placement, not a materialized membership table. Channels track fleet moves with no sync job; the cost is a five-way union evaluated on every read, mitigated by the target lookup index and by the domain loading an org's members once per request (feat(rollout): release channel domain service (channels and scopes) #1016).
  • The membership view inlines the placement joins instead of reading fleet_device_placement, so neither view blocks schema changes to the other.
  • Overlap is resolved by specificity, ties exclude the miner, and every relation is exposed rather than rejected at read time. Rejection happens at write time (feat(rollout): release channel domain service (channels and scopes) #1016); this view has to cope with overlaps that arise after saving because a miner moved.
  • One active rollout per pair is a partial unique index on the folded keys, so the invariant holds even if two enforcement instances race, instead of relying on application checks alone.
  • Rollout rows snapshot behaviour and baseline telemetry rather than joining to the channel and metrics at read time, so evidence is compared against what the miner did before this update and later channel edits do not rewrite history.
  • Rollout and member listings page by keyset, not offset: the cursor is the sort key of the last row ((created_at, id) for rollouts, (device_identifier, device_id) for members), compared as a row value against the same ORDER BY, so pages stay stable while rollouts start or miners move.
  • Assignments store the checksum, not the file id. The file is resolved from the checksum at read and dispatch time, so renaming, re-uploading or deleting a file never changes an assignment; "on target" needs the reported version and provenance equal to the checksum.

Contract alignment (after the #1014 merge)

#1014 merged on 2026-09-08 as 74454ed9. This stack was first cut against the contract of 2026-09-04; on 2026-09-08 every PR was rebased onto main and reworked to the merged contract, so each one is again one commit above its parent and green on its own. Head 8d62462ec.

This PR implements: Pair-keyed assignments (manufacturer, model, ASCII-folded unique index) whose row outlives clearing and carries assignment_generation; checksum artifact identity with the file's target metadata snapshotted; rollouts with manufacturer, checksum lineage, generation, revision/updated_at advanced by triggers on the row and on its devices, started-by/last-action-by actors, controller_timeout_seconds, min_sample_coverage_percent; skipped halts with skip_note; managed-deployment provenance per device; release_channel_member resolving specificity ties to no channel and release_channel_conflict exposing winner/loser/excluded-tie relations; paged model groups, conflicts and suppressed-member queries; files-service lookups for checksum identity.

Implementation notes:

  • Suppression: a member halted (failed, skipped, or left behind by a cancellation) in the most recent rollout of the current generation that holds it is suppressed until retried — CancelRollout's contract text makes canceled-remaining miners retryable, so cancellation suppresses too.
  • Mismatch rule: reported version plus provenance; the outstanding-command clause is not consulted, since continuous reconciliation corrects a late-finishing superseded update on the next tick.

Deferred to follow-up slices (not in this stack): delegated control (ROLLOUT_METHOD_DELEGATED, AdvanceRollout, SkipRolloutDevices, CompleteRollout, WAITING_FOR_CONTROLLER, controller_timeout_seconds) and the events feed (ListRolloutEvents, RolloutEvent, RolloutActor history). The schema, behavior, error reasons, permission entries and enum vocabulary for both are already in place; the handler answers those RPCs with Unimplemented and refuses the DELEGATED method until the slices land.

Testing & validation

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Note: This is an automated security-focused code review generated by Codex.
It should be used as a supplementary check alongside human review.
False positives are possible - use your judgment.

Scope summary

  • Reviewed pull request diff only (74454ed9378f8e28fc849427ede604a0058e07db...8d62462ec8b39b8e3fe2f6b6d7256cdeba35148b, exact PR three-dot diff)
  • Model: gpt-5.6-sol

💡 Click "edited" above to see previous reviews for this PR.


Review Summary

Overall Risk: HIGH

Findings

[HIGH] Automated review incomplete

  • Category: Other
  • Description: The automated review produced no usable result for 74454ed9378f8e28fc849427ede604a0058e07db...8d62462ec8b39b8e3fe2f6b6d7256cdeba35148b (workflow run 34185643704; reason: codex-job-timeout, elapsed: unknown, budget: 9 minutes).
  • Impact: The pull request has not received complete automated security, correctness, and reliability analysis.
  • Recommendation: Require human review before merging. Do not treat this result as approval-free or low risk.

Notes

Human review is required because the bounded automated review was incomplete.


Generated by Codex Security Review |
Triggered by: @rl-block |
Review workflow run

Storage for release channels and the rollouts that enforce them, aligned
with the merged RolloutService contract (#1014): assignments and rollouts
are keyed by canonical (manufacturer, model) pairs compared with an
ASCII-only fold, firmware is identified by payload checksum with the file's
target metadata snapshotted onto the assignment, every pair carries an
assignment generation that survives clearing, rollouts carry a revision that
a trigger advances on every change to the row or its devices, plus actor
attribution and delegated-control columns. Membership views resolve ties to
no channel and expose every conflicting relation; managed-deployment
provenance is recorded per device. The files service gains the two lookups
the domain needs for checksum identity.
@rl-block
rl-block force-pushed the rollout/01b-release-channel-schema branch from fe0c808 to 8d62462 Compare September 8, 2026 03:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-policy: needs-review Managed by the Review Policy workflow. server

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant