Skip to content

Releases: basecamp/hotcell

v1.0.0

Choose a tag to compare

@flavorjones flavorjones released this 05 Oct 18:52
c24ea28

v1.0.0 / 2026-10-05

Documentation

v0.6.0

v0.6.0 Pre-release
Pre-release

Choose a tag to compare

@flavorjones flavorjones released this 01 Oct 22:32
ffc9215

v0.6.0 / 2026-10-01

Upgrading

Some actions that application developers should consider taking when upgrading from an earlier version:

  • Replace the application's Yabeda integration for HotCell with the yabeda-hotcell gem. The README's "Metrics collection" lists the gem's metrics for comparison with the application's dashboards and alerts.
  • Remove the log line that the application writes for each HotCell call, in a perform.hot_cell subscriber or in its Yabeda integration. HotCell::LogSubscriber writes this line now; see the README's "Application logs".
  • Replace the application's HotCell health endpoints with HotCell::HealthController and HotCell::DiagnosticsController. The README's "Rails healthcheck" shows the routes and the authentication for the diagnostics route.
  • Replace the cell's copies of examples/operations/echo.rb and reopen.rb with require "hot_cell/health_operations", and change the application's clients to call health.echo and health.reopen. See the README's "Rails healthcheck".

HotCell::Server

Added

  • hot_cell/health_operations defines the health.echo and health.reopen operations. An application calls them to make sure that it can use the cell's work socket. A cell accepts them only when one of its operation files requires hot_cell/health_operations. Otherwise, the cell answers unsupported.
  • The hotcell.describe response includes server_version, the version of hotcell-server that the cell runs. Use it to find the version without a shell in the container. (#21)

Improved

  • A request that finds an idle worker no longer waits for fork. The supervisor forks a worker into each free slot at boot, and forks a replacement as soon as it reaps a worker that served a request. A request that waits in the queue still waits for fork. The supervisor runs Process.warmup once, at boot, before the first fork.

Fixed

  • A cell on macOS no longer stops when it kills a worker that started another process. Previously, Process.kill on the worker's process group could raise Errno::EPERM while that process was a zombie. The supervisor did not rescue the error, so the cell stopped and the caller received no response.

HotCell::Client

Added

  • HotCell.describe_cells warns at boot when a cell's hotcell-server version differs from the application's hotcell-client version. (#21)
  • HotCell::LogSubscriber writes one info line to the Rails log for each call. The line contains the cell, the operation, the outcome and both durations. It also contains the byte counts when the client measures them. The railtie attaches the subscriber. See the README's "Application logs".
  • HotCell::HealthController and HotCell::DiagnosticsController check each registered cell. HotCell::HealthController uses only the control socket and returns OK or FAIL. It takes no worker. HotCell::DiagnosticsController also sends health.echo and health.reopen on the work socket. It takes a worker for each round trip. It returns each result as JSON. To use the controllers, add a route for each to the application. Put the diagnostics route behind authentication. To do this, set HotCell.diagnostics_controller_parent in an initializer to the name of an authenticated controller class. Alternatively, subclass HotCell::DiagnosticsController, authenticate in the subclass, and route to the subclass. See the README's "Rails healthcheck".

Fixed

  • The perform.hot_cell event now contains cell and operation when an exception escapes the call. Previously, the event contained neither.
  • The client now returns capacity when a full cell closes the connection before the client finishes sending the request. Previously, the client returned unavailable for the broken pipe.

Yabeda::HotCell

Added

  • New gem yabeda-hotcell. It records Yabeda metrics for each call. On each scrape, it also sets gauges from the counters of each registered cell. Call Yabeda::HotCell.install! once at boot. See the README's "Metrics collection".

ActiveStorage::HotCell::Client

Fixed

  • require "active_storage/hot_cell/client" now works without the mini_magick gem. Previously, it raised LoadError when mini_magick was not installed. The gem now loads Transformers::Image::Magick when the application first references that constant. An application that uses only the Vips transformer can remove mini_magick.

ActiveStorage::HotCell::Server

Improved

  • The transform operations write their output directly to its final path. This removes one file rename from each transform. This change requires image_processing 2.2.0, which the gemspec now specifies. (#4)

Tooling

Changed

  • The example cell accepts health.echo and health.reopen from hot_cell/health_operations. Its own example.echo and example.reopen operations are removed.

Fixed

  • bin/conformance no longer fails intermittently at "offered overload answers capacity" against a healthy cell. Previously, a worker that was still cleaning up after the previous check could take a place that the overload check fills, so the cell did not fill. The check now waits until the cell is idle.
  • bin/example-image and bin/load pass their inputs as arguments, not through a shell. Previously, shell syntax in a checkout path ran as a command in bin/example-image. Shell syntax in a bin/load scenario, duration or thread count ran as a command in the driver container. (#33)

v0.5.0

v0.5.0 Pre-release
Pre-release

Choose a tag to compare

@flavorjones flavorjones released this 09 Sep 20:39
affe3f2

v0.5.0 / 2026-09-09

HotCell::Server

Added

  • The supervisor forks a sweeper every sweep_interval seconds (default 10) to delete the directories killed requests left behind. Before, only the next worker to answer on the same slot deleted them, so a slot whose every request was killed filled the scratch.

Fixed

  • A worker no longer logs slot.unswept when the sweeper deleted the tree first.

v0.4.1

v0.4.1 Pre-release
Pre-release

Choose a tag to compare

@flavorjones flavorjones released this 08 Sep 18:22
46bd334

v0.4.1 / 2026-09-08

Upgrading

Some actions that application developers should consider taking when upgrading from an earlier version:

  • Add --development to the hotcell command in Procfile.dev, or wherever a cell boots as a plain process beside the application. Without it a cell told no TMPDIR sweeps /tmp at boot, deleting every file the developer owns there. See the README's "Run it in development".

HotCell::Server

Added

  • hotcell --development, for a cell booted as a plain process beside the application. The cell never sweeps the directory it is given, TMPDIR or the system temporary directory, both shared with everything else a developer runs. Its scratch is hotcell-<HOTCELL_DIR with slashes as dashes> beneath it, and it refuses to boot when that name is taken by anything but a directory its uid owns. Before, it swept the developer's /tmp, or on macOS the per-user TMPDIR every shell sets. An explicit HOTCELL_WORKSPACE still has its parent swept. Without the flag nothing changes. cell.boot logs the tmpdir in use.

v0.4.0

v0.4.0 Pre-release
Pre-release

Choose a tag to compare

@flavorjones flavorjones released this 08 Sep 14:45
919491d

v0.4.0 / 2026-09-08

Upgrading

Some actions that application developers should consider taking when upgrading from an earlier version:

  • Log stderr from the perform.hot_cell event in the client Rails application. This improves observability and provides forensic evidence about cell crashes.
  • Set MAGICK_MEMORY_LIMIT, MAGICK_MAP_LIMIT and MAGICK_DISK_LIMIT in a cell image that installs ImageMagick. See docs/IMAGEMAGICK.md.

HotCell::Client

Added

  • The perform.hot_cell event carries stderr, the failure's captured stream, beside signal. A subscriber can log the diagnosis of a crash — libgomp: Thread creation failed — on the same line as its cause. Before, that text survived only in the exception's message, so a failure that was discarded rather than retried lost it. The field is already bounded by Failure.sanitize; it is text a tool wrote while processing a hostile file, so write it to a log field and interpolate it nowhere else.

HotCell::Core

Fixed

  • json 3.0.0 support.

HotCell::Server

Added

  • The supervisor empties the scratch at boot: every top-level entry of Dir.tmpdir and of the workspace's parent that the cell's uid owns, so rebooting the accessory clears a scratch a killed tool filled.
  • The cell refuses to boot when HOTCELL_DIR is inside the scratch, when the scratch is missing or reached through a symlink the cell's uid owns, or when HOTCELL_WORKSPACE, TMPDIR or HOTCELL_DIR is not an absolute, normalized path.

Fixed

  • A worker sets TMPDIR to the request's home, and an exec'd tool inherits it. Before, nothing set it, so a library's scratch went to /tmp — outside the slot tree that is removed when a request ends — and a killed worker orphaned every file there until the scratch filled.

ActiveStorage::HotCell::Server

Fixed

  • Every operation sets MAGICK_TMPDIR to the request's TMPDIR, so ImageMagick's pixel cache — from the magick the magick operations run and from the magickload libvips delegates to — is removed with the request, however it ends.
  • MagickOperation forwards the worker's MAGICK_*_LIMIT, TMPDIR and MAGICK_TMPDIR to the magick it spawns, read per request. MiniMagick.restricted_env had dropped them along with the rest of the environment, so an image's MAGICK_DISK_LIMIT bounded ImageMagick inside libvips and not the magick behind analyzers.image.magick and transformers.image.magick, which ran under ImageMagick's defaults — the host's RAM and an unbounded disk — and the request's TMPDIR above never reached it.

Improved

  • The two ImageMagick operations read their input through its descriptor instead of staging a copy of it, so the operation's file_size no longer bounds how large an input they can read. This needs mini_magick 5.4.0 and image_processing 2.1.0, which the gemspec now requires. (#7)

v0.3.1

v0.3.1 Pre-release
Pre-release

Choose a tag to compare

@flavorjones flavorjones released this 02 Sep 17:00
a38c8e9

v0.3.1 / 2026-09-02

HotCell::Core

Fixed

  • Descriptor#fd_path answers with the filename behind the descriptor on macOS rather than /dev/fd/N, where every opener shared one offset and libvips reported a readable HEIC file as unreadable. Linux is untouched. (#50)

ActiveStorage::HotCell::Server

Fixed

  • MagickOperation and VipsOperation classify ImageProcessing::Error as unreadable and permanent. Before, it answered failed, which is transient, so a file the pipeline had already refused was reconverted on every future request. (#42)

v0.3.0

v0.3.0 Pre-release
Pre-release

Choose a tag to compare

@flavorjones flavorjones released this 01 Sep 07:19
d0edad9

v0.3.0 / 2026-09-01

HotCell::Client

Added

  • The installed Dockerfile strips the setuid and setgid bits off every binary in the image as its last root step, so the strip covers anything a RUN above it installed rather than the base image alone. The cell already runs unprivileged under --cap-drop ALL and --security-opt no-new-privileges, which make mount, su and the rest inert, so this removes escalation tools a security image should not carry rather than closing a reachable path. The scaffold also now documents what it deliberately does not do for you: build with docker build --pull, pin a base digest and refresh it on a schedule if you need to name the exact image you shipped, and commit a platform-correct Gemfile.lock and build frozen for a reproducible dependency graph. Frozen mode is off by default so a freshly installed scaffold builds before you have generated a lockfile. An upgrade leaves an existing Dockerfile alone, so a cell installed before this needs the strip added by hand and the image rebuilt.

Breaking

  • HotCell.describe_cells no longer warns about a client whose operation the cell does not carry, and HotCell.clients is gone with it. The check could not tell a client the application calls from one it merely loaded, so a cell carrying a subset of what a gem ships warned on every boot. A request for an operation the cell does not carry is refused as unsupported, naming the operation, and that failure is transient and reaches the application's error reporting. An application that wants a boot check can write one over its own configuration.

Fixed

  • The installed Dockerfile sets OMP_NUM_THREADS and OMP_THREAD_LIMIT to the container's cpus. OpenMP sizes its thread pool from the host's core count, and a container's cpus quota is a CFS quota rather than an affinity mask, so libvips and ImageMagick asked a 98-core host for 98 threads however small the cell's share of it. A thread stack is 8MB of private anonymous memory, which RLIMIT_DATA charges, so the pool alone cleared the cell's memory limit — and libgomp calls exit(1) on the first pthread_create it cannot satisfy. An upgrade leaves an existing Dockerfile alone, so a cell installed before this needs both added by hand and the image rebuilt. docs/DEPLOYMENT.md covers why the guard has to be a test rather than a deploy to beta: the failure exists only at production's core count.

  • The installed Dockerfile applies Debian's pending security patches with an apt-get upgrade after the FROM. docker build --pull takes the newest base tag, but the tag itself can sit behind an advisory already in trixie-security until docker-library/ruby rebuilds it, so a clean build shipped a fixable High that no rebuild of the cell could clear. Upgrading during the build makes the image's patch level its own rather than upstream's release cadence, and stops the class rather than the one advisory. An upgrade leaves an existing Dockerfile alone, so a cell installed before this needs the line added by hand and the image rebuilt.

HotCell::Core and HotCell::Server

Added

  • A worker's file descriptor 2 is a pipe to the supervisor, which drains it as it runs and attaches the last 512 bytes to the worker.killed that reports its death, as hotcell.stderr, and to the failure the caller receives, as stderr. A C library that calls exit() raises nothing, so worker.crashed is never written and the connection carries a bare crashed; the account of what happened was on fd 2, which goes to the container runtime's log driver, where the fleet's collector drops complete non-JSON lines at ingest. libgomp: Thread creation failed: Resource temporarily unavailable is the line this exists for. The field makes a death legible rather than shipping a cell's stderr anywhere, so a worker that warns and then answers normally reports nothing. The capture is best effort and untrusted: fd 2 stays non-blocking so a warning written from inside libvips can never wait on the supervisor's scheduling, and everything a worker spawned inherits the descriptor. docs/LOGS.md states what the field is and is not.

HotCell::Server

Added

  • request, request.abandoned, worker.crashed, worker.killed and worker.undispatchable carry hotcell.op, the operation the line is about. A cell runs several operations at once and they do not share limits, so worker.killed cause=fsize on a host serving three PDF operations named none of them, and no join was available elsewhere: the response carries no operation, and hotcell_killed is tagged cell and cause only. The worker parses the name out of the request; the supervisor, which never reads one, learns it from the worker's report and holds it, because a killed worker cannot write its own worker.killed. Where the name is not known the field is null rather than an earlier request's name. See docs/LOGS.md.

  • worker.undispatchable is the one line where the supervisor reads a request, since the worker died before the dispatch write and nothing else has read it. It peeks rather than reads, taking neither the bytes nor the caller's descriptors off the connection, and never waits for a request that has not arrived.

Fixed

  • Operation#run_tool carries OMP_NUM_THREADS and OMP_THREAD_LIMIT from the cell's environment into the environment it writes for a tool. A tool sees only what its operation wrote for it, so the image's bound would otherwise have applied to in-process libvips and to nothing the cell execs.

ActiveStorage::HotCell::Server

Fixed

  • activestorage-hotcell-server requires mini_magick >= 5.2.0 and ruby-vips >= 2.2.1. The declared floors were 4.0 and 2.2, but MagickOperation sets MiniMagick.restricted_env= at require time and VipsOperation's before_fork guard requires Vips.block_untrusted, neither of which exists at those floors. A bundle that satisfied the gemspec could resolve mini_magick 5.1.2 or ruby-vips 2.2.0 and fail to boot — a NoMethodError on the magick side, a fail-closed ConfigurationError on the vips side — taking the whole conversion toolchain offline.

  • MiniMagick.cli_env carries the cell's OMP_NUM_THREADS and OMP_THREAD_LIMIT, so magick runs under the image's bound rather than sizing its pool from the host's cores. MiniMagick.restricted_env is what had removed them.

Tooling

Changed

  • bin/conformance verifies the container flags it claims rather than asserting them. The isolation check read the network interfaces, whether the root took a write, whether scratch was noexec, and an exec'd tool's environment — it never read the bounding capability set, so --cap-drop ALL could be dropped from the run and every check still passed. The read-only check was weak the same way: File.writable?("/") is false for the cell's non-root user whether or not the root is read-only. The isolation operation now reports what the kernel exposes — the bounding capability set and the no-new-privileges bit from /proc/self/status, the uid, and the root mount's read-only option from /proc/self/mounts — and the battery requires each. A field the cell cannot read comes back nil and fails, so a run that cannot see a flag is never mistaken for one that set it.

  • bin/conformance runs its own negative controls: it re-invokes itself once per flag with HOTCELL_CONFORMANCE_DROP, booting without network, read-only, cap-drop and no-new-privileges in turn, and requires each run to fail at that flag's own assertion rather than merely exiting non-zero. The tmpfs-noexec negative is decided by probing the runtime, so a runtime that force-mounts noexec reports SKIP with the reason instead of passing vacuously. The isolation checks run first, before the timing-sensitive deadline and overload checks, so a dropped flag fails there rather than behind a flake. examples/gate is a fast container-free guard that drives the isolation check with fabricated results, proves it rejects every insecure or unreadable value, and holds the negatives' expected messages against the battery's assertions so a reworded assertion cannot rot a negative into a grep that never matches.

  • bin/conformance and bin/load no longer pass --ulimit stack=2097152:2097152. docs/DEPLOYMENT.md forbids a lowered stack — an overflow becomes a SIGSEGV the supervisor reports as a transient killed/crashed — so the helpers were measuring a shape operators are told not to deploy. examples/gate asserts neither script sets it.

Fixed

  • docs/DEPLOYMENT.md no longer tells operators that conformance cannot observe cap-drop, no-new-privileges or the uid, and that read-only is checked by attempting a write. All four were true before the checks above and false after.

v0.2.0

v0.2.0 Pre-release
Pre-release

Choose a tag to compare

@flavorjones flavorjones released this 25 Aug 18:29
1a3c14e

v0.2.0 / 2026-08-25

HotCell::Client and HotCell::Server

Security

  • The file-size verdict is now earned from the failed write rather than read off a signal. Workers share a uid, so one worker could signal another and have the supervisor write a permanent fsize or memory judgment against the victim's unrelated input, which Active Storage would then cache forever. Every signal but the supervisor's own deadline kill is now crashed and transient. (#25)
  • Each request now gets a $HOME name no earlier request held, under a slot directory whose mode is reasserted first. A tool that reached code execution could chmod 0500 its own configuration directory, defeating both the worker's delete and the supervisor's rename, and hand the next request the tree that had refused to go. (#22)
  • A failed scratch removal is retried with the modes put back, and logs slot.unswept when it still fails. A chmod 0500 on a directory a conversion wrote left one tree per request on the tmpfs, readable by every later request on the slot, and sweep reported nothing. (#22)

Fixed

  • Cell#describe reads the description inside a rescue. A response with a correctly framed shape but the wrong types raised out of describe_cells, which the README recommends calling from after_initialize where nothing rescues it — so a cell answering badly could stop a Rails application from booting. An unreadable description is now logged and ignored, which is what an unreachable cell already returned. (#29)
  • HotCell.register raises ConfigurationError unless timeout and control_timeout are positive and finite. A nil timeout built no deadline at all, so a cell that accepted a connection and never answered held the caller for good. (#34)
  • The installer writes hotcell/operations/.keep rather than shipping it as a template. hotcell-client.gemspec selects Dir["lib/**/*"], which does not match a dotfile, so the published v0.1.0 installer wrote a scaffold whose generated Dockerfile could not build. (#26)

v0.1.0

v0.1.0 Pre-release
Pre-release

Choose a tag to compare

@flavorjones flavorjones released this 19 Aug 22:36
4c52b12

v0.1.0 / 2026-08-19

  • Birthday!