Releases: basecamp/hotcell
Release list
v1.0.0
v1.0.0 / 2026-10-05
Documentation
- The README is a short introduction and quick start, and the details are in a reference manual with one topic per page, starting at docs/index.md.
docs/DESIGN.md,docs/DEPLOYMENT.md,docs/TUNING.mdanddocs/LOGS.mdare gone. Their content is in docs/design/, docs/container.md, docs/cell-settings.md, docs/client-api.md, docs/tuning.md, docs/scratch.md and docs/observability.md. - The README and the reference manual are published as a site at https://basecamp.github.io/hotcell/.
v0.6.0
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-hotcellgem. 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_cellsubscriber or in its Yabeda integration.HotCell::LogSubscriberwrites this line now; see the README's "Application logs". - Replace the application's HotCell health endpoints with
HotCell::HealthControllerandHotCell::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.rbandreopen.rbwithrequire "hot_cell/health_operations", and change the application's clients to callhealth.echoandhealth.reopen. See the README's "Rails healthcheck".
HotCell::Server
Added
hot_cell/health_operationsdefines thehealth.echoandhealth.reopenoperations. 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 requireshot_cell/health_operations. Otherwise, the cell answersunsupported.- The
hotcell.describeresponse includesserver_version, the version ofhotcell-serverthat 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 forfork. The supervisor runsProcess.warmuponce, 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.killon the worker's process group could raiseErrno::EPERMwhile 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_cellswarns at boot when a cell'shotcell-serverversion differs from the application'shotcell-clientversion. (#21)HotCell::LogSubscriberwrites oneinfoline 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::HealthControllerandHotCell::DiagnosticsControllercheck each registered cell.HotCell::HealthControlleruses only the control socket and returnsOKorFAIL. It takes no worker.HotCell::DiagnosticsControlleralso sendshealth.echoandhealth.reopenon 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, setHotCell.diagnostics_controller_parentin an initializer to the name of an authenticated controller class. Alternatively, subclassHotCell::DiagnosticsController, authenticate in the subclass, and route to the subclass. See the README's "Rails healthcheck".
Fixed
- The
perform.hot_cellevent now containscellandoperationwhen an exception escapes the call. Previously, the event contained neither. - The client now returns
capacitywhen a full cell closes the connection before the client finishes sending the request. Previously, the client returnedunavailablefor 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. CallYabeda::HotCell.install!once at boot. See the README's "Metrics collection".
ActiveStorage::HotCell::Client
Fixed
require "active_storage/hot_cell/client"now works without themini_magickgem. Previously, it raisedLoadErrorwhenmini_magickwas not installed. The gem now loadsTransformers::Image::Magickwhen the application first references that constant. An application that uses only the Vips transformer can removemini_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.echoandhealth.reopenfromhot_cell/health_operations. Its ownexample.echoandexample.reopenoperations are removed.
Fixed
bin/conformanceno 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-imageandbin/loadpass their inputs as arguments, not through a shell. Previously, shell syntax in a checkout path ran as a command inbin/example-image. Shell syntax in abin/loadscenario, duration or thread count ran as a command in the driver container. (#33)
v0.5.0
v0.5.0 / 2026-09-09
HotCell::Server
Added
- The supervisor forks a sweeper every
sweep_intervalseconds (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.unsweptwhen the sweeper deleted the tree first.
v0.4.1
v0.4.1 / 2026-09-08
Upgrading
Some actions that application developers should consider taking when upgrading from an earlier version:
- Add
--developmentto thehotcellcommand inProcfile.dev, or wherever a cell boots as a plain process beside the application. Without it a cell told noTMPDIRsweeps/tmpat 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,TMPDIRor the system temporary directory, both shared with everything else a developer runs. Its scratch ishotcell-<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-userTMPDIRevery shell sets. An explicitHOTCELL_WORKSPACEstill has its parent swept. Without the flag nothing changes.cell.bootlogs thetmpdirin use.
v0.4.0
v0.4.0 / 2026-09-08
Upgrading
Some actions that application developers should consider taking when upgrading from an earlier version:
- Log
stderrfrom theperform.hot_cellevent in the client Rails application. This improves observability and provides forensic evidence about cell crashes. - Set
MAGICK_MEMORY_LIMIT,MAGICK_MAP_LIMITandMAGICK_DISK_LIMITin a cell image that installs ImageMagick. See docs/IMAGEMAGICK.md.
HotCell::Client
Added
- The
perform.hot_cellevent carriesstderr, the failure's captured stream, besidesignal. 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 byFailure.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.tmpdirand 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_DIRis inside the scratch, when the scratch is missing or reached through a symlink the cell's uid owns, or whenHOTCELL_WORKSPACE,TMPDIRorHOTCELL_DIRis not an absolute, normalized path.
Fixed
- A worker sets
TMPDIRto 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_TMPDIRto the request'sTMPDIR, so ImageMagick's pixel cache — from themagickthe magick operations run and from themagickloadlibvips delegates to — is removed with the request, however it ends. MagickOperationforwards the worker'sMAGICK_*_LIMIT,TMPDIRandMAGICK_TMPDIRto themagickit spawns, read per request.MiniMagick.restricted_envhad dropped them along with the rest of the environment, so an image'sMAGICK_DISK_LIMITbounded ImageMagick inside libvips and not themagickbehindanalyzers.image.magickandtransformers.image.magick, which ran under ImageMagick's defaults — the host's RAM and an unbounded disk — and the request'sTMPDIRabove 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_sizeno 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 / 2026-09-02
HotCell::Core
Fixed
Descriptor#fd_pathanswers 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 asunreadable. Linux is untouched. (#50)
ActiveStorage::HotCell::Server
Fixed
MagickOperationandVipsOperationclassifyImageProcessing::Errorasunreadableand permanent. Before, it answeredfailed, which is transient, so a file the pipeline had already refused was reconverted on every future request. (#42)
v0.3.0
v0.3.0 / 2026-09-01
HotCell::Client
Added
- The installed
Dockerfilestrips the setuid and setgid bits off every binary in the image as its last root step, so the strip covers anything aRUNabove it installed rather than the base image alone. The cell already runs unprivileged under--cap-drop ALLand--security-opt no-new-privileges, which makemount,suand 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 withdocker 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-correctGemfile.lockand 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 existingDockerfilealone, so a cell installed before this needs the strip added by hand and the image rebuilt.
Breaking
HotCell.describe_cellsno longer warns about a client whose operation the cell does not carry, andHotCell.clientsis 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 asunsupported, 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
DockerfilesetsOMP_NUM_THREADSandOMP_THREAD_LIMITto the container'scpus. OpenMP sizes its thread pool from the host's core count, and a container'scpusquota 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, whichRLIMIT_DATAcharges, so the pool alone cleared the cell'smemorylimit — and libgomp callsexit(1)on the firstpthread_createit cannot satisfy. An upgrade leaves an existingDockerfilealone, so a cell installed before this needs both added by hand and the image rebuilt.docs/DEPLOYMENT.mdcovers 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
Dockerfileapplies Debian's pending security patches with anapt-get upgradeafter theFROM.docker build --pulltakes the newest base tag, but the tag itself can sit behind an advisory already intrixie-securityuntil 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 existingDockerfilealone, 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.killedthat reports its death, ashotcell.stderr, and to the failure the caller receives, asstderr. A C library that callsexit()raises nothing, soworker.crashedis never written and the connection carries a barecrashed; 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 unavailableis 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.mdstates what the field is and is not.
HotCell::Server
Added
-
request,request.abandoned,worker.crashed,worker.killedandworker.undispatchablecarryhotcell.op, the operation the line is about. A cell runs several operations at once and they do not share limits, soworker.killed cause=fsizeon a host serving three PDF operations named none of them, and no join was available elsewhere: the response carries no operation, andhotcell_killedis taggedcellandcauseonly. 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 ownworker.killed. Where the name is not known the field isnullrather than an earlier request's name. See docs/LOGS.md. -
worker.undispatchableis 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_toolcarriesOMP_NUM_THREADSandOMP_THREAD_LIMITfrom 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-serverrequiresmini_magick >= 5.2.0andruby-vips >= 2.2.1. The declared floors were4.0and2.2, butMagickOperationsetsMiniMagick.restricted_env=at require time andVipsOperation'sbefore_forkguard requiresVips.block_untrusted, neither of which exists at those floors. A bundle that satisfied the gemspec could resolvemini_magick5.1.2 orruby-vips2.2.0 and fail to boot — aNoMethodErroron the magick side, a fail-closedConfigurationErroron the vips side — taking the whole conversion toolchain offline. -
MiniMagick.cli_envcarries the cell'sOMP_NUM_THREADSandOMP_THREAD_LIMIT, somagickruns under the image's bound rather than sizing its pool from the host's cores.MiniMagick.restricted_envis what had removed them.
Tooling
Changed
-
bin/conformanceverifies the container flags it claims rather than asserting them. The isolation check read the network interfaces, whether the root took a write, whether scratch wasnoexec, and an exec'd tool's environment — it never read the bounding capability set, so--cap-drop ALLcould 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. Theisolationoperation 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 backniland fails, so a run that cannot see a flag is never mistaken for one that set it. -
bin/conformanceruns its own negative controls: it re-invokes itself once per flag withHOTCELL_CONFORMANCE_DROP, booting withoutnetwork,read-only,cap-dropandno-new-privilegesin turn, and requires each run to fail at that flag's own assertion rather than merely exiting non-zero. Thetmpfs-noexecnegative is decided by probing the runtime, so a runtime that force-mountsnoexecreportsSKIPwith 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/gateis 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/conformanceandbin/loadno longer pass--ulimit stack=2097152:2097152.docs/DEPLOYMENT.mdforbids a lowered stack — an overflow becomes aSIGSEGVthe supervisor reports as a transientkilled/crashed— so the helpers were measuring a shape operators are told not to deploy.examples/gateasserts neither script sets it.
Fixed
docs/DEPLOYMENT.mdno longer tells operators that conformance cannot observecap-drop,no-new-privilegesor the uid, and thatread-onlyis checked by attempting a write. All four were true before the checks above and false after.
v0.2.0
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
fsizeormemoryjudgment against the victim's unrelated input, which Active Storage would then cache forever. Every signal but the supervisor's own deadline kill is nowcrashedand transient. (#25) - Each request now gets a
$HOMEname no earlier request held, under a slot directory whose mode is reasserted first. A tool that reached code execution couldchmod 0500its 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.unsweptwhen it still fails. Achmod 0500on a directory a conversion wrote left one tree per request on the tmpfs, readable by every later request on the slot, andsweepreported nothing. (#22)
Fixed
Cell#describereads the description inside a rescue. A response with a correctly framed shape but the wrong types raised out ofdescribe_cells, which the README recommends calling fromafter_initializewhere 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.registerraisesConfigurationErrorunlesstimeoutandcontrol_timeoutare positive and finite. Aniltimeout 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/.keeprather than shipping it as a template.hotcell-client.gemspecselectsDir["lib/**/*"], which does not match a dotfile, so the published v0.1.0 installer wrote a scaffold whose generatedDockerfilecould not build. (#26)
v0.1.0
v0.1.0 / 2026-08-19
- Birthday!