Skip to content

v0.6.0

Choose a tag to compare

@aaldersondev aaldersondev released this 07 Aug 21:31
· 4 commits to main since this release

An installation can now take an hour without being killed, and can no longer take for ever.

This is the groundwork for games installed from Steam. A depot is tens of gigabytes; the pipeline that installs a Minecraft jar in ten seconds was not built for it, in two specific ways.

An installation that has stopped is noticed

waitForExit was a bare container.wait() with no bound at all. An install that stalled on a dead mirror left the server in installing for ever, and nothing gave up.

There is a deadline now, and it measures inactivity rather than duration — a total cap would have to be set so high to accommodate a real depot that it would never fire, and low enough to be useful it would kill working installs.

What counts as activity is what the kernel charges the container's own cgroup: CPU time and block I/O, plus anything the install prints. A transfer that is moving burns CPU on every packet it takes off the socket; one blocked on a socket that stopped answering burns none and issues no I/O.

Two things it deliberately does not watch, both learned the hard way:

  • Output alone. Every install script here downloads with curl -sSL, and -s prints nothing at all for the whole transfer. A silence window would have killed a two-gigabyte modpack on a slow connection — the commonest install shape there is.
  • Network counters. Those are interface counters, not a measure of what the container caused: every server on a node shares one bridge, a bridge floods broadcast traffic to every port, and idle connections send keepalives. A stalled install kept looking alive on exactly the busy nodes where an endless one costs most.

A host that keeps neither counter is treated as unreadable, not as still — those are opposite answers, and confusing them would kill every installation on such a node.

A template that needs a longer window says so with installInactivityTimeoutMs; the default is fifteen minutes of nothing whatsoever.

Space is checked before anything is downloaded

There was no disk check anywhere. A depot larger than the node's free space filled the host disk, which takes down every server on that machine.

The check runs before the image is pulled, measures the filesystem the volumes are really on, and refuses rather than warning — naming how much is free, how much is wanted and which filesystem was measured. A reinstall counts what the volume already holds towards the requirement, since nothing wipes it first; without that a forty-gigabyte server could never be reinstalled at all. A template declares its appetite with installRequiredDiskBytes.

If the filesystem cannot be read, the installation proceeds and says so. Refusing every install on an unreadable filesystem would be a bigger failure than the one being guarded against.

Docker cannot hang the daemon

Every request the daemon makes to Docker is now bounded at one place — the client itself — rather than call by call. A call added tomorrow is bounded without its author having to know the rule exists, which is the only version of this that stays true.

The exceptions are named and deliberate: the wait for a container to end, and the console, statistics and download streams, which are held open by design. For those the bound covers Docker answering; what flows afterwards passes no clock, so a console idle for a week stays open.

Upgrading

From the panel: Settings → Update now. Nothing changes for an existing server or template. No shipped template declares either new field, the egg importer sets neither, and a template that declares nothing sends the daemon exactly the payload it sent before.

Not yet

Resuming an installation that a daemon restart interrupted, and real disk quotas. An install script writes into a bind mount with no quota on it — the preflight is a check, not an enforcement, and docs/security.md now says so rather than implying otherwise.