Skip to content

Fix: redis cleanup RPCs failing against unreachable ports (CAN-391) - #175

Merged
jayanthchundru merged 6 commits into
mainfrom
jayanthc/can-391-redis-cleanup-rpcs-fail-against-unreachable-ports-memory
Sep 22, 2026
Merged

jayanthchundru merged 6 commits into
mainfrom
jayanthc/can-391-redis-cleanup-rpcs-fail-against-unreachable-ports-memory

Conversation

@jayanthchundru

Copy link
Copy Markdown

Cleanup RPCs from the Global Controller were dialing the host-published port
(localhost:8001-8006), which isn't reachable from inside its own container --
every cleanup call failed, but the completed-request batch got dropped from
Redis anyway, so memory grew unbounded. Fixed to dial the container-reachable
address instead, and to only drop a batch once every instance confirms receipt.

Fixing that exposed a second issue: once cleanup actually succeeds, it can
delete a request's data before the telemetry poll loop has read it, silently
dropping spans under load. Switched that cleanup to expire the keys with a
grace period instead of deleting them immediately, same pattern already used
in deploy.py.

See commit message for the full investigation and benchmark numbers.

🤖 Generated with Claude Code

…e telemetry reads it (CAN-391)

Global Controller's cleanup broadcast dialed instance["endpoint"], the
host-published port (e.g. localhost:8001), which is unreachable from
inside the GC's own container. Every Cleanup RPC failed, but the
completed-request batch was drained from Redis regardless, so futures
and affinity keys behind those requests were never deleted and Redis
grew unbounded.

- global_controller.py: resolve the cleanup RPC endpoint through
  instance_manager._routing_endpoint_for() (container-reachable),
  matching every other in-container RPC caller in this file. Only
  drain "request:completed" once every instance has confirmed
  receipt; on any failure (or no instances at all), leave the batch
  queued for retry next cleanup cycle. The receiving RPC is already
  idempotent (setnx lock, no-op if futures are already gone), so
  retrying is safe.

- local_controller_frontend.py: once cleanup RPCs actually succeed,
  a second issue surfaces under load -- _cleanup_request deleted
  future:{id} keys immediately, racing GlobalController's poll loop
  (every 5s), which reads those same keys to build OTel spans
  (telemetry_logging.pull_runtime_information). Whichever side lost
  the race silently dropped that request's span. Verified via a
  controlled before/after on the same image build: reverting just
  the reachability fix restored 7000/7000 spans landing, while
  keeping it landed only ~4900/7000. Switched future-key cleanup
  from redis.delete to redis.expire(30s), the same grace-period
  pattern deploy.py already uses for request:*:status/:result, so
  the poll loop has time to capture telemetry before the keys
  disappear. Affinity bindings are untouched since nothing else
  reads them.

Verified with both benchmark.py runs and unit tests: cleanup succeeds
with zero failures, future:*/affinity:*/request:*:futures keys no
longer accumulate (confirmed expiring rather than piling up), and
otel_counts_match is back to true with all 7000/7000 expected spans
landing, on the same 1000-request/concurrency-5 workload that
previously showed the leak and the span-loss regression.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 22, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 4b032ad6-30b0-4aa6-93ea-3a1453dab8c6


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved TTL and telemetry-polling issues, plus missing routing-endpoint regression coverage, block approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Fixes Redis cleanup RPC routing and prevents premature loss of telemetry data.

Changes:

  • Uses container-reachable cleanup endpoints.
  • Retains batches until all instances acknowledge receipt.
  • Replaces immediate deletion with grace-period expiration.
  • Updates cleanup tests for retry and TTL behavior.
File Summary Review notes
packages/​core/​tests/​test_local_controller_cleanup.py Tests TTL-based cleanup. No final comments.
packages/​core/​tests/​test_global_controller_cleanup.py Tests batching and retry retention. Moderate (1 vote): add distinct published and routing endpoint coverage.
packages/​core/​canyonos_core/​controller/​local_controller_frontend.py Applies expiration grace periods. Critical (2 votes): retries refresh TTL indefinitely. Moderate (3 votes): fixed TTL may expire before telemetry polling.
packages/​core/​canyonos_core/​controller/​global_controller.py Routes cleanup RPCs and handles batch retries. No final comments.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/core/canyonos_core/controller/local_controller_frontend.py Outdated
# Expiring instead of deleting keeps memory bounded (CAN-391's actual
# requirement) while giving the poll loop several chances to read the data
# first -- same pattern as deploy.py's COMPLETED_TTL_SECONDS.
FUTURE_CLEANUP_GRACE_SECONDS = 30

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a big issue since this would only happen if the polling_interval in global_controller.yaml files is >30 seconds, but maybe something to change, like config.get("poll_interval") * 5 or something, idk.

@Saaketh0 Saaketh0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line 839 of gc.py, discussed with Jayanth in person

jayanthchundru and others added 2 commits September 21, 2026 19:18
No behavior change -- verified with the existing unit tests (all 257
still pass) and a repeat 1000-request/concurrency-5 benchmark run,
which showed the same result as before the refactor: zero cleanup
failures, no leftover future:*/affinity:*/request:*:futures keys, and
otel_counts_match true with all 7000/7000 expected spans landing.

Replaces computing all_sent in two branches and checking it two lines
later with two early-return guard clauses (no instances -> return,
not all(...) -> return), so there's no intermediate boolean carrying
state between where it's computed and where it's checked.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… retry

FUTURE_CLEANUP_GRACE_SECONDS was hardcoded to 30s, so a configured
poll_interval >= 30s (or a slow poll cycle) could let the TTL expire
before GlobalController's poll loop ever read the future data, silently
dropping telemetry spans. The grace period now scales with the actual
CANYONOS_POLL_INTERVAL. RedisClient.expire() also gained an nx flag so
retried Cleanup RPCs (e.g. against an unreachable instance) don't keep
resetting the TTL on keys that already have one.

Also adds RoutingEndpointTests to test_global_controller_cleanup.py,
which give an instance's published and routing endpoints distinct
values so a regression back to instance["endpoint"] (the CAN-391 bug)
would actually fail the suite -- every existing fixture collapses the
two to the same value and can't catch that.
'
@jayanthchundru
jayanthchundru requested review from Saaketh0 and a lite review from Copilot September 22, 2026 19:45
@userAugustos userAugustos changed the title Fix Redis cleanup RPCs failing against unreachable ports (CAN-391) Fix: redis cleanup RPCs failing against unreachable ports (CAN-391) Sep 22, 2026

@Saaketh0 Saaketh0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

Comment thread packages/core/canyonos_core/controller/local_controller_frontend.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Address the three unresolved review findings before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity

Open (2)
Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Reloaded poll interval is not propagated to running agents

packages/​core/​canyonos_core/​controller/​local_controller_frontend.py:30

This grace period is computed once from the agent's launch-time environment, but GlobalController.reload_config() updates self.poll_interval in place without restarting existing agent containers. If SIGHUP changes the interval from 5s to 60s, those agents still use a 30s TTL and can expire future:{id} before the next telemetry poll, so the span-loss race remains. Propagate the reloaded interval to running agents or make the grace value come from state that reload updates.

Comment thread packages/core/tests/test_local_controller_cleanup.py Outdated
jayanthchundru and others added 3 commits September 22, 2026 12:58
Updated expire method to include 'nx' parameter for conditional expiration.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

@userAugustos userAugustos left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

On future though this should be an e2e test, unit test is shallow

@userAugustos userAugustos left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

On future though this should be an e2e test, unit test is shallow

@jayanthchundru
jayanthchundru merged commit a6664ab into main Sep 22, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants