GitLab's migration on its dedicated postgres sidecar failed in CI run
35674334334 with "ERROR: out of shared memory" and the hint to raise
max_locks_per_transaction. The default of 64 locks per transaction cannot
cover a schema that locks thousands of tables and indexes in one
transaction: a reproduction on postgres:18 failed at table 7453 with the
default and held 20000 tables at 256.
The value sits next to the other postgres tuning constants in the
resource lookup, is exposed as POSTGRES_MAX_LOCKS_PER_TRANSACTION, and
reaches both the central svc-db-postgres command and the per-app sidecar.
The sidecar's new command keeps the image entrypoint and only adds the
flag. The shared lock table grows to about 256 x max_connections entries,
roughly 30 MB at the 400-connection cap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
plugins/lookup/timeout.py takes TIMEOUT_FACTOR straight off `variables`. That
is a plain dict read, and a dict read never renders Jinja, so routing the
constant through the resource lookup handed the plugin the template string
instead of a number. The acceptance deploy died on the first scaled wait:
lookup('timeout'): TIMEOUT_FACTOR "{{ lookup('resource', 'timeout_factor') }}"
is not a number
tasks/utils/apt/ready.yml:15
Drop timeout_factor from the resource lookup and restore the literal. Every
other constant in 17_resource.yml is read through Jinja and stays on the
lookup; this one is the exception because its consumer is Python.
The new guard test pins that boundary, so the next attempt to route a
Python-read constant through a lookup fails in the suite rather than on a host.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Output of `make quality-high`, which rewrites the tree before it tests it.
Formatting only: the YAML null goes from `~` to an empty value, which parses
the same, and the Python changes are line reflow plus the parenthesized `with`
groups.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ansible's Linux hardware collector reads /sys/block/<dev>/size without a None
guard, and that read runs before the CPU facts in the same collector. One
unreadable block device therefore drops every hardware fact at once and without
a warning. Seven expressions across four files dereferenced processor_vcpus or
memtotal_mb directly and died on the first render. Measured on the interpreter
the deploy actually uses, gather_subset=all returns none of those facts.
plugins/lookup/resource.py now measures and computes every host-resource
constant. The two measured inputs resolve as an inventory override, then the
host's own hardware facts, then a probe of the machine the plugin runs on. The
probe applies only to a target reached over the local connection, because a
lookup executes on the Ansible controller and would otherwise report the
controller's hardware for a remote host.
No value is ever substituted. What that abort may interrupt depends on context.
A play carries inventory_hostname and has to stop, while the inventory
generator and the lint suite render the same applications payload with no host
and must not fail over a value they never read. The first case raises
UnresolvableValueError, the one class the best-effort render layers re-raise
instead of swallowing; the second raises an ordinary AnsibleError that they
absorb, exactly as the missing fact did before.
The meta files of web-app-nextcloud and svc-opt-swapfile call the lookup
directly rather than through a group_var. _render_with_templar replaces the
templar's variables with a flat dict, which drops the lazy resolution a play
uses to expand a variable that itself holds a lookup, so the indirection
renders 0M there while the same expression yields 2133M in a play.
The container count is computed only for the keys that divide by it. Reading
applications pulls the payload back through the templar while it is still being
produced, and those meta files sit in that payload.
utils/cache/errors.py lives in the cache package rather than at the top of
utils/: ansible.cfg exposes ./utils as module_utils, so a top-level errors.py
becomes ansible.module_utils.errors and shadows the real one inside every
module payload zip, which broke uri and get_url with an ImportError pointing
into a temporary zip. A lint now guards that class of collision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>