Add cancellation and cleanup for native server creation

This commit is contained in:
2026-10-04 18:00:11 -04:00
parent 19f1f4e279
commit 176b7dd86b
17 changed files with 482 additions and 26 deletions
+91
View File
@@ -209,6 +209,97 @@ class AgentTests(unittest.TestCase):
self.assertTrue((self.library / "content/game.ff").exists())
self.assertFalse(self.agent.instance_dir(INSTANCE, 1).exists())
def test_cancel_creation_bypasses_occupancy_and_preserves_library(self):
self.create()
self.agent.rcon = lambda *_: self.fail("Cancellation must not wait for player evidence")
operation = self.operation("instance.cancel-create", {"createOperationId": "create"}, "cancel")
self.assertEqual(self.agent.execute(operation)["state"], "completed")
self.assertFalse(self.agent.inspect(INSTANCE, 1)["exists"])
self.assertFalse(self.agent.instance_dir(INSTANCE, 1).exists())
self.assertTrue((self.library / "content/game.ff").exists())
self.assertEqual(self.agent.execute(operation)["state"], "completed")
self.assertEqual(sum(call[:2] == ("container", "rm") for call in self.docker.calls), 1)
def test_heartbeat_cancellation_stops_create_before_starting_container(self):
original_call = self.docker.call
def cancel_during_create(*args, **kwargs):
result = original_call(*args, **kwargs)
if args[:2] == ("container", "create"):
self.agent.cancelled_creations = frozenset(["create"])
return result
self.docker.call = cancel_during_create
payload = {"name": "test", "game": "t5", "libraryId": self.library_id, "image": "ghcr.io/ayymoss/plutainer:latest", "port": 28960, "env": {}, "rconPassword": "secret", "files": []}
self.assertEqual(self.agent.execute(self.operation("instance.create", payload, "create"))["state"], "unknown")
self.assertFalse(any(call[:2] == ("container", "start") for call in self.docker.calls))
result = self.agent.execute(self.operation("instance.cancel-create", {"createOperationId": "create"}))
self.assertEqual(result["state"], "completed")
self.assertFalse(self.agent.inspect(INSTANCE, 1)["exists"])
def test_cancellation_cleanup_resumes_after_agent_restart(self):
self.create()
self.docker.lose_reply_after = "rm"
cancel = self.operation("instance.cancel-create", {"createOperationId": "create"}, "cancel")
self.assertEqual(self.agent.execute(cancel)["state"], "unknown")
self.agent.journal.db.close()
self.agent = Agent(self.config, self.docker)
self.assertEqual(self.agent.execute(cancel)["state"], "completed")
self.assertFalse(self.agent.instance_dir(INSTANCE, 1).exists())
def test_cancel_before_creation_delivery_fences_delayed_create(self):
result = self.agent.execute(self.operation("instance.cancel-create", {"createOperationId": "late-create"}))
self.assertEqual(result["state"], "completed", result)
result = self.agent.execute(self.operation("instance.create", {}, "late-create"))
self.assertEqual(result["state"], "failed")
self.assertFalse(any(call[:2] == ("container", "create") for call in self.docker.calls))
def test_cancel_removes_partial_files_without_a_container(self):
directory = self.agent.instance_dir(INSTANCE, 1)
(directory / "app").mkdir(parents=True)
(directory / "app/partial.cfg").write_text("partial")
self.agent.journal.accept(self.operation("instance.create", {}, "create"))
result = self.agent.execute(self.operation("instance.cancel-create", {"createOperationId": "create"}))
self.assertEqual(result["state"], "completed", result)
self.assertFalse(directory.exists())
self.assertTrue((self.library / "content/game.ff").exists())
def test_cancel_rejects_a_creation_id_from_another_instance(self):
other = self.operation("instance.create", {}, "other-create")
other["instanceId"] = "another-instance"
self.agent.journal.accept(other)
result = self.agent.execute(self.operation("instance.cancel-create", {"createOperationId": "other-create"}))
self.assertEqual(result["state"], "failed", result)
self.assertFalse((self.agent.instance_dir(INSTANCE, 1).parent / "current.json").exists())
def test_cancel_creation_rejects_other_creation_and_newer_generation(self):
self.create()
result = self.agent.execute(self.operation("instance.cancel-create", {"createOperationId": "wrong"}))
self.assertEqual(result["state"], "failed")
module.atomic_json(self.agent.instance_dir(INSTANCE, 1).parent / "current.json", {"generation": 2})
result = self.agent.execute(self.operation("instance.cancel-create", {"createOperationId": "create"}, "old-generation"))
self.assertEqual(result["state"], "failed")
self.assertTrue(self.agent.inspect(INSTANCE, 1)["running"])
self.assertFalse(any(call[:2] == ("container", "rm") for call in self.docker.calls))
def test_cancel_creation_reconciles_lost_removal_reply_and_fences_unknown_create(self):
self.docker.lose_reply_after = "start"
payload = {"name": "test", "game": "t5", "libraryId": self.library_id, "image": "ghcr.io/ayymoss/plutainer:latest", "port": 28960, "env": {}, "rconPassword": "secret", "files": []}
create = self.operation("instance.create", payload, "create")
self.assertEqual(self.agent.execute(create)["state"], "unknown")
self.docker.lose_reply_after = "rm"
cancel = self.operation("instance.cancel-create", {"createOperationId": "create"}, "cancel")
self.assertEqual(self.agent.execute(cancel)["state"], "unknown")
self.assertEqual(self.agent.execute(cancel)["state"], "completed")
self.assertEqual(self.agent.execute(create)["state"], "failed")
self.assertFalse(self.agent.instance_dir(INSTANCE, 1).exists())
self.assertTrue(json.loads((self.agent.instance_dir(INSTANCE, 1).parent / "current.json").read_text())["removed"])
def test_cancel_creation_rejects_unowned_container(self):
self.create()
self.docker.containers[self.agent.container_name(INSTANCE, 1)]["Config"]["Labels"] = {}
result = self.agent.execute(self.operation("instance.cancel-create", {"createOperationId": "create"}))
self.assertEqual(result["state"], "failed")
self.assertFalse(any(call[:2] == ("container", "rm") for call in self.docker.calls))
def test_unowned_container_never_mutated(self):
self.create()
self.docker.containers[self.agent.container_name(INSTANCE, 1)]["Config"]["Labels"] = {}