tests/spa/orchestration/test_orchestration_spa_commander.py¶
Source from this local checkout, regenerated when the reader rebuilds.
Line links use #L<number>; a GitHub line range opens its first line.
1 # Copyright 2025 Softwell S.r.l.2 #3 # Licensed under the Apache License, Version 2.0 (the "License");4 # you may not use this file except in compliance with the License.5 # You may obtain a copy of the License at6 #7 # https://www.apache.org/licenses/LICENSE-2.08 #9 # Unless required by applicable law or agreed to in writing, software10 # distributed under the License is distributed on an "AS IS" BASIS,11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12 # See the License for the specific language governing permissions and13 # limitations under the License.14 15 """The vertex on its own: who it writes, what it answers, what it discards.16 17 The chain's tests look at the fold from above — an envelope arrives and the18 indexes move. These look at the same object from the front: the birth of a row19 for whoever shows up, the predicates the rest of the machine reads it20 through, the waiting room that is a raised exception and not a field, and the two21 things the vertex does that nobody below it can — discarding what a dead process22 left on disk, and writing the account of every order.23 """24 25 from __future__ import annotations26 27 import asyncio28 import json29 import logging30 import pickle31 32 import pytest33 34 35 from genro_asgi_multiworker_spa.orchestration import SpaCommander, UserOnHold36 from genro_asgi_multiworker_spa.orchestration import FreezeHandler37 from genro_asgi_multiworker_spa.orchestration.freeze_handler import USER_REGISTER_ITEM_NAME38 from genro_asgi_multiworker_spa.orchestration.spa_commander import GUEST_PREFIX39 40 WORKER_NAME = "standard_0001"41 42 43 44 def minted(commander, cid: str) -> str:45 """The identity the site would baptise for this cookie, learned by the vertex.46 47 The old mint died with the doctrine (the cookie routes, the site names):48 tests stage the junction the fold of ``new_connection`` would have written.49 """50 user = f"guest_{cid}"51 commander.record_connection_user(cid, user)52 return user53 54 55 @pytest.fixture56 def commander(short_root):57 return SpaCommander(short_root / "frozen_users")58 59 60 def parked_state(commander: SpaCommander, user: str) -> None:61 """What a freeze leaves on disk, written the way a worker writes it."""62 commander.freeze_handler.take_lock(user, WORKER_NAME)63 commander.freeze_handler.write_user_register_item(64 user, {"store": "whatever"}, writer=WORKER_NAME, cause="freeze", group="standard"65 )66 commander.freeze_handler.release_lock(user, WORKER_NAME)67 68 69 def test_whoever_shows_up_is_minted_before_anything_descends(commander):70 user = minted(commander, "cid-a")71 72 assert user == f"{GUEST_PREFIX}cid-a"73 assert commander.connection_user_map == {"cid-a": user}74 assert commander.user_map[user] == {"group": None, "frozen": False, "on_hold": None}75 76 77 def test_a_cid_already_known_is_answered_and_nothing_is_written_twice(commander):78 first = minted(commander, "cid-a")79 commander.user_map[first]["group"] = "kept"80 81 assert minted(commander, "cid-a") == first82 assert commander.user_map[first]["group"] == "kept"83 84 85 def test_a_cookie_that_outlived_its_row_is_still_that_person(commander):86 commander.connection_user_map["cid-a"] = "mario"87 88 assert commander.resolve_user("cid-a") == "mario"89 assert commander.user_map["mario"]["frozen"] is False90 91 92 def test_a_user_on_his_way_out_is_not_routed_but_raised(commander):93 user = minted(commander, "cid-a")94 commander.hold_user(user, "transfer_flag T")95 96 with pytest.raises(UserOnHold) as refusal:97 commander.resolve_user("cid-a")98 99 assert refusal.value.user == user100 assert refusal.value.cause == "transfer_flag T"101 assert str(refusal.value) == f"{user} is on hold: transfer_flag T"102 103 104 def test_the_cause_of_a_hold_is_the_one_that_explains_the_wait(commander):105 user = minted(commander, "cid-a")106 commander.hold_user(user, "transfer_flag T")107 commander.hold_user(user, "transfer_flag X")108 109 assert commander.user_map[user]["on_hold"] == "transfer_flag T"110 111 112 def test_nobody_is_frozen_until_it_is_written_down(commander):113 user = minted(commander, "cid-a")114 115 assert commander.user_is_frozen("somebody nobody knows") is False116 assert commander.user_is_frozen(user) is False117 118 commander.mark_user_frozen(user)119 120 assert commander.user_is_frozen(user) is True121 122 123 def test_a_freeze_ends_the_wait_it_was_the_reason_for(commander):124 user = minted(commander, "cid-a")125 commander.hold_user(user, "transfer_flag T")126 127 commander.mark_user_frozen(user)128 129 assert minted(commander, "cid-a") == user130 131 132 def test_an_adoption_takes_the_mark_off(commander):133 user = minted(commander, "cid-a")134 commander.mark_user_frozen(user)135 136 commander.mark_user_adopted(user)137 138 assert commander.user_is_frozen(user) is False139 140 141 def test_dropping_what_is_already_gone_is_that_same_outcome(commander):142 commander.drop_page("never-existed")143 commander.drop_connection("never-existed")144 commander.drop_user("never-existed")145 146 assert commander.page_connection_map == {}147 assert commander.user_map == {}148 149 150 def test_a_user_who_is_gone_takes_his_connections_pages_and_freezer_state_with_him(commander):151 user = minted(commander, "cid-a")152 commander.connection_user_map["cid-b"] = user153 commander.page_connection_map["p1"] = "cid-a"154 commander.page_connection_map["p2"] = "cid-b"155 parked_state(commander, user)156 157 assert commander.drop_user(user) is True158 159 assert commander.user_map == {}160 assert commander.connection_user_map == {}161 assert commander.page_connection_map == {}162 # Nothing of an identity nobody answers for is left behind: what the sweep of163 # the freezer finds later is only what a row lost WITHOUT a drop.164 assert commander.freeze_handler.user_folders == set()165 assert commander.counters["frozen_users_discarded"] == 1166 167 168 def test_what_a_dead_process_left_on_disk_is_discarded_and_counted(commander, caplog):169 caplog.set_level(logging.INFO)170 user = minted(commander, "cid-a")171 parked_state(commander, user)172 without_state = minted(commander, "cid-b")173 174 commander.drop_users([user, without_state], cause="process_aborted")175 176 assert commander.freeze_handler.user_folders == set()177 assert commander.user_map == {}178 assert commander.counters["frozen_users_discarded"] == 1179 assert caplog.text.count("order=drop_user") == 2180 181 182 def test_every_order_leaves_its_row_on_the_file_of_the_orders(short_root):183 log_path = short_root / "orchestration.log"184 commander = SpaCommander(short_root / "frozen_users", orchestration_log_path=log_path)185 186 commander.log_order(187 "standard",188 "quit_process",189 WORKER_NAME,190 numbers={"occupancy_percent": 12.0},191 outcome="quitted",192 )193 194 row = log_path.read_text().strip()195 assert "decided_by=standard" in row196 assert "order=quit_process" in row197 assert f"subject={WORKER_NAME}" in row198 assert "numbers={'occupancy_percent': 12.0}" in row199 assert "outcome=quitted" in row200 201 202 def test_every_order_also_leaves_a_structured_decision(short_root):203 log_path = short_root / "orchestration.log"204 commander = SpaCommander(short_root / "frozen_users", orchestration_log_path=log_path)205 206 commander.log_order(207 "standard",208 "quit_process",209 WORKER_NAME,210 numbers={"occupancy_percent": 12.0},211 outcome="quitted",212 reason="worker_was_ordered_away",213 )214 215 decision_path = log_path.with_suffix(".decisions.jsonl")216 row = json.loads(decision_path.read_text())217 assert row["schema"] == 1218 assert row["decision_id"].endswith("-1")219 assert row["decided_by"] == "standard"220 assert row["decision"] == "quit_process"221 assert row["subject"] == WORKER_NAME222 assert row["outcome"] == "quitted"223 assert row["reason"] == "worker_was_ordered_away"224 assert row["numbers"] == {"occupancy_percent": 12.0}225 assert row["candidates"] == []226 assert row["timestamp"].endswith("+00:00")227 228 229 def test_a_calculation_can_be_recorded_without_inventing_an_order(short_root):230 log_path = short_root / "orchestration.log"231 commander = SpaCommander(short_root / "frozen_users", orchestration_log_path=log_path)232 233 commander.log_decision(234 "standard",235 "placement_candidates",236 "standard_0002",237 reason="hottest_cpu_open_candidate",238 subject="mario",239 candidates=[240 {"name": "standard_0001", "cpu_admission_open": False},241 {"name": "standard_0002", "cpu_admission_open": True},242 ],243 )244 245 assert log_path.read_text() == ""246 row = json.loads(log_path.with_suffix(".decisions.jsonl").read_text())247 assert row["decision"] == "placement_candidates"248 assert row["outcome"] == "standard_0002"249 assert row["reason"] == "hottest_cpu_open_candidate"250 assert [candidate["name"] for candidate in row["candidates"]] == [251 "standard_0001",252 "standard_0002",253 ]254 255 256 def test_one_process_has_one_vertex_and_the_log_is_its_own(short_root):257 first_path = short_root / "first.log"258 second_path = short_root / "second.log"259 SpaCommander(short_root / "frozen_users", orchestration_log_path=first_path)260 second = SpaCommander(short_root / "frozen_users", orchestration_log_path=second_path)261 262 second.log_order("standard", "quit_process", WORKER_NAME, outcome="quitted")263 264 assert len(logging.getLogger("genro_asgi.orchestration.orders").handlers) == 1265 assert "order=quit_process" in second_path.read_text()266 assert first_path.read_text() == ""267 assert "quit_process" in second_path.with_suffix(".decisions.jsonl").read_text()268 assert first_path.with_suffix(".decisions.jsonl").read_text() == ""269 270 271 def test_the_machine_starts_running_and_holds_the_master_of_the_store(commander):272 assert commander.state == "running"273 assert commander.global_register == {}274 275 276 def test_the_concession_is_this_servers_share_of_the_whole_machine(commander, monkeypatch):277 monkeypatch.setattr(278 commander,279 "_machine_memory_gauges",280 lambda: {"MemTotal": 8_000_000_000.0, "MemAvailable": 6_000_000_000.0},281 )282 commander.memory_max_percent = 25.0283 284 assert commander.memory_concession_bytes == 2_000_000_000285 # And the alarm line is read against the machine, not against the concession.286 assert commander._machine_memory_used_percent() == 25.0287 288 289 def test_the_machine_total_is_read_off_the_platform_itself(commander):290 # No monkeypatch: os.sysconf answers on every platform this suite runs on,291 # /proc/meminfo or not.292 assert commander.memory_concession_bytes > 0293 294 295 def test_the_elected_group_receives_the_newcomer(commander):296 commander.group_map["stable"] = object()297 commander.group_map["canary"] = object()298 299 # Nobody elected: the first declared is the one the recipe named first.300 assert commander.default_group == "stable"301 302 commander._default_group = "canary"303 assert commander.default_group == "canary"304 305 306 def test_a_vertex_with_nowhere_to_put_a_newcomer_says_so(commander):307 with pytest.raises(KeyError):308 commander.default_group309 310 commander._default_group = "nobody"311 commander.group_map["stable"] = object()312 with pytest.raises(KeyError):313 commander.default_group314 315 316 def test_the_group_of_a_user_is_recorded_where_the_placement_decided_it(commander):317 user = minted(commander, "cid-a")318 319 commander.record_user_group(user, "stable")320 321 assert commander.user_map[user]["group"] == "stable"322 323 324 async def test_a_request_for_a_user_on_hold_waits_for_his_release(commander):325 user = minted(commander, "cid-a")326 commander.hold_user(user, "transfer_flag T")327 328 waiting = asyncio.ensure_future(commander.await_user_release(user, timeout=5.0))329 await asyncio.sleep(0)330 assert not waiting.done()331 332 commander.mark_user_adopted(user)333 334 await waiting335 assert commander.user_hold_event_map == {}336 assert minted(commander, "cid-a") == user337 338 339 async def test_the_freezer_mark_releases_the_wait_too(commander):340 user = minted(commander, "cid-a")341 commander.hold_user(user, "transfer_flag F")342 waiting = asyncio.ensure_future(commander.await_user_release(user, timeout=5.0))343 await asyncio.sleep(0)344 345 commander.mark_user_frozen(user)346 347 await waiting348 assert commander.user_hold_event_map == {}349 350 351 async def test_a_user_dropped_while_held_wakes_whoever_waited_for_him(commander):352 user = minted(commander, "cid-a")353 commander.hold_user(user, "transfer_flag T")354 waiting = asyncio.ensure_future(commander.await_user_release(user, timeout=5.0))355 await asyncio.sleep(0)356 357 commander.drop_users([user], cause="expired")358 359 await waiting360 assert commander.user_hold_event_map == {}361 362 363 async def test_a_wait_that_outlives_its_own_deadline_gives_up(commander):364 user = minted(commander, "cid-a")365 commander.hold_user(user, "transfer_flag T")366 367 with pytest.raises(TimeoutError):368 await commander.await_user_release(user, timeout=0.01)369 370 # The hold is still up: giving up on a wait decides nothing about the user.371 assert user in commander.user_hold_event_map372 373 374 async def test_nobody_waits_on_a_user_who_is_not_held(commander):375 user = minted(commander, "cid-a")376 377 await commander.await_user_release(user, timeout=0.01)378 379 380 def test_a_hold_already_up_keeps_its_first_cause_and_its_own_door(commander):381 user = minted(commander, "cid-a")382 commander.hold_user(user, "transfer_flag T")383 door = commander.user_hold_event_map[user]384 385 commander.hold_user(user, "transfer_flag X")386 387 assert commander.user_map[user]["on_hold"] == "transfer_flag T"388 assert commander.user_hold_event_map[user] is door389 390 391 class QuietGroup:392 """A group that records the order it was given and parks nobody."""393 394 def __init__(self) -> None:395 self.ordered_into: list[str] = []396 397 async def quit_all(self, freezer_path: str) -> None:398 self.ordered_into.append(freezer_path)399 400 401 async def test_the_quit_orders_every_group_and_commits_the_photo_by_renaming(commander):402 group = QuietGroup()403 commander.group_map["standard"] = group404 commander.record_connection_user("cid-a", "mario")405 406 await commander.quit()407 408 assert group.ordered_into == [str(commander.reboot_temp_path)]409 assert not commander.reboot_temp_path.exists()410 assert commander.reboot_data_path.exists()411 saved = FreezeHandler(commander.reboot_data_path).read_commander_register_item()412 assert saved["connection_user_map"] == {"cid-a": "mario"}413 414 415 async def test_the_saved_rows_are_normalised_for_a_boot_that_adopts_nobody(commander):416 commander.group_map["standard"] = QuietGroup()417 commander.record_connection_user("cid-a", "mario")418 commander.hold_user("mario", "moving")419 420 await commander.quit()421 422 saved = FreezeHandler(commander.reboot_data_path).read_commander_register_item()423 row = saved["user_map"]["mario"]424 assert row["frozen"] is True425 assert row["on_hold"] is None426 427 428 async def test_a_quit_that_dies_before_the_rename_leaves_no_photo(commander, monkeypatch):429 commander.group_map["standard"] = QuietGroup()430 431 def refuse(*args, **kwargs):432 raise OSError("the disk said no")433 434 monkeypatch.setattr(FreezeHandler, "write_commander_register_item", refuse)435 436 with pytest.raises(OSError):437 await commander.quit()438 439 assert commander.reboot_temp_path.exists()440 assert not commander.reboot_data_path.exists()441 442 443 def photographed(commander, user="mario", cid="cid-a", ts=None):444 """A photo on disk as a soft quit leaves it: one parcel, and the vertex's item."""445 photo = FreezeHandler(commander.reboot_data_path)446 photo.take_lock(user, WORKER_NAME)447 photo.write_user_register_item(448 user, {"store": "whatever"}, writer=WORKER_NAME, cause="quit", group="standard"449 )450 photo.release_lock(user, WORKER_NAME)451 if ts is not None:452 path = photo.root_path / photo.user_to_userkey(user) / USER_REGISTER_ITEM_NAME453 envelope = pickle.loads(path.read_bytes())454 envelope["header"]["ts"] = ts455 path.write_bytes(pickle.dumps(envelope))456 photo.write_commander_register_item(457 {458 "user_map": {user: dict(commander._new_row(), frozen=True, group="standard")},459 "connection_user_map": {cid: user},460 "page_connection_map": {},461 "global_register": {},462 "quit_ts": 0.0,463 },464 writer="vertex",465 cause="quit",466 )467 return photo468 469 470 def test_a_boot_with_frozen_registers_becomes_them_and_leaves_the_parcels_where_wakes_look(471 commander,472 ):473 photographed(commander)474 475 commander.adopt_frozen_registers()476 477 assert commander.connection_user_map == {"cid-a": "mario"}478 assert commander.user_is_frozen("mario") is True479 assert not commander.reboot_data_path.exists()480 assert commander.freeze_handler.read_user_register_item("mario") is not None481 482 483 def test_a_boot_with_no_frozen_registers_wipes_the_working_deposit_and_starts_clean(commander):484 parked_state(commander, "mario")485 486 commander.adopt_frozen_registers()487 488 assert commander.user_map == {}489 assert commander.freeze_handler.user_folders == set()490 491 492 def test_frozen_registers_that_cannot_be_read_boot_clean(commander):493 photo = photographed(commander)494 (photo.root_path / "commander_register_item.pickle").write_bytes(b"not a pickle")495 496 commander.adopt_frozen_registers()497 498 assert commander.user_map == {}499 assert not commander.reboot_data_path.exists()500 assert commander.freeze_handler.user_folders == set()501 502 503 def test_a_reboot_temp_left_by_a_dead_quit_is_never_read(commander):504 FreezeHandler(commander.reboot_temp_path)505 506 commander.adopt_frozen_registers()507 508 assert not commander.reboot_temp_path.exists()509 assert commander.user_map == {}510 511 512 async def test_a_user_past_his_expiry_is_dropped_at_the_boot_not_woken(commander):513 photographed(commander, ts=0.0)514 commander.adopt_frozen_registers()515 516 await commander.drop_expired_users(now=True)517 518 assert "mario" not in commander.user_map519 assert commander.freeze_handler.user_folders == set()520 521 522 async def test_the_cookie_survives_a_quit_and_the_boot_that_follows(commander):523 """The whole point: the same cid still names the same person on the other side."""524 commander.group_map["standard"] = QuietGroup()525 commander.record_connection_user("cid-a", "mario")526 parked_state(commander, "mario")527 commander.mark_user_frozen("mario")528 await commander.quit()529 530 reborn = SpaCommander(commander.freeze_handler.root_path)531 reborn.adopt_frozen_registers()532 533 assert reborn.resolve_user("cid-a") == "mario"534 assert reborn.user_is_frozen("mario") is True