fix(delivery): fit bounded health probes into startup budgets
This commit is contained in:
@@ -415,9 +415,16 @@ end-to-end target still requires measured acceptance.
|
||||
Fixed health helpers include interpreter startup in their wall-clock deadline.
|
||||
Outbox and import queries have 60 seconds each; factory status has 30 seconds and
|
||||
broker metadata has 45 seconds. Node readiness has 300 seconds for the sequential
|
||||
checks. Each helper keeps its fixed CPU, memory, process and privilege limits.
|
||||
These operator deadlines do not change student execution budgets. An expired or
|
||||
failed query keeps admission closed.
|
||||
checks. Each ordinary helper has one CPU within the node's two-CPU ceiling;
|
||||
its memory, process and privilege limits remain fixed. These operator budgets
|
||||
do not change student execution budgets. Startup opens admission only after all
|
||||
required readiness queries succeed.
|
||||
|
||||
Broker metadata uses one fixed read-only RPC with a five-second remote deadline
|
||||
and a 30-second CLI process deadline, within the helper's 45-second limit. It reads
|
||||
queue metadata, vhost limits, usernames/tags and permissions; it does not read
|
||||
messages or password hashes. Readiness requires exactly the configured classic
|
||||
queue and budgets, including RabbitMQ's optional `x-queue-type=classic` argument.
|
||||
|
||||
The broker helper has a 64 MiB per-file ceiling for Erlang JIT mappings.
|
||||
[OTP 27.3.4](https://github.com/erlang/otp/blob/OTP-27.3.4/erts/emulator/beam/jit/beam_jit_main.cpp)
|
||||
|
||||
@@ -4,56 +4,33 @@ import argparse
|
||||
import json
|
||||
import stat
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from . import broker_snapshot, operator_units, preflight
|
||||
from . import dependency_policy as dependencies
|
||||
from . import operator_units, preflight
|
||||
from .policy import DeliveryError, canonical, fields, integer
|
||||
|
||||
CTL = Path("/usr/lib/rabbitmq/sbin/rabbitmqctl")
|
||||
QUERIES = {
|
||||
"queues": [
|
||||
"list_queues",
|
||||
"-p",
|
||||
"kf",
|
||||
"name",
|
||||
"durable",
|
||||
"auto_delete",
|
||||
"exclusive",
|
||||
"type",
|
||||
"state",
|
||||
"arguments",
|
||||
"messages_ready",
|
||||
"messages_unacknowledged",
|
||||
"message_bytes",
|
||||
"consumers",
|
||||
"policy",
|
||||
"operator_policy",
|
||||
"effective_policy_definition",
|
||||
],
|
||||
"vhost_limits": ["list_vhost_limits", "--global"],
|
||||
"users": ["list_users"],
|
||||
"permissions": ["list_permissions", "-p", "kf"],
|
||||
}
|
||||
CTL = broker_snapshot.CTL
|
||||
|
||||
|
||||
def queue_arguments(value) -> dict:
|
||||
if isinstance(value, dict):
|
||||
result = value
|
||||
elif isinstance(value, list) and len(value) == 4:
|
||||
result = dict(value)
|
||||
elif isinstance(value, list) and len(value) in (4, 5):
|
||||
result = {}
|
||||
for row in value:
|
||||
if not isinstance(row, list) or len(row) != 3 or row[0] in result:
|
||||
raise DeliveryError("duplicate/unknown broker argument")
|
||||
name, kind, argument = row
|
||||
if name not in dependencies.QUEUE_ARGUMENTS or kind != (
|
||||
"longstr" if name == "x-overflow" else "long"
|
||||
if name not in {*dependencies.QUEUE_ARGUMENTS, "x-queue-type"} or kind != (
|
||||
"longstr" if name in {"x-overflow", "x-queue-type"} else "long"
|
||||
):
|
||||
raise DeliveryError("broker argument type differs")
|
||||
result[name] = argument
|
||||
else:
|
||||
raise DeliveryError("broker argument shape differs")
|
||||
if "x-queue-type" in result and result.pop("x-queue-type") != "classic":
|
||||
raise DeliveryError("actual broker queue type differs")
|
||||
if canonical(result) != canonical(dependencies.QUEUE_ARGUMENTS):
|
||||
raise DeliveryError("actual broker queue budgets differ")
|
||||
return result
|
||||
@@ -148,49 +125,6 @@ def validate_snapshot(value: dict) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def command(name: str):
|
||||
if name not in QUERIES:
|
||||
raise DeliveryError("unknown broker metadata query")
|
||||
# A file-backed capped stream prevents malicious metadata/log output from
|
||||
# allocating an unbounded capture buffer. The unit adds a hard file limit.
|
||||
env = {
|
||||
"PATH": "/usr/bin:/bin",
|
||||
"LC_ALL": "C.UTF-8",
|
||||
"HOME": dependencies.STORAGE["rabbitmq"]["path"],
|
||||
"ERL_EPMD_ADDRESS": "127.0.0.1",
|
||||
"RABBITMQ_NODENAME": "kf@localhost",
|
||||
"RABBITMQ_CTL_ERL_ARGS": (
|
||||
"+S 1:1 +A 2 +P 1024 +Q 1024 -kernel inet_dist_use_interface {127,0,0,1}"
|
||||
),
|
||||
}
|
||||
with tempfile.TemporaryFile() as output:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
str(CTL),
|
||||
"-q",
|
||||
"-n",
|
||||
"kf@localhost",
|
||||
"--timeout",
|
||||
"5",
|
||||
*QUERIES[name],
|
||||
"--formatter=json",
|
||||
],
|
||||
stdout=output,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=8,
|
||||
env=env,
|
||||
)
|
||||
except subprocess.TimeoutExpired as error:
|
||||
raise DeliveryError("broker " + name + " query exceeded deadline") from error
|
||||
if result.returncode:
|
||||
raise DeliveryError("broker " + name + " query failed")
|
||||
if output.tell() > 32768:
|
||||
raise DeliveryError("broker " + name + " query output exceeds bound")
|
||||
output.seek(0)
|
||||
return operator_units.decode(output.read(32769))
|
||||
|
||||
|
||||
def observe() -> dict:
|
||||
operator_units.guarded("broker-health")
|
||||
preflight.protected(CTL)
|
||||
@@ -202,8 +136,7 @@ def observe() -> dict:
|
||||
or (info.st_uid, info.st_gid, stat.S_IMODE(info.st_mode)) != (1204, 1204, 0o400)
|
||||
):
|
||||
raise DeliveryError("ordinary private broker cookie required")
|
||||
data = {name: command(name) for name in QUERIES}
|
||||
return validate_snapshot(data)
|
||||
return validate_snapshot(broker_snapshot.command())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""One fixed read-only RabbitMQ 4.3.6 metadata RPC; no caller expressions."""
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from . import dependency_policy, operator_units
|
||||
from .policy import DeliveryError, fields
|
||||
|
||||
CTL = Path("/usr/lib/rabbitmq/sbin/rabbitmqctl")
|
||||
# Fixed getters only: no caller text, mutation, message reads or password hashes.
|
||||
SNAPSHOT_EXPRESSION = r"""
|
||||
rpc:call(node(), erlang, apply, [fun() ->
|
||||
Count = rabbit_amqqueue:count(<<"kf">>),
|
||||
true = (Count =:= 0 orelse Count =:= 1),
|
||||
Normalize = fun
|
||||
(name, {resource, <<"kf">>, queue, Value}) -> Value;
|
||||
(arguments, Value) -> [[K, atom_to_binary(T, utf8), V] || {K,T,V} <- Value];
|
||||
(type, rabbit_classic_queue) -> <<"classic">>;
|
||||
(K, Value) when K =:= policy; K =:= operator_policy ->
|
||||
case Value of undefined -> <<>>; none -> <<>>; '' -> <<>>; _ -> Value end;
|
||||
(effective_policy_definition, Value) when is_list(Value) -> maps:from_list(Value);
|
||||
(tags, Value) -> [case T of
|
||||
A when is_atom(A) -> atom_to_binary(A,utf8); _ -> T end || T <- Value];
|
||||
(_, Value) when is_atom(Value), Value =/= true, Value =/= false ->
|
||||
atom_to_binary(Value,utf8);
|
||||
(_, Value) -> Value
|
||||
end,
|
||||
ToMap = fun(Items) -> maps:from_list(
|
||||
[{atom_to_binary(K,utf8),Normalize(K,V)} || {K,V} <- Items]) end,
|
||||
Queues = [ToMap(Row) || Row <- rabbit_amqqueue:info_all(<<"kf">>,
|
||||
[name,durable,auto_delete,exclusive,type,state,arguments,messages_ready,
|
||||
messages_unacknowledged,message_bytes,consumers,policy,operator_policy,
|
||||
effective_policy_definition])],
|
||||
Limits = [#{<<"vhost">> => V,
|
||||
<<"limits">> => iolist_to_binary(rabbit_json:encode(maps:from_list(L)))}
|
||||
|| {V,L} <- rabbit_vhost_limit:list()],
|
||||
Users = [ToMap(Row) || Row <- rabbit_auth_backend_internal:list_users()],
|
||||
Permissions = [ToMap(Row) || Row <-
|
||||
rabbit_auth_backend_internal:list_vhost_permissions(<<"kf">>)],
|
||||
iolist_to_binary(rabbit_json:encode(#{<<"queues">> => Queues, <<"vhost_limits">> => Limits,
|
||||
<<"users">> => Users, <<"permissions">> => Permissions}))
|
||||
end, []], 5000).
|
||||
""".strip()
|
||||
|
||||
|
||||
def command() -> dict:
|
||||
env = {
|
||||
"PATH": "/usr/bin:/bin",
|
||||
"LC_ALL": "C.UTF-8",
|
||||
"HOME": dependency_policy.STORAGE["rabbitmq"]["path"],
|
||||
"ERL_EPMD_ADDRESS": "127.0.0.1",
|
||||
"RABBITMQ_NODENAME": "kf@localhost",
|
||||
"RABBITMQ_CTL_ERL_ARGS": (
|
||||
"+S 1:1 +A 2 +P 1024 +Q 1024 -kernel inet_dist_use_interface {127,0,0,1}"
|
||||
),
|
||||
}
|
||||
with tempfile.TemporaryFile() as output:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
str(CTL),
|
||||
"-q",
|
||||
"-n",
|
||||
"kf@localhost",
|
||||
"--timeout",
|
||||
"5",
|
||||
"eval",
|
||||
SNAPSHOT_EXPRESSION,
|
||||
"--formatter=json",
|
||||
],
|
||||
stdout=output,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=30,
|
||||
env=env,
|
||||
)
|
||||
except subprocess.TimeoutExpired as error:
|
||||
raise DeliveryError("broker snapshot query exceeded deadline") from error
|
||||
if result.returncode:
|
||||
raise DeliveryError("broker snapshot query failed")
|
||||
if output.tell() > 32768:
|
||||
raise DeliveryError("broker snapshot query output exceeds bound")
|
||||
output.seek(0)
|
||||
value = operator_units.decode(output.read(32769))
|
||||
fields(value, {"result", "node", "value"}, "broker snapshot envelope")
|
||||
if (
|
||||
value["result"] != "ok"
|
||||
or value["node"] != "kf@localhost"
|
||||
or not isinstance(value["value"], str)
|
||||
):
|
||||
raise DeliveryError("broker snapshot result or node differs")
|
||||
return fields(
|
||||
operator_units.decode(value["value"].encode()),
|
||||
{"queues", "vhost_limits", "users", "permissions"},
|
||||
"broker snapshot",
|
||||
)
|
||||
@@ -66,6 +66,7 @@ def binding() -> tuple[dict, dict, Path]:
|
||||
"availability.py",
|
||||
"node_health.py",
|
||||
"broker_health.py",
|
||||
"broker_snapshot.py",
|
||||
"factory_health.py",
|
||||
"import_health.py",
|
||||
"operator_units.py",
|
||||
|
||||
@@ -11,10 +11,10 @@ from .policy import DeliveryError, invalid_constant, unique_object
|
||||
|
||||
PROFILES = {
|
||||
"node-health": ("root", 0, 128, 16, 50, 300),
|
||||
"broker-health": ("rabbitmq", 1204, 256, 64, 50, 45),
|
||||
"factory-health": ("kf-worker", 1202, 32, 4, 10, 30),
|
||||
"import-health": ("kf-oj", 1200, 128, 16, 25, 60),
|
||||
"outbox-health": ("kf-oj", 1200, 128, 16, 25, 60),
|
||||
"broker-health": ("rabbitmq", 1204, 256, 64, 100, 45),
|
||||
"factory-health": ("kf-worker", 1202, 32, 4, 100, 30),
|
||||
"import-health": ("kf-oj", 1200, 128, 16, 100, 60),
|
||||
"outbox-health": ("kf-oj", 1200, 128, 16, 100, 60),
|
||||
"node-maintenance": ("root", 0, 256, 32, 50, 900),
|
||||
"node-observation": ("root", 0, 128, 16, 25, 360),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user