Anvil
Building blocksEnvironmentsActions

Killing processes

End a server or proxy without its shutdown sequence to test crash recovery, then start it again.

Kill a process when a test needs to know what a plugin does when a server or proxy dies: whether it recovers its state, expires locks or heartbeats, or lets players reconnect. processes.kill(name) terminates that one process at once, so the platform runs no stop command, shutdown hooks, or plugin disable logic. processes.stop(name) is the clean counterpart.

Kill a proxy under a connected player

Use the proxy environment, with proxy as the entrypoint and lobby as its backend. Enable rapid reconnections in the proxy declaration when its throttle would delay the rejoin. The Session and Players capabilities must be installed.

Place this helper at src/anvil/java/com/example/test/ProxyCrashChecks.java. Call ProxyCrashChecks.verify(anvil) from a test method in the same package:

package com.example.test;

import me.whereareiam.anvil.api.scenario.ScenarioContext;
import me.whereareiam.anvil.api.type.DisconnectCause;
import me.whereareiam.anvil.api.type.ProcessState;
import me.whereareiam.anvil.capability.players.PlayerWatch;
import me.whereareiam.anvil.capability.players.Players;
import me.whereareiam.anvil.capability.session.Session;

import java.time.Duration;

import static org.junit.jupiter.api.Assertions.assertEquals;

public final class ProxyCrashChecks {
	public static void verify(ScenarioContext anvil) {
		var processes = anvil.processes();
		var alice = anvil.players().create("CrashAlice");
		var session = alice.capability(Session.class);
		PlayerWatch lobby = processes.server("lobby").capability(Players.class).watch();
		session.connect();
		session.connected(Duration.ofSeconds(30));
		lobby.joined(alice);

		var killed = processes.proxy("proxy");
		processes.kill("proxy");

		session.disconnected(Duration.ofSeconds(30));
		assertEquals(DisconnectCause.CONNECTION_LOST, session.state().disconnectCause());
		assertEquals(ProcessState.STOPPED, killed.state());
		assertEquals(ProcessState.READY, processes.server("lobby").state());

		// Assert what your plugin left behind here, for example a lock it must release after a crash.

		processes.start("proxy");
		session.rejoin();
		session.connected(Duration.ofSeconds(30));
		lobby.joined(alice);
	}
}

Run the containing test with ./gradlew anvilTest --tests 'your.package.YourTest'. The watch on lobby is created before the first login and keeps its position, so the second joined(alice) waits for the join after the rejoin, not the one before the kill.

What a kill does

ResourceBehavior
ProcessTerminated at once: a local process tree with SIGKILL through destroyForcibly, a Docker container with docker kill
Shutdown sequenceNot run: no stop command, shutdown hooks, or plugin disable logic
Process handleEnds STOPPED, like after a stop
Agent connection and launch attachmentsReleased, like on a stop
Workspace and listener addressRetained; start(name) creates a new generation on the same address
Other processesKeep running, including the processes that depend on the killed one
Connected playersLose their connection with DisconnectCause.CONNECTION_LOST and must reconnect explicitly

Killing a process that never started or already stopped has no effect.

A kill is not a crash

A deliberate kill does not mark the scenario unsuccessful, so success-only caches are finalized and the run is reported as passed when your assertions pass. Its console history and the workspace's anvil-console.log end with an Anvil line naming it:

[Anvil] Process 'proxy' was killed on request without a clean shutdown

A process that exits on its own still fails the scenario, also when a test kills it afterwards: the kill then only finishes the stop of a generation that had already crashed, and the console carries no kill line.

Limits

  • A kill cannot be undone: data the platform had not written to disk is lost, exactly as in a real crash. Assert that loss deliberately rather than relying on it.
  • A killed proxy sends its backends no disconnect messages; they only see their connections to it closed by the operating system, and handle that as their platform does.
  • Docker execution kills the whole container; a local process is killed together with the processes it started.
  • ProcessException reports a process that does not exit within the configured shutdown timeout after it is killed, and that failure marks the scenario unsuccessful.

On this page