Fix node-recycler filtering with cached layout and live-node retries - #194
Fix node-recycler filtering with cached layout and live-node retries#194Rui Gao (hippogr) wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens node selection and Kusto query scoping to avoid cross-endpoint contamination, and makes node-recycler more resilient by introducing centralized filtering with cached layout parsing and retrying live-node discovery.
Changes:
- Adds endpoint-scoped filters (
Endpoint == self.endpoint) to node status/action Kusto queries. - Introduces policy-based candidate filtering in
node-recycler, including one-time layout caching and live-node retries with backoff. - Updates deployment/runtime inputs (mount cluster layout config, add PyYAML) and extends tests for the new filtering/retry behavior.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/kusto-sdk/ltp_kusto_sdk/features/node_status/client.py | Adds endpoint scoping to node status queries and adjusts the “latest status” query shape. |
| src/kusto-sdk/ltp_kusto_sdk/features/node_action/client.py | Adds endpoint scoping to node action queries (duplicate check + retrieval paths). |
| src/kusto-sdk/tests/test_node_status_client.py | Updates assertions to require endpoint filtering in generated queries. |
| src/alert-manager/src/node-recycler/recycler.py | Adds cached layout parsing, live-node loading with retry/backoff, and centralized policy filtering for OFR/operate/validate. |
| src/alert-manager/src/node-recycler/tests/test_recycler.py | Extends test coverage for caching, fallback behavior, and retry exhaustion/success paths; stabilizes VMSS test side effects. |
| src/alert-manager/src/node-recycler/requirements.txt | Adds PyYAML dependency for parsing layout.yaml. |
| src/alert-manager/deploy/alert-manager-deployment.yaml.template | Mounts pai-configuration into the node-recycler container for layout access. |
Suppressed comments (2)
src/kusto-sdk/ltp_kusto_sdk/features/node_status/client.py:150
- The endpoint value is interpolated directly into the Kusto query string without escaping. If CLUSTER_ID/endpoint ever contains a single quote, the query will become invalid (and this pattern can open up query-injection risks). Escaping single quotes makes the query construction more robust.
query = (
f"{self.table_name}"
f" | where HostName == '{hostname}'"
f" | where Endpoint == '{self.endpoint}'"
)
src/kusto-sdk/ltp_kusto_sdk/features/node_status/client.py:218
- This query also interpolates endpoint into the Kusto string without escaping. Escaping single quotes here avoids malformed queries if endpoint contains unexpected characters and keeps query construction consistent with other string sanitization in this client.
query = f"""
{self.table_name}
| where Endpoint == '{self.endpoint}'
{timestamp_condition}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| _live_nodes_retry_attempts = int(os.getenv("LIVE_NODES_RETRY_ATTEMPTS", "3")) | ||
| _live_nodes_retry_interval_seconds = int(os.getenv("LIVE_NODES_RETRY_INTERVAL_SECONDS", "1")) |
| timestamp_str = None | ||
| if timestamp is not None: | ||
| timestamp_str = convert_timestamp(timestamp, format="str") |
| {self.table_name} | ||
| | where Endpoint == '{self.endpoint}' | ||
| | where HostName == '{node}' and Timestamp == datetime('{timestamp}') and Action == '{action}' |
Rui Gao (hippogr)
left a comment
There was a problem hiding this comment.
I found two additional issues that should be considered before merging:
-
Endpoint isolation is still incomplete: In
src/kusto-sdk/ltp_kusto_sdk/features/node_action/client.py:279-290, neither thenext_available_tssubquery nor the final query infind_triaged_failure()filters onEndpoint == self.endpoint. If different clusters contain the same hostname, an action from another endpoint can incorrectly truncate the current cluster's time window, and triaged actions from another endpoint can be returned. Please scope both table scans by endpoint and add a cross-endpoint test. -
A transient layout load failure permanently disables layout filtering: In
src/alert-manager/src/node-recycler/recycler.py:61-72,_layout_nodes_loadedis set toTrueregardless of whether loading succeeds. If the ConfigMap is temporarily unreadable, invalid, or empty at startup, the process never reloads it after the configuration recovers, leaving layout filtering in fail-open mode for the lifetime of the process. Please cache only successful, non-empty results, or retry/refresh failed and empty loads.
|
Follow-up on my earlier layout comment: after reconsidering the deployment model, I recommend fail-fast startup validation instead of retrying or falling back. This PR already mounts the
The retry environment variables should follow the same startup-validation principle. Parse |
|
Addressed review feedback in commit 9e08edc:\n\n1) Endpoint isolation in \n- Added endpoint scoping to both table scans in the KQL (the subquery and the final triaged-actions query).\n- Also escaped endpoint when embedding it in KQL ().\n- Files:\n - \n - (added assertion test for endpoint scoping in this query path)\n\n2) Layout loading behavior\n- Switched to fail-fast startup/dependency behavior for layout initialization:\n - raise if layout load fails\n - raise if parsed node set is empty\n - raise if cached layout state is unexpectedly empty after successful init\n- Removed fail-open fallback in layout filtering.\n- Files:\n - \n - (updated/added tests for runtime-error behavior)\n\nValidation run:\n- .............................. [100%] |
|
Correction note (previous comment had shell-escaping artifacts). Below is the intended update. Addressed review feedback in commit 9e08edc:
Validation run:
Note: |
|
Thanks for the update. The endpoint scoping in There are three remaining review items to check:
The first two are recommended before merging; the annotation fix is low priority but straightforward. |
|
There is still a PostgreSQL backend compatibility issue in the narrowed implementation.
client.get_node_status(node, use_current_endpoint=True)
client.get_latest_node_action(node, use_current_endpoint=True)The new keyword is supported by the Kusto implementations, but the corresponding PostgreSQL methods still have their original signatures and do not accept Please avoid passing this Kusto-specific option through backend-neutral callers. Since the goal is to keep the SDK impact narrow, one approach is to remove the changes from unrelated services and have node-recycler pass the option only when its configured backend is Kusto; PostgreSQL calls should retain their existing signatures. A backend-parameterized test through |
Summary
This PR hardens
node-recyclernode selection and query scoping while keeping behavior safe under transient dependency failures.What this PR changes
1) Endpoint-scoped Kusto queries
Adds
Endpoint == self.endpointfilters to key node action/status query paths so data access is isolated to the current cluster endpoint.src/kusto-sdk/ltp_kusto_sdk/features/node_action/client.pyupdate_node_actionduplicate check queryget_node_actionsget_latest_node_actionget_latest_action_by_statesrc/kusto-sdk/ltp_kusto_sdk/features/node_status/client.pyget_node_statusget_nodes_by_statusThis prevents cross-cluster contamination when hostnames overlap.
2) Policy-based filtering in
node-recyclerIntroduces centralized candidate filtering for OFR/operate/validate flows in:
src/alert-manager/src/node-recycler/recycler.pyFiltering can enforce:
layout.yaml)Ready=True) for validation-stage filtering3) Load layout once per process
Adds one-time layout initialization and caching:
_initialize_layout_nodes()_layout_nodes_cache,_layout_nodes_loaded,_layout_nodes_load_successlayout.yamlis parsed once and reused to reduce repeated file I/O.4) Live-node retry with fail-fast stage skip
Adds live-node retrieval retries with exponential backoff:
_load_live_nodes_with_retry(stage)LIVE_NODES_RETRY_ATTEMPTS(default3)LIVE_NODES_RETRY_INTERVAL_SECONDS(default1)If all retries fail, that stage is skipped and naturally retried in the next loop iteration.
5) Graceful fallback for layout issues
If layout load fails or layout is empty:
This avoids hard-stopping the pipeline solely due to layout data issues.
6) Deployment/runtime support
pai-configurationinto node-recycler container at/pai-cluster-config:src/alert-manager/deploy/alert-manager-deployment.yaml.templatesrc/alert-manager/src/node-recycler/requirements.txt(PyYAML)7) Test coverage improvements
Extended
node-recyclertests:src/alert-manager/src/node-recycler/tests/test_recycler.pyAdded/updated coverage for:
Also updated query assertion for endpoint filtering:
src/kusto-sdk/tests/test_node_status_client.pyValidation
Executed:
pytest -q src/alert-manager/src/node-recycler/tests/test_recycler.pyResult:
Notes
src/kusto-sdk/tests/test_node_status_client.pywas not runnable in this environment due to missing test dependencyazure.kusto.ingestduring collection.