Skip to content

Upgrade hotcell to 0.6.0 - #3145

Merged
flavorjones merged 5 commits into
mainfrom
card-5280-hotcell-0-6-0
Oct 2, 2026
Merged

flavorjones merged 5 commits into
mainfrom
card-5280-hotcell-0-6-0

Conversation

@flavorjones

Copy link
Copy Markdown
Member

Motivation

This upgrades hotcell from 0.4.1 to 0.6.0 (v0.5.0 and v0.6.0 release notes). The 0.6.0 gems ship code that Fizzy wrote for itself, and the release's upgrading notes say to delete the application's own copies:

  1. Yabeda::HotCell moves into the new yabeda-hotcell gem.
  2. The per-call log line moves into HotCell::LogSubscriber.
  3. The /hotcellz endpoints move into HotCell::HealthController and HotCell::DiagnosticsController.
  4. The echo and reopen operations copied from hotcell's examples become hot_cell/health_operations.

Item 2 is not optional. The 0.6.0 railtie attaches HotCell::LogSubscriber, and our subscriber also writes that line, so without this change every call is logged twice.

Details

  • Upgrade hotcell to 0.6.0 pins the app's and the cell's gems to 0.6.0. In the cell's bundle, image_processing moves to 2.2.0, which activestorage-hotcell-server 0.6.0 requires. The accessory is pinned to cell image d8c4c8004de0.
  • Record HotCell metrics with the yabeda-hotcell gem deletes saas/lib/yabeda/hot_cell.rb. The gem defines the same metrics with the same names, tags and buckets, so dashboards and alerts are unchanged. The gem sets a killed gauge for every cause on the first scrape, where ours set one only after that cause's first kill. The log line now omits null fields, adds exception when one escapes the call, and escapes non-ASCII.
  • Serve /hotcellz from hotcell's health controllers keeps both paths. /hotcellz behaves as before. /hotcellz/test is now HotCell::DiagnosticsController with AdminController as its parent, with these effects:
    • A signed-out caller is redirected to sign in. Before, it got a 403.
    • The JSON nests the checks under cells.active_storage and adds healthy. It no longer includes root.
    • The probes are health.echo and health.reopen, which is now their operation label in hotcell_requests_total.
    • In development, a reload of AdminController does not reach the diagnostics controller, which resolves its parent once, when it loads. After changing AdminController's filters, restart the server for them to apply there.
  • Drop mini_magick from the SaaS bundle removes the gem. Fizzy needed it only because activestorage-hotcell-client 0.4.1 loaded its ImageMagick transformer unconditionally.

Additional information

The pre-deploy hook reboots the cell before the app. Until the app containers are replaced, a 0.4.1 app gets unsupported from /hotcellz/test, because the 0.6.0 cell no longer has example.echo or example.reopen. Conversions keep working, because both releases use wire protocol version 1.

A client that hits a full cell now records capacity in cases where it used to record unavailable (see the v0.6.0 notes). Add capacity to any alert that uses unavailable as the saturation signal.

Cell image d8c4c8004de0 is built locally only. The pre-build kamal hook publishes it on the next deploy.

The upgrade plans are in the 🤖 comments below and in 37signals-hq: v0.4.1..v0.5.0, v0.5.0..v0.6.0, and image_processing v2.1.0..v2.2.0.

Pin the app's and the cell's hotcell gems to 0.6.0, and pin the
accessory to the cell image this tree builds. `image_processing` moves
to 2.2.0 in the cell's bundle, because `activestorage-hotcell-server`
0.6.0 requires it.
Since the upgrade to 0.6.0, `HotCell::LogSubscriber` and our
`Yabeda::HotCell` both logged each HotCell call, so each call appeared
twice in the log. Replace `Yabeda::HotCell` with the `yabeda-hotcell`
gem, which records the same metrics and writes no log line.
hotcell 0.6.0 ships `HotCell::HealthController`,
`HotCell::DiagnosticsController`, and the `health.echo` and
`health.reopen` operations, which do what our `HotcellzController`,
`Fizzy::Saas::Cell.diagnostics` and copied example operations did. Route
`/hotcellz` and `/hotcellz/test` to the gem's controllers, make
`AdminController` the diagnostics controller's parent, and load the
operations from `hot_cell/health_operations`.

`/hotcellz/test` now redirects a signed-out caller to sign in instead of
answering 403. Its JSON nests each cell's checks under `cells` and no
longer includes `root`.
`activestorage-hotcell-client` loaded its ImageMagick transformer even
when unused, so the bundle needed `mini_magick` though nothing called
it. Since 0.6.0 the gem loads that transformer only when the app
references it, so remove `mini_magick`.
Copilot AI balanced review requested due to automatic review settings October 1, 2026 23:22
@flavorjones

Copy link
Copy Markdown
Member Author

🤖 Upgrade Plan: hotcell v0.4.1..v0.5.0

Upgrade Plan: hotcell v0.4.1..v0.5.0 for fizzy

  • Date: 2026-10-01
  • Gem: hotcell (hotcell-client, activestorage-hotcell-client, hotcell-server, activestorage-hotcell-server, hotcell-core)
  • Range: v0.4.1 (46bd334d)..v0.5.0 (affe3f2f), 3 commits
  • Target: fizzy, worktree fizzy--card-5280-hotcell-0-6-0 (branch card-5280-hotcell-0-6-0, at a703bf1d), on 0.4.1

Summary

  • Total commits: 3
  • No impact: 2 (d0ead86f version bump to 0.5.0.dev, affe3f2f release v0.5.0)
  • Analyzed, not affected: 0
  • Requires mitigation: 1 (849f358c, hotcell#63): pin bumps and an alerting check, no code change

The upgrade needs no Fizzy code change. Fizzy pins every hotcell gem exactly at "0.4.1", so
Gemfile.saas and saas/hotcell/Gemfile must change along with both lockfiles. The one
behavioral change is server-side: the cell's supervisor now forks a short-lived sweeper process to
delete scratch trees that killed requests left behind. It is on by default.

Commits Requiring Mitigation

849f358c: Sweep a killed request's tree from a supervisor-forked sweeper (hotcell#63)

Impact: unlikely impact

What changed (from the v0.4.1..v0.5.0 diff):

  • HotCell::Supervisor checks every sweep_interval seconds (new Configuration setting, default 10, must be positive) for slots holding a discarded tree. When it finds one, it forks a HotCell::Sweeper to delete them. The sweeper runs under limits.deadline, the same as a worker.
  • In 0.4.1, only the next worker on the same slot deleted the tree, so a slot whose every request was killed filled the scratch.
  • New log events, with their levels in HotCell::Log::LEVELS: sweeper.forked (INFO), sweeper.deadline (WARN), sweeper.unforkable (ERROR), sweeper.crashed (ERROR), sweeper.died (WARN), scratch.swept (INFO).
  • slot.unswept now logs at WARN. In 0.4.1 it was absent from LEVELS and defaulted to INFO. A worker no longer emits it when the sweeper deleted the tree first.
  • Filesystem.remove_tree treats an already-gone tree as success. Slot#discarded and Slot#discarded? are new.

Matched in:

  • Gemfile.saas:14-15: gem "hotcell-client", "0.4.1" and gem "activestorage-hotcell-client", "0.4.1"
  • saas/hotcell/Gemfile:12-13: gem "hotcell-server", "0.4.1" and gem "activestorage-hotcell-server", "0.4.1". This bump brings the sweeper into the cell image.
  • saas/config/deploy.yml:110: pids-limit: 512, pinned by saas/test/lib/hotcell_accessory_test.rb:27. With concurrency 4 (saas/hotcell/config.rb), the sweeper adds at most one process at a time. The limit leaves ample headroom, so it needs no change.

No matches for sweep_interval, HotCell::Filesystem, remove_tree, sweeper.*, scratch.swept, slot.unswept, pids.max or ulimit -u in app/, lib/, test/, config/ or saas/. No bundled gem other than hotcell references them.

Mitigation:

  1. Change all four pins from "0.4.1" to "0.5.0" in Gemfile.saas and saas/hotcell/Gemfile.
  2. Run BUNDLE_GEMFILE=Gemfile.saas bundle lock --update hotcell-client activestorage-hotcell-client hotcell-core, then bundle lock --update hotcell-server activestorage-hotcell-server hotcell-core in saas/hotcell/. Both lockfiles, including their sha256 checksum lines, must show 0.5.0. saas/test/lib/hotcell_lockfiles_test.rb fails when the two lockfiles resolve different hotcell-core versions.
  3. Leave sweep_interval unset. Fizzy has no reason to move it from the default of 10 seconds.
  4. Check log-based alerting outside the repo for rules that key on WARN or ERROR from the cell accessory. slot.unswept moved from INFO to WARN, and the new sweeper.* events log at WARN or ERROR. saas/lib/yabeda/hot_cell.rb reads only the control-socket counters, so the metrics are unaffected.
  5. Verify: run the saas tests (hotcell_lockfiles_test.rb, hotcell_image_test.rb, hotcell_accessory_test.rb, saas/test/hotcell-check-test), and run saas/hotcell/bin/check against the rebuilt image. In bin/dev, upload an image and confirm a variant renders through the cell.

Override audit

None of Fizzy's hotcell surface depends on what the range changed. The range changed hotcell-server
(configuration.rb, filesystem.rb, log.rb, server.rb, slot.rb, supervisor.rb, new
sweeper.rb), docs/DEPLOYMENT.md, docs/LOGS.md and version constants.

  • saas/lib/yabeda/hot_cell.rb reads the running, queued, queue_high_water, cancelled, uptime_s and killed_by counters. The sweeper adds no counter.
  • saas/hotcell/config.rb sets only HotCell.limits. sweep_interval is a Configuration option, and its default applies.
  • saas/config/deploy.yml:90 mounts /var/lib/hotcell-scratch:/tmp (4G, noexec). The sweeper unlinks as the same uid inside that mount.
  • Fizzy has no cron or find -delete cleanup for leftover scratch trees, so the sweeper makes no Fizzy workaround redundant.
  • saas/app/controllers/hotcellz_controller.rb, saas/config/routes.rb, saas/lib/fizzy/saas/cell.rb, saas/lib/fizzy/saas/engine.rb, saas/Dockerfile, saas/Procfile.dev, saas/bin/setup, saas/hotcell/Dockerfile, saas/hotcell/bin/*, saas/hotcell/operations/*.rb, config/initializers/vips.rb and the remaining saas/test/ files use only the client or control API and pin no version.

The in-range assessments contain no CVE or GHSA identifiers.

No Impact (Skipped)

Commit Summary
d0ead86f version bump to 0.5.0.dev
affe3f2f Release v0.5.0 (version constants and CHANGELOG heading only)

@flavorjones

Copy link
Copy Markdown
Member Author

🤖 Upgrade Plan: hotcell v0.5.0..v0.6.0

Upgrade Plan: hotcell v0.5.0..v0.6.0 for fizzy

  • Date: 2026-10-01
  • Gem: hotcell (hotcell-client, activestorage-hotcell-client, hotcell-server, activestorage-hotcell-server, hotcell-core, new yabeda-hotcell)
  • Range: v0.5.0 (affe3f2f)..v0.6.0 (ffc92154), 22 commits
  • Target: fizzy, worktree fizzy--card-5280-hotcell-0-6-0 (branch card-5280-hotcell-0-6-0)

Fizzy is on 0.4.1, not 0.5.0: Gemfile.saas:14-15 and saas/hotcell/Gemfile:12-13 pin "0.4.1"
exactly. The upgrade therefore also crosses v0.4.1..v0.5.0. Those commits already had recon
assessments; the only one above "no impact", 849f358c (hotcell#63),
is analyzed below and needs no action.

Summary

  • Total commits: 22 (plus 3 from v0.4.1..v0.5.0)
  • No impact: 11 (skipped at recon)
  • Analyzed, no action: 2 (c8e10ac8, 6263bcbf), plus 849f358c from v0.4.1..v0.5.0
  • Requires mitigation: 9

The upgrade is not a lockfile bump. Without code changes, fizzy will log every HotCell call twice, and the
cell bundle will not resolve. The release's "Upgrading" steps each replace a fizzy file whose behavior
upstream copied almost line for line. The switch costs fizzy little, with one exception: upstream's
diagnostics controller is unauthenticated by default, and the alternative parent class answers a
signed-out caller with a redirect where fizzy answers 403.

Tasks

  • T1. Bump pins by hand to "0.6.0": Gemfile.saas:14-15 and saas/hotcell/Gemfile:12-13. saas/hotcell/bin/build:60-76 cannot do this alone. Its sed rewrites only lockfile lines containing "hotcell", so with the Gemfile still pinning "0.4.1" Bundler resolves back to 0.4.1 and the check at :72 fails.
  • T2. Cell bundle: BUNDLE_GEMFILE=saas/hotcell/Gemfile bundle update --conservative hotcell-server activestorage-hotcell-server hotcell-core image_processing. image_processing must reach ≥ 2.2.0 (saas/hotcell/Gemfile.lock:6,23 is 2.1.0). See 1ff4726f.
  • T3. App bundle: BUNDLE_GEMFILE=Gemfile.saas bundle update --conservative hotcell-client activestorage-hotcell-client hotcell-core, plus yabeda-hotcell once T4 adds it. Gemfile.saas.lock's image_processing (2.1.0) does not have to move: activestorage-hotcell-client depends only on hotcell-client and activestorage (gemspec). saas/test/lib/hotcell_lockfiles_test.rb:9 requires both lockfiles to show the same hotcell-core.
  • T4. Add gem "yabeda-hotcell", "0.6.0" to Gemfile.saas. Delete saas/lib/yabeda/hot_cell.rb and saas/test/lib/yabeda/hot_cell_test.rb. Keep saas/lib/fizzy/saas/engine.rb:174-175. See D1.
  • T5. Decide the health endpoints (D2), then replace the operations: add require "hot_cell/health_operations" to a cell operation file (for example a new saas/hotcell/operations/health.rb). Delete saas/hotcell/operations/echo.rb, reopen.rb, and the client classes at saas/lib/fizzy/saas/cell.rb:155-163.
  • T6. Delete the mini_magick line, Gemfile.saas:16.
  • T7. Add yabeda-hotcell to the update command in the help text at saas/hotcell/bin/build:35. Update saas/README.md:71-75,127 for /hotcellz, example.echo, Cell.diagnostics(work: true) and "four checks".
  • T8. Re-run saas/hotcell/bin/build. The image tree hash changes because Gemfile.lock and operations/ change (saas/hotcell/bin/image:15-16), so saas/test/lib/hotcell_accessory_test.rb:78-83 and the pin in saas/config/deploy.yml:77 move.
  • T9. Verify:
    • The saas suite passes.
    • bin/dev in SaaS mode boots with no hotcell: version-skew warning, and each call logs exactly one HotCell (…ms) {…} line.
    • /metrics still exposes hotcell_requests_total, hotcell_perform_seconds and the gauges.
    • The diagnostics endpoint returns 200 for a staff user and 403 (or whatever D2 decides) when signed out.
    • An image variant renders through the cell.

Decisions

D1: adopt yabeda-hotcell (recommended: yes)

Fizzy's saas/lib/yabeda/hot_cell.rb is the file upstream turned into the gem. Every metric matches,
so no Prometheus series is renamed and no dashboard or alert changes. Both declare group
:hotcell, with these metrics:

  • counter :requests, tags cell operation code cause
  • histogram :perform, seconds, tags cell operation, buckets [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 120]
  • gauges up running queued queue_high_water cancelled uptime_seconds (tag cell) and killed (tags cell cause), all aggregation: :most_recent

Both implementations compute values the same way: code || "ok", cause.to_s, and perform_ms / 1000.0 defaulting to 0. They share the same collect
loop. Compare fizzy's file with upstream.

Fizzy gains or loses the following by switching:

  • Gains: killed series from the first scrape. Upstream iterates HotCell::Codes::PERMANENT_BY_CAUSE (fsize memory deadline crashed) and sets missing causes to 0. Fizzy sets only the causes present in killed_by, so a cause's series does not exist until its first kill. The label values are the same strings.
  • Loses: the log line. log_perform (saas/lib/yabeda/hot_cell.rb:68,75-86) has no counterpart in the gem. HotCell::LogSubscriber replaces it; see 2ce02743.
  • No change: errors are reported to ActiveSupport.error_reporter, which is Rails.error in Rails.
  • Shared gap: an escaped exception carries no :code, so both implementations count it as code="ok" with a 0-second histogram sample. In 0.6.0 those events now carry cell and operation, so they move from the {cell="",operation=""} series into the real cell's code="ok" series.

Adopting the gem means fizzy's file must be deleted. fizzy-saas is a path gem, so saas/lib is on
$LOAD_PATH. Both the gem's yabeda-hotcell.rb and engine.rb:174 call require "yabeda/hot_cell", and
that require loads whichever file comes first on the load path. Dependencies are compatible:

  • yabeda >= 0.12: fizzy has 0.14.0.
  • hotcell-client exactly matching.
  • Ruby ≥ 3.3: fizzy runs 3.4.8.

D2: health endpoints (recommended: keep HotcellzController, delegate to HotCell.diagnose)

Fizzy routes get "hotcellz" to hotcellz#show (unauthenticated) and get "hotcellz/test" to
hotcellz#test (staff only), in saas/config/routes.rb:10-11. No Kamal healthcheck, hook or script in
the repo calls either path. Only docs do. An outside uptime monitor cannot be ruled out from the repo.

show and HotCell::HealthController. Both return plain-text OK/FAIL with 200/503 and use only the control socket.

  • Upstream returns FAIL when no cell is registered. Fizzy always registers one.
  • Gain from switching: fizzy's show calls cell.describe on every poll (saas/lib/fizzy/saas/cell.rb:85). In 0.6.0 that method runs the new version-skew warning (163c62c1), so every poll during a mixed-version rollout writes a warning to $stderr. HotCell.logger is unset in fizzy, so it defaults to $stderr. Upstream's Cell#diagnose uses a raw control(DESCRIBE) to skip those warnings.
  • HotCell::HealthController < ActionController::Base, so none of ApplicationController's filters run. That is harmless for this action.

test and HotCell::DiagnosticsController.

Fizzy hotcellz#test Upstream HotCell::DiagnosticsController#show
Body flat {at, host, root, describe, metrics, echo, reopen} {at, host, healthy, cells: {"active_storage" => {describe, metrics, echo, reopen}}}
Round-trip result {bytes, staged, echoed: true} {bytes, staged}
Operations example.echo, example.reopen health.echo, health.reopen
Cell off "HOTCELL_ROOT is unset, so no cell is configured" "no socket directory, so this cell is off"
Wrong bytes "returned N bytes, not the M sent" "returned other bytes than it was sent"
Exceptions message as-is Failure.one_line
Auth signed out → 403, non-staff → 403 (ensure_staff_access) none by default (parent ActionController::Base, cells.rb:88-92)

Both use the cell's timeout and control_timeout, and neither caches. Fizzy loses root and gains
healthy. The probe operation rename also changes the operation label on probe calls in
hotcell_requests_total and hotcell_perform_seconds.

Upstream offers these ways to authenticate the diagnostics route, plus a third that keeps fizzy's controller:

  1. HotCell.diagnostics_controller_parent = "AdminController". AdminController gives staff-only access, but a signed-out caller gets a 302 to login instead of 403. Fizzy's controller comment rejects that on purpose ("a prober wants an answer"), and saas/test/controllers/hotcellz_controller_test.rb:30 asserts the 403. A development reload of AdminController also does not reach the parent.
  2. Subclassing HotCell::DiagnosticsController. Its parent is ActionController::Base, so Current.identity is never resumed and fizzy's staff check always answers 403, unless the subclass re-includes the authentication concern.
  3. Recommended: keep HotcellzController, its routes and its auth, and have show/test call HotCell.diagnose / HotCell.diagnose(work: true) and healthy?. This keeps the path and the 403 and gains the quiet poll. Delete Fizzy::Saas::Cell.diagnostics, round_trip and the Echo/Reopen clients (saas/lib/fizzy/saas/cell.rb:79-163). The response body changes to upstream's nested shape.

The tests that change with any of these:

  • In saas/test/controllers/hotcellz_controller_test.rb:
    • the Fizzy::Saas::Cell.echo stub at :22
    • the flat keys at :50
    • the four checks and "HOTCELL_ROOT is unset" at :75-76
    • parsed_body["echo"] at :85-86,91
  • saas/test/lib/cell_test.rb:26-45 goes with the code it tests.

Rollout window

The pre-deploy hook reboots the cell before the app. A 0.4.1 app against a 0.6.0 cell without
example.* gets unsupported on /hotcellz/test until the app is replaced. Only the diagnostic is
affected. The wire protocol is version 1 in both releases. The 0.4.1 app also logs no skew warning,
because the warning is a 0.6.0 client feature.

Commits Requiring Mitigation

2ce02743: Log every HotCell call to the Rails log by default (hotcell#78)

Impact: likely impact
Matched in:

  • saas/lib/yabeda/hot_cell.rb:52: ActiveSupport::Notifications.subscribe "perform.hot_cell"
  • saas/lib/yabeda/hot_cell.rb:78: ::Rails.logger.info " HotCell (#{duration_ms}ms) " + labels.merge(...).to_json

The railtie's hot_cell.log_subscriber initializer attaches HotCell::LogSubscriber whenever Rails is
loaded. Fizzy loads it: saas/lib/fizzy/saas/cell.rb:5 requires hot_cell/client. Every call logs twice
until fizzy's line goes. The two lines have the same prefix, the same level (info) and the same destination (Rails.logger).
Upstream's line differs from fizzy's
(log_subscriber.rb):

  • Upstream drops nil fields. Fizzy always writes "cause":null, "stderr":null and "bytes_in":null.
  • Upstream adds exception (the class name). For an escaped exception it omits code, where fizzy writes "code":"ok".
  • Upstream moves stderr to the end.
  • Upstream encodes with JSON.generate(..., ascii_only: true), which escapes non-ASCII. AS to_json escapes < > & and U+2028/2029. Only the rendering of stderr changes.

Log queries that match on a literal "cause":null or rely on field order would break.

Mitigation: T4 (delete fizzy's file). To keep fizzy's line instead, add HotCell::LogSubscriber.detach_from :hot_cell to an initializer.


a55a6a24: Ship Yabeda metrics as the yabeda-hotcell gem (hotcell#77)

Impact: unlikely impact
Matched in: saas/lib/yabeda/hot_cell.rb (whole file; group :hotcell at :8, perform.hot_cell at :52, HotCell.cells.each_value at :35); saas/lib/fizzy/saas/engine.rb:174-175.

Mitigation: D1 and T4.


fc98fb2d: Ship health and diagnostics controllers for registered cells (hotcell#70)

Impact: unlikely impact (opt-in)
Matched in: saas/config/routes.rb:10-11; saas/app/controllers/hotcellz_controller.rb:6-33; saas/lib/fizzy/saas/cell.rb:79-152.

Mitigation: D2. Do not route HotCell::DiagnosticsController with its default parent, because every request takes a worker and the response exposes the hostname and cell description.


ea48cfc2: Ship the two health probes instead of leaving them as examples (hotcell#39)

Impact: unlikely impact
Matched in:

  • saas/hotcell/operations/echo.rb:3,7-8: copied from the now-deleted examples/operations/echo.rb, answers to example.echo
  • saas/hotcell/operations/reopen.rb:3,9-10: copied from examples/operations/reopen.rb, answers to example.reopen
  • saas/lib/fizzy/saas/cell.rb:157,162: the client classes for those names

The perform bodies and {bytes:, staged:} results match
health_operations.rb
exactly. Only the names differ. The copies keep working under example.*. A copy renamed to health.*
while the require is present raises HotCell::ConfigurationError at boot.

Mitigation: T5. Remove the copies and the require in the same change.


1ff4726f: Require image_processing 2.2.0 and drop the transform rename (hotcell#82)

Impact: unlikely impact
Matched in: saas/hotcell/Gemfile.lock:6,23: image_processing (>= 2.1.0) / image_processing (2.1.0).

activestorage-hotcell-server now requires image_processing >= 2.2.0, and transforms write directly to
the final path. Fizzy's saas/hotcell/operations/active_storage.rb:6-18 only requires files and calls
Vips.block on loaders, so the change does not touch it. The one behavior change, an ImageMagick format with no name such as jfif now failing as
unreadable, does not apply to fizzy, which uses Vips. config/initializers/vips.rb configures app-side
Vips only when no cell is enabled and is unaffected.

Mitigation: T2.


be7b286a: Stop requiring mini_magick for apps on the Vips transformer (hotcell#74)

Impact: unlikely impact
Matched in: Gemfile.saas:16: gem "mini_magick", require: false # activestorage-hotcell-client loads the ImageMagick transformer even when unused; saas/lib/fizzy/saas/cell.rb:71 uses only Transformers::Image::Vips.

Transformers::Image::Magick is now an autoload, so the comment no longer holds. Nothing else in
Gemfile.saas.lock depends on mini_magick. The cell still gets mini_magick through
activestorage-hotcell-server's own dependency.

Mitigation: T6.


163c62c1: Warn at boot when a cell runs another hotcell release (hotcell#90)

Impact: unlikely impact
Matched in: saas/lib/fizzy/saas/engine.rb:72 (::HotCell.describe_cells in after_initialize); saas/lib/fizzy/saas/cell.rb:85 (cell.describe on every /hotcellz poll).

A 0.4.1 cell reports no server_version, so a 0.6.0 app warns against it at boot and, through
cell.rb:85, on every health poll. Upstream provides no option to turn the warning off.

Mitigation: deploy client and cell on the same release (T1-T3, T8). D2's recommended option removes the per-poll warning.


cc8dc2ed: Pre-fork workers so requests don't wait on fork (hotcell#67)

Impact: likely impact
Matched in: saas/hotcell/config.rb:6 (concurrency: 4); saas/config/deploy.yml:93-110 (memory: 2g, memory-swap: 2g, pids-limit: 512).

The supervisor now forks 4 idle workers at boot and runs Process.warmup first. The accessory memory
is already sized from "concurrency × peak RSS" (deploy.yml:93-94), so 4 idle workers sit below the
sized ceiling, and 6 processes is far below 512 pids. hotcell_running counts busy children only, so
idle workers do not raise it. On the client, a broken pipe while sending to a full cell now returns
capacity instead of unavailable. Both codes are transient, so ProcessingUnavailable still raises.
Saturation moves from code="unavailable" to code="capacity" in hotcell_requests_total.

Mitigation: none in code. After deploy, check idle cell memory, and check any alert keyed on code="unavailable" as the saturation signal.


ffc92154: Release v0.6.0

Impact: unlikely impact
Matched in: saas/hotcell/Gemfile:12-13, Gemfile.saas:14-15 (exact "0.4.1" pins).

Mitigation: T1-T3, T8.


Analyzed, No Action

Commit Summary Impact Level Why no action
c8e10ac8 (#73) hotcell.describe adds server_version unlikely impact engine.rb:72 discards the result; no test compares the full describe hash
6263bcbf (#91) macOS supervisor rescues Errno::EPERM killing a zombie group unlikely impact No match; production is Linux
849f358c (#63, v0.4.1..v0.5.0) Supervisor forks a sweeper every sweep_interval (10s) unlikely impact One extra process against pids-limit: 512; config.rb needs no sweep_interval

Override audit

All findings are folded into D1, D2 and the commits above. In summary:

  • saas/lib/yabeda/hot_cell.rb is redundant, with metrics identical to upstream's. Once yabeda-hotcell is installed it is broken by the load-path clash, and it double-logs from 0.6.0 even without the gem.
  • saas/test/lib/yabeda/hot_cell_test.rb is broken after the switch. Its log tests at :85-114 test removed code, and :37 looks up cause: :memory where the gem sets "memory".
  • HotcellzController and Fizzy::Saas::Cell.diagnostics are a superset in auth only. Their checks are the same as HotCell.diagnose. See D2.
  • saas/hotcell/operations/echo.rb and reopen.rb are redundant copies of hot_cell/health_operations under other names.
  • Gemfile.saas:16 (mini_magick) is redundant.
  • saas/hotcell/bin/build is insufficient for this bump. See T1.
  • saas/hotcell/config.rb, saas/hotcell/Dockerfile (the HEALTHCHECK still uses the shipped hotcell-health), config/initializers/vips.rb, saas/.kamal/hooks/*, saas/Procfile.dev, bin/dev and saas/bin/setup are not at risk.

No CVE or GHSA identifiers appear in the range's assessments. That is not evidence of no security fixes.

No Impact (Skipped)

11 commits assessed as "no impact" during recon, not analyzed against the app:

  • 805dde16
  • 93f4afb2
  • d467bbcc
  • 715602b2
  • 079e8115
  • 33559aa5
  • 6c775e26
  • d267bcd1
  • a005bbb8
  • 879be9a3
  • f1618942

These are version bumps, CI, docs, dev scripts, test helpers, and the root lockfile.

Transitive Dependency Upgrades

Gem From To Commits Mitigations Plan
image_processing (cell bundle only) 2.1.0 2.2.0 4 0 fizzy-20261001-image_processing_v2.1.0..v2.2.0.md
yabeda-hotcell (new, app bundle) — 0.6.0 — — covered by a55a6a24 above
mini_magick (removed from app bundle) 5.3.3 — — — covered by be7b286a above

No other gem moved in Gemfile.saas.lock or saas/hotcell/Gemfile.lock.

Execution (2026-10-01, branch card-5280-hotcell-0-6-0)

  • T1–T3, T8: Upgrade hotcell to 0.6.0. T8 rebuilt the cell image; saas/config/deploy.yml pins d8c4c8004de0 at the head of the branch.
  • T4, T7 (build help): Record HotCell metrics with the yabeda-hotcell gem.
  • T5, T7 (README): Serve /hotcellz from hotcell's health controllers. D2 executed as option 1, not the recommended option 3: the release notes ask apps to replace their health endpoints with the gem's controllers, so /hotcellz routes to HotCell::HealthController and /hotcellz/test to HotCell::DiagnosticsController with HotCell.diagnostics_controller_parent = "AdminController". A signed-out caller of /hotcellz/test is redirected to sign in instead of getting 403; the PR flags this for review.
  • T6: Drop mini_magick from the SaaS bundle.
  • T9: a dev cell booted from the branch registers health.echo and health.reopen; HotCell.diagnose(work: true) passes all four checks; each call writes one HotCell (…ms) line; a vips variant renders through the cell. The built image boots with the same operation list.

@flavorjones

Copy link
Copy Markdown
Member Author

🤖 Transitive Upgrade Plan: image_processing v2.1.0..v2.2.0

Upgrade Plan: image_processing v2.1.0..v2.2.0 for fizzy

  • Date: 2026-10-01
  • Gem: image_processing
  • Range: v2.1.0..v2.2.0 (4 commits)
  • Target: fizzy (hotcell cell bundle only — saas/hotcell/Gemfile.lock)

Scope

This gem moves only in the cell's lockfile, pulled by activestorage-hotcell-server 0.6.0
(image_processing >= 2.2.0). The app lockfiles (Gemfile.lock, Gemfile.saas.lock) keep
their current version and are out of scope.

The cell loads image_processing/vips only. saas/hotcell/operations/active_storage.rb
requires the gem's vips transformer and analyzer; nothing requires image_processing/mini_magick
or magick_operation.rb, and the image has no ImageMagick binary.

Summary

  • Total commits: 4
  • No impact: 2 (skipped at recon)
  • Analyzed, not affected: 0
  • Requires mitigation: 0 (2 affect the cell; both are the behavior 0.6.0 depends on)

The upgrade is safe. The two code commits change what happens when a pipeline with #convert
saves to a destination that has no extension. That is exactly how
activestorage-hotcell-server 0.6.0 now writes every transform, and the reason it raised its
floor to 2.2.0. Nothing in fizzy needs to change.

Commits Affecting the Cell

5827e836: Save in the #convert format to an extensionless destination (janko/image_processing#150)

Impact: unlikely impact
Matched in:

  • activestorage-hotcell-server/lib/active_storage/hot_cell/server/transforming.rb:18 @ v0.6.0 —
    pipeline(source, format, operations).call(destination: destination.path), where
    destination.path is an extensionless scratch path
  • transforming.rb:36 @ v0.6.0 — .convert(format)

Pipeline#call now passes format: through Processor.call to save_image when #convert is
set and the destination has no extension. On vips, 2.1.0 raised Vips::Error here; 2.2.0 picks
the saver from the format. On mini_magick (not loaded in the cell), 2.1.0 silently wrote the
source format.

hotcell 1ff4726f
("Require image_processing 2.2.0 and drop the transform rename", #82) removed the old
workaround, which encoded to a sibling path with the format as its extension and renamed it into
place. With 0.6.0 on image_processing 2.1.0, every vips transform would fail. The lockfile
resolves 2.2.0, so this cannot happen in practice.

Mitigation: none for fizzy. The recon mitigations (update custom Processor.call /
save_image overrides, restrict #convert to plain format names) do not apply: the cell has no
such overrides, and the format comes from hotcell's CONTENT_TYPES map.

92086c5d: Handle AVIF format on extensionless destinations

Impact: unlikely impact
Matched in:

  • transforming.rb:18 and :36 @ v0.6.0 (same call site as above)
  • activestorage-hotcell-server/lib/active_storage/hot_cell/server/operation.rb:29 @ v0.6.0 —
    "avif" => "image/avif"

Vips only. On an extensionless destination, #convert("avif") now saves through libvips' target
API (vips_foreign_find_save_target with a .avif suffix plus write_to_target), so the output
is AV1 rather than HEIC. A format with no target saver now raises Vips::Error "No known saver"
(libvips 8.9+ required; the cell already requires 8.13+ via Vips.block_untrusted).

Mitigation: none required. Fizzy requests no AVIF variants (no avif or heif reference in
app/, config/, lib/ or saas/). If it ever does, the cell's Dockerfile:35 installs
libvips42 with --no-install-recommends and no explicit AV1 encoder, so an AVIF transform
could fail as unreadable. Check libheif-plugin-aomenc in the image before enabling AVIF
output. I did not verify the image's package set.


Analyzed — No App Impact

None. Both commits above "no impact" match the cell's single transform call site.

No Impact (Skipped)

Commit Summary
8f7b5383 Skip AVIF vips tests when no encoder is present; add libheif-plugin-aomenc to CI. No lib/ change.
d9737fbf Version bump to 2.2.0 and CHANGELOG entry.

Override audit

Override surface for image_processing in the cell: saas/hotcell/operations/active_storage.rb
(sets Transformers::Image::Vips.limits and Vips.block for openslide and tiff) and
saas/hotcell/config.rb (HotCell.limits). Neither touches an API the range changed.

In activestorage-hotcell-server v0.6.0, no code overrides Processor.call or save_image.
transformers/image/vips.rb reopens the output with Vips::Image.new_from_file, which sniffs
content rather than extension, so the extensionless output describes correctly.

Residual delta: none for fizzy. The only workaround the range made redundant (the
extension-and-rename in transforming.rb) was already removed in hotcell 1ff4726f. Any
leftover Destination#path(extension:) / Destination#adopt helpers are hotcell's cleanup.

Advisories

No CVE- or GHSA- identifiers in any assessment. This is not evidence of absence; the range is
a behavior fix, a test fix and a release bump.

Plan

  1. Accept the transitive bump as part of the activestorage-hotcell-server 0.6.0 upgrade;
    nothing to change in app code.
  2. Verification is the cell's existing vips transform coverage: a variant request must return
    bytes in the requested format. A failure there on 2.2.0 would point at this range.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approved

The upgrade consistently adopts the documented 0.6.0 APIs, preserves authorization and telemetry, and keeps the deployment pin synchronized.

Review effort: Balanced
Findings: None

What changed in this PR

Upgrades HotCell to 0.6.0 and replaces Fizzy’s duplicated diagnostics, telemetry, and health operations with gem-provided implementations.

Changes:

  • Upgrades application and cell dependencies to HotCell 0.6.0.
  • Adopts packaged health controllers, operations, logging, and Yabeda metrics.
  • Updates tests, documentation, routes, and deployment image pin.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
Gemfile.saas Adds the upgraded clients and Yabeda integration.
Gemfile.saas.lock Locks application dependencies to 0.6.0.
saas/​README.md Documents the gem-provided diagnostics behavior.
saas/​app/​controllers/​hotcellz_controller.rb Removes the superseded controller.
saas/​config/​deploy.yml Pins the verified upgraded cell image.
saas/​config/​routes.rb Routes checks to HotCell controllers.
saas/​hotcell/​Gemfile Upgrades cell server dependencies.
saas/​hotcell/​Gemfile.lock Locks server and image-processing upgrades.
saas/​hotcell/​bin/​build Includes yabeda-hotcell in upgrade instructions.
saas/​hotcell/​operations/​echo.rb Removes the copied echo operation.
saas/​hotcell/​operations/​health.rb Loads packaged health operations.
saas/​hotcell/​operations/​reopen.rb Removes the copied reopen operation.
saas/​lib/​fizzy/​saas/​cell.rb Removes superseded diagnostics implementation.
saas/​lib/​fizzy/​saas/​engine.rb Configures diagnostics authorization inheritance.
saas/​lib/​yabeda/​hot_cell.rb Removes the duplicated telemetry integration.
saas/​test/​controllers/​hotcellz_controller_test.rb Removes tests for the deleted controller.
saas/​test/​integration/​hotcellz_test.rb Tests the replacement health endpoints.
saas/​test/​lib/​cell_test.rb Removes tests for deleted diagnostics helpers.
saas/​test/​lib/​hotcell_telemetry_test.rb Verifies logging and metrics remain singular.
saas/​test/​lib/​yabeda/​hot_cell_test.rb Removes tests now owned by the gem.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@flavorjones

Copy link
Copy Markdown
Member Author

Looks good in staging.

@flavorjones
flavorjones merged commit d157d2c into main Oct 2, 2026
13 checks passed
@flavorjones
flavorjones deleted the card-5280-hotcell-0-6-0 branch October 2, 2026 15:00
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.

2 participants