Skip to content

Commit fe352d2

Browse files
authored
perf(azure): eliminate fleet lists from slot reads (#391)
1 parent 65dfe7b commit fe352d2

3 files changed

Lines changed: 195 additions & 66 deletions

File tree

bin/fm-azure-worker-provider.py

Lines changed: 159 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -484,11 +484,52 @@ def resource_record(kind, value, power_state=None, tags_override=None):
484484

485485
def azure_resource_not_found(stderr):
486486
return bool(re.search(
487-
r"(?:\(ResourceNotFound\)|^Code:\s*ResourceNotFound\s*$)",
487+
r"(?:\((?:ResourceNotFound|ParentResourceNotFound|ContainerNotFound)\)"
488+
r"|^(?:Code|ErrorCode):\s*"
489+
r"(?:ResourceNotFound|ParentResourceNotFound|ContainerNotFound)\s*$)",
488490
str(stderr or ""), re.MULTILINE,
489491
))
490492

491493

494+
def show_exact_optional(controller, args):
495+
"""Read one exact Azure object; only a recognized absence becomes None."""
496+
value, rc, stderr = az(controller, args, check=False)
497+
if rc != 0:
498+
if azure_resource_not_found(stderr):
499+
return None
500+
raise ProviderError("exact Azure inventory read failed or was malformed: {}".format(stderr))
501+
if not isinstance(value, dict):
502+
raise ProviderError("exact Azure inventory read failed or was malformed: {}".format(stderr))
503+
return value
504+
505+
506+
def show_exact_optional_many(controller, operations):
507+
"""Read independent exact objects concurrently, preserving error order."""
508+
if not operations:
509+
return {}
510+
keys = [key for key, _ in operations]
511+
if len(keys) != len(set(keys)):
512+
raise ProviderError("exact Azure inventory operation keys are not unique")
513+
with concurrent.futures.ThreadPoolExecutor(max_workers=min(4, len(operations))) as executor:
514+
futures = {
515+
key: executor.submit(show_exact_optional, controller, args)
516+
for key, args in operations
517+
}
518+
results = {}
519+
failures = []
520+
for key in keys:
521+
try:
522+
results[key] = futures[key].result()
523+
except Exception as exc:
524+
failures.append((key, exc))
525+
if failures:
526+
key, exc = failures[0]
527+
if isinstance(exc, ProviderError):
528+
raise ProviderError("{}: {}".format(key, exc))
529+
raise ProviderError("{}: exact Azure inventory read failed: {}".format(key, exc))
530+
return results
531+
532+
492533
def show_full(controller, resource_id, api_version=None, inventory_missing_ok=False):
493534
args = ["resource", "show", "--ids", resource_id]
494535
if api_version:
@@ -1098,37 +1139,116 @@ def inventory(controller, include_metrics=True, target_slot=None):
10981139
raise ProviderError("Azure subscription scope is not the exact enabled controller binding")
10991140

11001141
prefix = re.escape(controller["prefix"])
1101-
vms = list_json(
1102-
controller,
1103-
["vm", "list", "--resource-group", controller["resource_group"], "--show-details"],
1104-
transient_not_found_attempts=4,
1105-
)
1106-
nics = list_json(controller, ["network", "nic", "list", "--resource-group", controller["resource_group"]])
1107-
disks = list_json(controller, ["disk", "list", "--resource-group", controller["resource_group"]])
1108-
identities = list_json(controller, ["identity", "list", "--resource-group", controller["resource_group"]])
1109-
extensions = list_json(controller, [
1110-
"resource", "list", "--resource-group", controller["resource_group"],
1111-
"--resource-type", "Microsoft.Compute/virtualMachines/extensions",
1112-
])
1113-
run_commands = list_json(controller, [
1114-
"resource", "list", "--resource-group", controller["resource_group"],
1115-
"--resource-type", "Microsoft.Compute/virtualMachines/runCommands",
1116-
])
1117-
schedules = list_json(controller, [
1118-
"resource", "list", "--resource-group", controller["resource_group"],
1119-
"--resource-type", "Microsoft.DevTestLab/schedules",
1120-
])
1121-
# Scope casing varies across ARM responses, so filter client-side rather
1122-
# than with a case-sensitive JMESPath query.
1123-
scope_marker = "/resourcegroups/{}/".format(controller["resource_group"]).lower()
1124-
roles = [
1125-
role for role in list_json(controller, ["role", "assignment", "list", "--all"])
1126-
if scope_marker in str(role.get("scope") or "").lower()
1127-
]
1128-
containers = list_json(controller, [
1129-
"storage", "container", "list", "--auth-mode", "login", "--include-metadata",
1130-
"--account-name", os.environ.get("FM_AZURE_STORAGE_NAME", ""),
1131-
])
1142+
if target_slot is None:
1143+
vms = list_json(
1144+
controller,
1145+
["vm", "list", "--resource-group", controller["resource_group"], "--show-details"],
1146+
transient_not_found_attempts=4,
1147+
)
1148+
nics = list_json(controller, ["network", "nic", "list", "--resource-group", controller["resource_group"]])
1149+
disks = list_json(controller, ["disk", "list", "--resource-group", controller["resource_group"]])
1150+
identities = list_json(controller, ["identity", "list", "--resource-group", controller["resource_group"]])
1151+
extensions = list_json(controller, [
1152+
"resource", "list", "--resource-group", controller["resource_group"],
1153+
"--resource-type", "Microsoft.Compute/virtualMachines/extensions",
1154+
])
1155+
run_commands = list_json(controller, [
1156+
"resource", "list", "--resource-group", controller["resource_group"],
1157+
"--resource-type", "Microsoft.Compute/virtualMachines/runCommands",
1158+
])
1159+
schedules = list_json(controller, [
1160+
"resource", "list", "--resource-group", controller["resource_group"],
1161+
"--resource-type", "Microsoft.DevTestLab/schedules",
1162+
])
1163+
# Scope casing varies across ARM responses, so filter client-side rather
1164+
# than with a case-sensitive JMESPath query.
1165+
scope_marker = "/resourcegroups/{}/".format(controller["resource_group"]).lower()
1166+
roles = [
1167+
role for role in list_json(controller, ["role", "assignment", "list", "--all"])
1168+
if scope_marker in str(role.get("scope") or "").lower()
1169+
]
1170+
containers = list_json(controller, [
1171+
"storage", "container", "list", "--auth-mode", "login", "--include-metadata",
1172+
"--account-name", os.environ.get("FM_AZURE_STORAGE_NAME", ""),
1173+
])
1174+
else:
1175+
names = expected_names(controller, target_slot)
1176+
resource_group = controller["resource_group"]
1177+
storage = os.environ.get("FM_AZURE_STORAGE_NAME", "")
1178+
1179+
vm_id = exact_id(controller, "Microsoft.Compute", "virtualMachines", names["vm"])
1180+
exact = show_exact_optional_many(controller, [
1181+
("vm", [
1182+
"vm", "show", "--resource-group", resource_group,
1183+
"--name", names["vm"], "--show-details",
1184+
]),
1185+
("nic", [
1186+
"network", "nic", "show", "--resource-group", resource_group,
1187+
"--name", names["nic"],
1188+
]),
1189+
("os-disk", [
1190+
"disk", "show", "--resource-group", resource_group,
1191+
"--name", names["os-disk"],
1192+
]),
1193+
("task-disk", [
1194+
"disk", "show", "--resource-group", resource_group,
1195+
"--name", names["task-disk"],
1196+
]),
1197+
("account-disk", [
1198+
"disk", "show", "--resource-group", resource_group,
1199+
"--name", names["account-disk"],
1200+
]),
1201+
("identity", [
1202+
"identity", "show", "--resource-group", resource_group,
1203+
"--name", names["identity"],
1204+
]),
1205+
("monitor-extension", [
1206+
"resource", "show", "--ids",
1207+
vm_id + "/extensions/" + names["monitor-extension"],
1208+
]),
1209+
("bootstrap-command", [
1210+
"resource", "show", "--ids",
1211+
vm_id + "/runCommands/" + names["bootstrap-command"],
1212+
]),
1213+
("task-command", [
1214+
"resource", "show", "--ids",
1215+
vm_id + "/runCommands/" + names["task-command"],
1216+
]),
1217+
("ttl-schedule", [
1218+
"resource", "show", "--ids", exact_id(
1219+
controller, "Microsoft.DevTestLab", "schedules", names["ttl-schedule"],
1220+
), "--api-version", "2018-09-15",
1221+
]),
1222+
("state-container", [
1223+
"storage", "container", "show", "--auth-mode", "login",
1224+
"--account-name", storage, "--name", names["state-container"],
1225+
]),
1226+
])
1227+
1228+
def present(kind):
1229+
return [exact[kind]] if exact[kind] is not None else []
1230+
1231+
vms = present("vm")
1232+
nics = present("nic")
1233+
disks = [
1234+
exact[kind] for kind in ("os-disk", "task-disk", "account-disk")
1235+
if exact[kind] is not None
1236+
]
1237+
identities = present("identity")
1238+
extensions = present("monitor-extension")
1239+
run_commands = [
1240+
exact[kind] for kind in ("bootstrap-command", "task-command")
1241+
if exact[kind] is not None
1242+
]
1243+
schedules = present("ttl-schedule")
1244+
containers = present("state-container")
1245+
container_scope = (
1246+
exact_id(controller, "Microsoft.Storage", "storageAccounts", storage)
1247+
+ "/blobServices/default/containers/" + names["state-container"]
1248+
)
1249+
roles = list_json(controller, [
1250+
"role", "assignment", "list", "--scope", container_scope,
1251+
]) if containers else []
11321252

11331253
workers = {}
11341254
conflicts = []
@@ -1234,7 +1354,9 @@ def add(kind, value, slot, power=None, tags_override=None):
12341354
)
12351355
# The generic resource listing omits properties and etag; only the full
12361356
# object carries an immutable child identity.
1237-
value = show_full(controller, extension["id"], inventory_missing_ok=True)
1357+
value = dict(extension) if target_slot is not None else show_full(
1358+
controller, extension["id"], inventory_missing_ok=True,
1359+
)
12381360
if value is None:
12391361
continue
12401362
value["attached_to"] = vm_id
@@ -1250,7 +1372,9 @@ def add(kind, value, slot, power=None, tags_override=None):
12501372
if kind is None:
12511373
conflicts.append({"kind": "run-command", "slot": slot, "reason": "undeclared worker Run Command child"})
12521374
continue
1253-
value = show_full(controller, command["id"], inventory_missing_ok=True)
1375+
value = dict(command) if target_slot is not None else show_full(
1376+
controller, command["id"], inventory_missing_ok=True,
1377+
)
12541378
if value is None:
12551379
continue
12561380
value["attached_to"] = exact_id(
@@ -1263,7 +1387,7 @@ def add(kind, value, slot, power=None, tags_override=None):
12631387
for schedule in schedules:
12641388
slot = slot_from_name(schedule.get("name"), r"^shutdown-computevm-vm-{}-wkr-".format(prefix))
12651389
if slot is not None and (target_slot is None or slot == target_slot):
1266-
value = show_full(
1390+
value = dict(schedule) if target_slot is not None else show_full(
12671391
controller, schedule["id"], api_version="2018-09-15",
12681392
inventory_missing_ok=True,
12691393
)

docs/azure-workers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ The guest verifies the runtime's exact file inventory, runs the role command wit
404404
The wrapper verifies the semantic bytes and head binding before writing the controller-facing result; a process exit, missing outcome, malformed outcome, or changed read-only head is a failed result, never `CLEAR` by inference.
405405
Repair results return one digest-bound single-ref bundle whose head must descend from the requested head, while review and test return no code bundle and must keep the exact requested head.
406406
The wrapper records a retryable local candidate before cleanup, releases through `service-complete` only after the lifecycle owns the exact execution result, and replays the candidate after a lost response instead of executing the step again.
407-
Admission, execute recovery, and cleanup use `service-reconcile`, which advances only the caller's exact task generation or replays that task's own pending slot claim rather than converging unrelated fleet work. Once a service task owns a slot, its execution and cleanup inventory expands only that exact slot's Azure children; queued admission retains the whole-fleet quota, spend, and conflict census.
407+
Admission, execute recovery, and cleanup use `service-reconcile`, which advances only the caller's exact task generation or replays that task's own pending slot claim rather than converging unrelated fleet work. Once a service task owns a slot, its execution and cleanup inventory reads that slot's deterministic Azure object paths in a bounded parallel batch; it neither lists the resource group nor expands peer children. Queued admission retains the whole-fleet quota, spend, and conflict census.
408408
The guest supervisor marks a no-mistakes Azure execution as the already-isolated test boundary, so the repository test command runs the focused service suite directly instead of recursively provisioning the general validation fleet or a Herdr lab.
409409
The root-owned supervisor stages the job, then runs the no-mistakes process as the dedicated non-root `fmworker` user with no supplementary groups; the sealed runtime remains root-owned and read-only while the exact repository and projected account are writable only by that service identity.
410410
`bin/fm-azure-service-test-scope.py` owns that focused inventory and the narrow source set eligible for focused pull-request CI; an empty, mixed, or unknown diff and every push to `main` retain the complete behavior suite.

tests/fm-worker-lifecycle.test.sh

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9223,40 +9223,45 @@ controller = {
92239223
tags = {
92249224
"workload": "firstmate", "deployment-generation": "dep", "cleanup-owner": "owner",
92259225
}
9226-
vms = [
9227-
{"name": "vm-fixture-wkr-01", "id": "/vm/1", "vmId": "vm-id-1",
9228-
"powerState": "VM running", "tags": tags},
9229-
{"name": "vm-fixture-wkr-02", "id": "/vm/2", "vmId": "vm-id-2",
9230-
"powerState": "VM running", "tags": tags},
9231-
]
9232-
extensions = [
9233-
{"name": "vm-fixture-wkr-01/AzureMonitorLinuxAgent", "id": "/vm/1/ext"},
9234-
{"name": "vm-fixture-wkr-02/AzureMonitorLinuxAgent", "id": "/vm/2/ext"},
9235-
]
9236-
expanded = []
9237-
9238-
provider.az = lambda _controller, args, check=False, timeout=provider.AZ_TIMEOUT_SECONDS: (
9239-
({"id": controller["subscription"], "state": "Enabled"}, 0, "")
9240-
if args[:2] == ["account", "show"] else (None, 1, "unexpected")
9226+
calls = []
9227+
vm_id = provider.exact_id(
9228+
controller, "Microsoft.Compute", "virtualMachines", "vm-fixture-wkr-02",
9229+
)
9230+
extension_id = vm_id + "/extensions/AzureMonitorLinuxAgent"
9231+
9232+
def exact_read(_controller, args, check=False, timeout=provider.AZ_TIMEOUT_SECONDS):
9233+
calls.append(tuple(args))
9234+
if args[:2] == ["account", "show"]:
9235+
return {"id": controller["subscription"], "state": "Enabled"}, 0, ""
9236+
if args[:2] == ["vm", "show"]:
9237+
assert args[args.index("--name") + 1] == "vm-fixture-wkr-02", args
9238+
return {
9239+
"name": "vm-fixture-wkr-02", "id": vm_id, "vmId": "vm-id-2",
9240+
"powerState": "VM running", "tags": tags, "identity": {},
9241+
}, 0, ""
9242+
if args[:3] == ["resource", "show", "--ids"] and args[3] == extension_id:
9243+
return {
9244+
"name": "vm-fixture-wkr-02/AzureMonitorLinuxAgent",
9245+
"id": extension_id, "tags": tags,
9246+
"properties": {"provisioningState": "Succeeded"},
9247+
}, 0, ""
9248+
if args[:3] == ["storage", "container", "show"]:
9249+
return None, 1, "ErrorCode:ContainerNotFound"
9250+
return None, 1, "(ResourceNotFound) exact fixture absence"
9251+
9252+
provider.az = exact_read
9253+
provider.list_json = lambda *_args, **_kwargs: (_ for _ in ()).throw(
9254+
AssertionError("targeted inventory issued a list operation")
92419255
)
9242-
def listing(_controller, args, transient_not_found_attempts=1):
9243-
if args[:2] == ["vm", "list"]:
9244-
return vms
9245-
if args[:2] == ["resource", "list"] and args[-1] == "Microsoft.Compute/virtualMachines/extensions":
9246-
return extensions
9247-
return []
9248-
provider.list_json = listing
9249-
def show(_controller, resource_id, api_version=None, inventory_missing_ok=False):
9250-
expanded.append(resource_id)
9251-
return {
9252-
"id": resource_id, "tags": tags,
9253-
"properties": {"provisioningState": "Succeeded"},
9254-
}
9255-
provider.show_full = show
92569256
92579257
snapshot = provider.inventory_slot(controller, 2)
92589258
assert [worker["slot"] for worker in snapshot["workers"]] == [2], snapshot
9259-
assert expanded == ["/vm/2/ext"], expanded
9259+
assert snapshot["workers"][0]["resources"]["monitor-extension"]["id"] == extension_id
9260+
assert all("wkr-01" not in " ".join(call) for call in calls), calls
9261+
assert not any("list" in call[:3] for call in calls), calls
9262+
assert any(call[:2] == ("vm", "show") for call in calls), calls
9263+
assert any(call[:2] == ("network", "nic") for call in calls), calls
9264+
assert sum(call[:2] == ("disk", "show") for call in calls) == 3, calls
92609265
assert snapshot["capacity_reservations"] == []
92619266
assert snapshot["metrics"]["actual_usd"] is None
92629267
try:

0 commit comments

Comments
 (0)