ADR-0104: Studio restarts itself for a plugin, but only when something will start it again
- Status: accepted
- Date: 2026-09-23
- Deciders: Mahdi Amirabdollahi
- Change:
openspec/changes/plugins - Amends: ADR-0099 — its "Studio states the command and never restarts itself"
- Builds on: ADR-0103
Context
Most plugin changes take effect without a restart (ADR-0099's Instant and Brief-maintenance classes). A restart is still needed in three cases:
- A plugin declares
activation: restart. - A running version does not stop within its drain time.
- A stopped version's class loader is never garbage-collected.
ADR-0099 had Studio show the restart command and nothing more. That leaves the operator to find a shell on the host at the moment they have just confirmed a change in the UI.
Two ways to restart from inside Studio exist, and both have problems:
- The actuator
restartendpoint (Spring Cloud Commons) refreshes the application context inside the same JVM. It therefore never frees a plugin class loader or its metaspace, which is the reason for the restart. Exposing restart or shutdown over HTTP is also attack surface Studio does not otherwise need. - Exiting the process frees everything, but it is only a restart if something starts the process again. Under a plain
docker runwithout--restart, or a developer'sjust dev, exiting would simply stop Studio.
Decision
- A restart is a graceful exit. Studio closes every plugin, records a clean stop in
studio_boot(so the crash-loop guard does not count it), and exits with code 75 after a 3-second delay so the response reaches the browser. The code is non-zero so that a supervisor configured to restart only on failure still restarts it. - Only when supervised. Studio exits only when
artemis-studio.plugins.restart.supervisedis true, or, when that property is unset, when it runs on Kubernetes (detected by Boot'sCloudPlatform). Both compose files set the property next torestart: unless-stopped, and the two lines are documented as a pair. Everywhere else, Studio refuses to exit and shows the command. - Automatic only with consent. The review of a restart-class activation says in advance whether Studio will restart itself (
AUTOMATIC) or the operator must restart it (MANUAL). Confirming anAUTOMATICplan is the consent: the version is recorded, and the next boot starts it. - A manual "Restart Studio" covers the leaked and stuck cases. It has the same guards as every other plugin action (ADR-0103): an installer, a browser session, a step-up within 5 minutes, and an audit row.
- It is refused within 2 minutes of a boot, so a compromised session cannot hold Studio in a restart loop.
- A second request while a restart is pending does nothing.
- Leak detection. Every closed version's class loader is held weakly. If a loader is still reachable a minute after close, Studio lists it as "still in memory" and marks a restart as needed. Studio asks the JVM for a collection before deciding, at most every 10 minutes and only while such a candidate exists.
-XX:MaxMetaspaceSize=256min the composeJAVA_OPTSbounds the damage from a leak until the restart happens.
Consequences
- An operator who confirms a restart-class plugin sees Studio come back with the plugin running, and never needs a shell.
- Every signed-in user is disconnected for as long as Studio takes to start. The review states this before confirmation.
- A deployment that restarts Studio some other way (systemd, a custom orchestrator) sets
restart.supervised=trueitself. Until it does, it gets the command, never an outage. - Removing
restart: unless-stoppedwithout also removing the property would leave Studio down after a plugin restart. The compose files say this next to both lines.
Alternatives considered
- Actuator
restart/shutdownover HTTP. Rejected: an in-JVM refresh does not free leaked class loaders, and the endpoints add attack surface. - Always exiting and relying on the operator to have a supervisor. Rejected: under
docker runwithout--restart, a plugin install would take Studio down. - Restarting automatically without consent whenever a restart is needed (for example, on a detected leak). Rejected: it would disconnect everyone at a moment nobody chose.