fix(sys-service-loader): hand preload-context lifecycle to the constructor

list_or_shoot.yml was previously a no-op during the constructor's
"Bootstrap registry + NFS clients" block because nothing had activated
the preload context yet, so transitive deps such as sys-ctl-hlth-msmtp
got inline-shot before web-app-mailu existed and crashed every swarm
deploy. The constructor now opens the preload context (`service_loader_preloading: true` plus an empty `service_loader_post_run`) right before the Bootstrap block, so list_or_shoot routes everything it touches into the post-load queue and the loader's existing flush phase runs them after the preload pass has brought up web-app-mailu.

sys-service-loader's own queue-init task is gone (the constructor is
the sole opener of the context), list_or_shoot's append falls back
to `default([])` for standalone callers, and the two facts lose
their leading underscore because they are now public play-level state
owned by the constructor rather than a role-internal convention.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 4.7 committed 2026-06-10 21:17:27 +02:00
1 parent a1bd538765
commit 68adb944ff
5 files changed
+19 -18

No files matched your search

+1 -1
View File
@@ -39,4 +39,4 @@
state: started
when:
- MODE_ASSERT_LOCAL | bool
- not (_service_loader_preloading | default(false) | bool)
- not (service_loader_preloading | default(false) | bool)
@@ -1,7 +1,7 @@
---
# Route a list of role names through the service-loader queue when inside a
# preload context, otherwise include them immediately. Encapsulates the
# `_service_loader_preloading`-aware branching so callers can stay terse.
# `service_loader_preloading`-aware branching so callers can stay terse.
#
# Usage:
# - include_role:
@@ -13,8 +13,8 @@
# - sys-ctl-hlth-container
#
# - During the preload pass driven by sys-service-loader
# (`_service_loader_preloading: true`), the names are appended to
# `_service_loader_post_run` for the loader's post-load flush.
# (`service_loader_preloading: true`), the names are appended to
# `service_loader_post_run` for the loader's post-load flush.
# - Outside any preload pass, the roles are included on the spot, so the
# helper is a no-op compared with a direct `include_role` loop. This is
# the standalone fallback when the service-loader is bypassed (tests,
@@ -22,9 +22,9 @@
- name: "List post-load roles for service-loader flush"
ansible.builtin.set_fact:
_service_loader_post_run: "{{ _service_loader_post_run + list_or_shoot_roles }}"
service_loader_post_run: "{{ (service_loader_post_run | default([])) + list_or_shoot_roles }}"
changed_when: false
when: (_service_loader_preloading | default(false) | bool)
when: (service_loader_preloading | default(false) | bool)
- name: "Shoot post-load roles directly (no preload context active)"
ansible.builtin.include_role:
@@ -32,4 +32,4 @@
loop: "{{ list_or_shoot_roles }}"
loop_control:
loop_var: list_or_shoot_role
when: not (_service_loader_preloading | default(false) | bool)
when: not (service_loader_preloading | default(false) | bool)
@@ -3,7 +3,7 @@
block:
- name: "Mark shared service preload context for {{ service_key }}"
ansible.builtin.set_fact:
_service_loader_preloading: true
service_loader_preloading: true
changed_when: false
- name: "Load {{ service_key }} (Once)"
@@ -16,5 +16,5 @@
always:
- name: "Clear shared service preload context for {{ service_key }}"
ansible.builtin.set_fact:
_service_loader_preloading: false
service_loader_preloading: false
changed_when: false
+4 -9
View File
@@ -9,17 +9,12 @@
#
# Two-phase contract:
# 1. Preload pass — iterate `_ordered_services` in dependency order and
# load each service body once (`_service_loader_preloading: true`).
# 2. Post-load pass — flush the `_service_loader_post_run` queue that
# load each service body once (`service_loader_preloading: true`).
# 2. Post-load pass — flush the `service_loader_post_run` queue that
# roles registered during preload via this role's `list_or_shoot.yml`.
# Tasks listed there run with `_service_loader_preloading: false`,
# Tasks listed there run with `service_loader_preloading: false`,
# so they see the fully preloaded service graph.
- name: "Reset service-loader post-load queue"
ansible.builtin.set_fact:
_service_loader_post_run: []
changed_when: false
- name: "Discover ordered shared services"
ansible.builtin.set_fact:
_ordered_services: >-
@@ -50,6 +45,6 @@
- name: "Run service-loader post-load roles"
ansible.builtin.include_role:
name: "{{ post_run_role }}"
loop: "{{ _service_loader_post_run | unique }}"
loop: "{{ service_loader_post_run | unique }}"
loop_control:
loop_var: post_run_role
+6
View File
@@ -151,6 +151,12 @@
name: svc-swarm-node
when: "'svc-swarm-node' | application_allowed(group_names, lookup('deployment').whitelist)"
- name: "Open service-loader preload context BEFORE Bootstrap"
ansible.builtin.set_fact:
service_loader_post_run: []
service_loader_preloading: true
changed_when: false
- name: "Bootstrap registry + NFS clients (MUST precede Preload)"
ansible.builtin.include_tasks: "./tasks/groups/{{ role_group }}-roles.yml"
loop: