Skip to content

[WIP] mooncake-operator#2837

Draft
liang09255 wants to merge 32 commits into
kvcache-ai:mainfrom
liang09255:liang09255
Draft

[WIP] mooncake-operator#2837
liang09255 wants to merge 32 commits into
kvcache-ai:mainfrom
liang09255:liang09255

Conversation

@liang09255

@liang09255 liang09255 commented Jul 10, 2026

Copy link
Copy Markdown

Description

Module

  • Transfer Engine (mooncake-transfer-engine)
  • Mooncake Store (mooncake-store)
  • Mooncake EP (mooncake-ep)
  • Mooncake PG (mooncake-pg)
  • Integration (mooncake-integration)
  • P2P Store (mooncake-p2p-store)
  • Python Wheel (mooncake-wheel)
  • Common (mooncake-common)
  • Mooncake RL (mooncake-rl)
  • CI/CD
  • Docs
  • Other

Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Breaking change
  • Documentation update
  • Performance improvement
  • Other

How Has This Been Tested?

Test commands:

# Example: bash scripts/run_ci_test.sh

Test results:

  • Unit tests pass
  • Integration tests pass (if applicable)
  • Manual testing done (describe below)

Checklist

  • I have performed a self-review of my own code
  • I have formatted my code using ./scripts/code_format.sh
  • I have run pre-commit run --all-files and all hooks pass
  • I have updated the documentation (if applicable)
  • I have added tests to prove my changes are effective
  • For changes >500 LOC: I have filed an RFC issue

AI Assistance Disclosure

  • No AI tools were used
  • AI tools were used (specify below)

root and others added 30 commits May 29, 2026 19:37
Switch HA backend from ETCD to K8S lease; add no-CUDA Dockerfile; skip
git submodules in CI Docker builds; add libk8s_lease_wrapper.so to wheel
packaging; add ASIO include dirs to transfer engine targets; add K8S lease
operator skeleton; update pybind11 and yalantinglibs submodules; improve
.dockerignore and .gitignore patterns.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…aults, fix segment display

- Add vLLM PD-disaggregated inference orchestration (proxy/prefill/decode deployments)
- Fix eviction config: use safe defaults (high_watermark=0.8, ratio=0.1) when CR values are 0/unset
- Fix MC_SEGMENT_SIZE from 0 to 1GB (1073741824) in test job env
- Fix null reference in worker-segments API handler
- Add store-read-verify-job.yaml for data verification
- Add Dockerfile.hostbuild for local pre-built UI images
- Add test page for UI segment verification
- Update .gitignore to track mooncake-operator/ui/lib/ files
- Update submodules (pybind11, yalantinglibs)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…to UI ClusterRole

Web UI editing of clusters (e.g. reducing worker count) failed with a 403
forbidden error because the mooncake-operator-ui ClusterRole only granted
get/list/watch/create/delete verbs but not update/patch. The edit page sends
a PUT request to the Kubernetes API which requires the update verb.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace nvidia/cuda base image with ubuntu:22.04 for both builder and
runtime stages. Disable CUDA/EP options, remove nvlink allocator build
step, use system Python 3.10 directly, and add GOPROXY for China mirror.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Remove all Chinese text from WebUI, replace with English equivalents
- Add dedicated migration page at /clusters/[ns]/[name]/migrate
  with source/target worker selection, config params, and real-time progress
- Add drain-worker targetIPs support to api-handler.js
- Fix tsconfig target to es2015 for Set iteration support
- Add operator controller and CRD changes for migration support
- Add mooncake-store drain/transfer enhancements and HA supervision

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…gment reassignment, and misc improvements

- mooncake-store: Add speed_mbps to drain job query response for real-time
  speed display; fix bandwidth throttling bug where rate check was skipped
  when elapsed_ms >= 1000, causing bursty throughput; calculate speed even
  when bandwidth is unlimited
- mooncake-store: Handle stale segment reassignment in MountSegment — when
  a worker pod restarts or a segment is DRAINED, allow remounting by
  reassigning to the new client instead of returning SEGMENT_ALREADY_EXISTS
- mooncake-store: Trigger segment remount after reconnecting to master in
  heartbeat thread (client_service.cpp)
- mooncake-transfer-engine: On rePublishRpcMetaEntry SET failure, verify via
  GET to tolerate duplicate-key metadata server implementations
- mooncake-operator CRD: Allow worker replicas to be 0 (scale-to-zero);
  add resource comparison to statefulSetSpecsEqual/deploymentSpecsEqual so
  resource changes trigger pod rollouts; reduce sample resource requests
- mooncake-operator RBAC: Add pod delete verb to UI ClusterRole
- mooncake-operator UI: Add Speed column to migration progress table;
  remove inline drain monitoring from cluster detail page (moved to dedicated
  migration page); add 5s auto-refresh with silent mode; add back-to-cluster
  links on edit page; add rimraf dev dependency

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add defensive JSON parsing in edit page to handle empty response bodies
- Ensure server.js always sends a JSON error body even when headers were already sent
- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ca scaling

- Bump operator replicas to 2 with pod anti-affinity for HA deployment
- Add /api/operator endpoint exposing operator pods and leader election status
- Add /api/operator/scale endpoint for dynamic replica adjustment (min=1)
- Display Operator HA Status section on Dashboard with pod table and Leader label
- Grant UI RBAC leases read + deployments patch permissions

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…implement auto-drain scale-down

- Store: Add YLT_REFL for ReplicateConfig to fix "invalid rpc arg" serialization
  bug (struct_pack missing group_ids field)
- Store: Remove drain success client eviction from ok_client_ to prevent DRAINED
  segment being reset to OK by remount race
- Store: Add --local_buffer_size flag to mooncake_client
- Operator: Implement auto-drain orchestration on worker scale-down with progress
  tracking in CRD status
- Operator: Add PreStop hook with dynamic leader master discovery for graceful
  worker termination
- Operator: Add dynamic leader discovery for metadata server registration
- UI: Display drain job progress on cluster detail page
- UI: Add standalone deployment manifest

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Clean up drain jobs where query fails (404 after master restart) and
  the pod no longer exists, instead of skipping cleanup
- Remove drain jobs stuck at CREATED for over 10 minutes as a safety net
  for orphaned entries

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ter image dropdown and defaults

- Operator: Force-delete worker pods after drain completes (status >= 3) to
  prevent pods stuck in Terminating when PreStop hook misses DRAINED signal
- Operator: Force-delete terminating pods whose segment is already DRAINED
- RBAC: Add pod delete/patch permission to operator manager ClusterRole
- PreStop: Re-discover master leader on each poll cycle to handle HA leader
  changes during drain
- UI: Add /api/images endpoint listing mooncake-store images from K8s nodes
- UI: Replace Image text input with live dropdown fetched from /api/images
- UI: Auto-generate random cluster name (adj-noun-xx) on Create page
- UI: Default RDMA to disabled, segment size to 2Gi on Create page
- RBAC: Add nodes get/list/watch permission to UI ClusterRole

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rain, and fast Dockerfile deps

- **Dual-leader fix**: MasterConfig reads `local_hostname` from JSON config;
  operator injects `POD_IP:50051` via patched.json sed; used as K8s Lease
  holderIdentity instead of the default "0.0.0.0:50051"
- **active_clients reset**: MasterMetricManager::reset_active_clients() clears
  stale client count on HA leader change (previously accumulated indefinitely)
- **Drain sequential scheduling**: keys_in_plan ensures same-key segments are
  migrated one at a time per batch, preventing duplicate Move tasks
- **Fast Dockerfile runtime deps**: Add missing apt libs (libyaml-cpp0.7,
  libjsoncpp25, libgflags2.2, libgoogle-glog0v5, libunwind-15); COPY
  libk8s_lease_wrapper.so and run ldconfig for HA mode
- **Operator Args sync**: Sync and compare container `Args` field in
  StatefulSet reconciliation to prevent unnecessary rollouts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add WHEEL_REUSE=1 fast mode to build_wheel.sh: unpack existing wheel,
  replace .so/.bin, repack — skips python -m build + auditwheel (~130s → ~10s)
- Fix 'python' → 'python3' in build_wheel.sh (externally-managed-env error)
- Fix 'kind load' to use --name three-node-cluster (no nodes found error)
- Make transfer_engine_bench copy optional (not built in all configs)
- Use --break-system-packages for pip in build_wheel.sh
- Auto-detect wheel reuse in dev-cycle.sh deploy-store
- Install ccache for faster C++ incremental compilation

Co-Authored-By: Claude <noreply@anthropic.com>
…h WebUI status

Transfer engine: MultiTransport now tracks per-transport health and automatically
falls back to TCP when RDMA fails consecutively. Configurable via
MC_FAILOVER_THRESHOLD and MC_FAILOVER_RECOVERY_SECS env vars.

Operator: New TransportFailover field in WorkerSpec configures failover per
cluster. Env vars MC_FAILOVER_ENABLED/THRESHOLD/RECOVERY_SECS are injected
into worker pods.

WebUI: Dashboard now shows RDMA Transport Status panel with per-worker health
badges and RDMA device allocation counts.

Mock RDMA transport (MC_RDMA_MOCK=1) enables testing without real RDMA
hardware. Fault injection via MC_RDMA_MOCK_FAIL_RATE/FAIL_AFTER.

Co-Authored-By: Claude <noreply@anthropic.com>
- api-handler.js: Dynamic MC_PROTOCOL from CR's rdmaEnabled, privileged
  + /dev/infiniband mount for test pods when RDMA enabled
- api-handler.js: rdma-status checks CR rdmaEnabled + Soft-RoCE hostPath
  mount instead of only device plugin resources
- page.tsx: Show "Soft-RoCE" badge + mode indicator for software RDMA
- resources.go: Worker pods get --protocol=rdma, hostPath, privileged
- CRD: transportFailover schema
- Dockerfile: Add ibverbs-providers for Soft-RoCE
- .gitignore: Add CMake build artifacts, version.h, wheel, binary

Co-Authored-By: Claude <noreply@anthropic.com>
- Add getTransportHealthMap() to MultiTransport/TransferEngine/Impl layer
- Add /transport_health HTTP endpoint in mooncake-store returning
  per-transport healthy/consecutive_failures/cooling_down JSON
- Enable HTTP server (--enable_http_server --http_port=9300) on workers
- Sync Volumes/Ports/SecurityContext/VolumeMounts in controller reconcile
- Add Ports/Volumes/SecurityContext/VolumeMounts equality checks
- Update WebUI to query /transport_health on port 9300 for real runtime
  health data instead of inferring from resource allocation

Co-Authored-By: Claude <noreply@anthropic.com>
… polling

- Add benchmark API endpoints (create, status, list, delete) to api-handler.js
- Add benchmark configuration & results page with PUT-only / Full Cycle workloads
- Add Benchmark button to cluster detail page
- Accelerate status detection: check pod container state directly instead of
  waiting for Job controller status update
- Increase frontend polling frequency: 3s→2s (Pending), 3s→1s (Running)
- Apply same polling improvements to existing Test page
…d error propagation

Three operator-layer fixes addressing data loss during pod drain migration:

1. waitDrainJobs: Track FAILED (4) and CANCELED (5) drain job statuses and
   return an error when any drain job completes with failures, instead of
   silently treating all terminal statuses as success.

2. Force-delete: Only force-delete terminating pods when drain job status
   is SUCCEEDED (3), not on FAILED or CANCELED. Previously used >= 3 which
   lost data by force-killing pods whose migration had not fully succeeded.

3. orchestrateDecodeScaleDown: Collect migration errors and return a
   composite error instead of swallowing them. Prevents the deployment
   from being patched with fewer replicas when KV cache migration fails.

Also fixes transfer engine cleanup ordering (destroy transports before
removing RPC metadata to prevent 404 cascades) and adds store.close()
to benchmark stress test scripts.

Co-Authored-By: Claude <noreply@anthropic.com>
…strateWorkerScaleDown

Problem: when scaling down worker replicas, orchestrateWorkerScaleDown created
drain jobs for the newest pods, but the Deployment patch (triggered immediately
after) caused K8s to terminate a DIFFERENT set of pods. The drained pods stayed
running with their segments stuck in DRAINING state on the master, and the
terminated pods were never drained. This left worker segments unable to accept
new writes despite the pod being healthy.

Fix: remove orchestrateWorkerScaleDown entirely. Instead, patch the Deployment
first (allowing K8s to naturally select pods for termination), and rely on
reconcileTerminatingWorkers to create drain jobs for the pods that K8s actually
terminates. The PreStop hook polls master's segment lifecycle for DRAINED status
while the operator manages drain job creation and force-deletion.

This eliminates the double-path conflict where two drain mechanisms competed
over different pod sets.

Co-Authored-By: Claude <noreply@anthropic.com>
C++ (mooncake-store):
- Fix SelectDrainTargetForKey to not fallback to existing replica segments
- Fix MoveEnd to reject empty replica_ids, preventing replica count loss
- Add POST /api/v1/objects/remove endpoint for batch key removal

UI (mooncake-operator):
- Add Terminating status badge when pod has deletionTimestamp
- Disable Delete button for terminating pods
- Shorten worker names to worker-1, worker-2, worker-3 format
- Add Clear Data button on worker detail page
- Add /clear-worker-data API endpoint with segment-key filtering
- Fix worker-keys API to return all matching keys per batch

Dev-cycle:
- Fix kind load missing --name flag for deploy-ui and deploy-operator
- Copy mooncake_master.bin from build output before Docker build
- Restart all statefulsets/deployments directly by name (fix label mismatch)

Co-Authored-By: Claude <noreply@anthropic.com>
…test pod from mounting segment

Three-layer fix for multi-replica (replica_num>1) data integrity:

1. Allocation strategy (allocation_strategy.h):
   - Increase max_retry from names.size() to replica_num * names.size()
   - Fix used_segments tracking after each successful allocation

2. Validation layer (master_service.cpp, client_service.cpp):
   - HasExpectedReplicaAllocation now requires strict exact-match for
     RELIABLE_MULTI_REPLICA mode, rejecting partial allocations
   - Prevents silent data loss: if system can't satisfy requested replica
     count, put fails with NO_AVAILABLE_HANDLE instead of silently accepting

3. UI test pod (api-handler.js):
   - Set MC_SEGMENT_SIZE=0 so test pod runs as pure client without
     mounting a segment, preventing replicas from being placed on a
     temporary segment that disappears when the test completes

Co-Authored-By: Claude <noreply@anthropic.com>
…ULLY_UNMOUNTING segment on drain fail, fix pre-existing lease test failures

Fix 1 (controller): Force-delete pod with GracePeriodSeconds(1) when drain
job reaches max retries, instead of silently abandoning the pod to wait
600s PreStop timeout.

Fix 2 (master_service): Set segment to GRACEFULLY_UNMOUNTING instead of OK
when drain job fails. This prevents the allocator from reassigning the
dying segment for new writes.

Test fixes: Updated 4 pre-existing test failures caused by PutEnd now
granting default_kv_lease_ttl leases. Fixed GroupedEviction, RemoveAllLeasedObject,
RemoveSoftPinObject (TTL adjustments), and MoveStart (MoveEnd now correctly
refuses to delete source when target already has replica).

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Resolved 16 merge conflicts:
- .gitignore, docker/mooncake.Dockerfile: merged both sides
- mooncake-store CMakeLists/build: kept operator build infra + upstream options
- mooncake-store master_service: kept operator drain/recovery logic
- mooncake-store rpc_service: kept operator metric reporting
- mooncake-store master_service_test: kept operator-specific tests
- mooncake-store http_metadata_server, real_client_main: took upstream improvements
- mooncake-transfer-engine: merged TransportHealthMap + device transport accessors
- build_wheel.sh, transport_selector_test: took upstream improvements

Co-Authored-By: Claude <noreply@anthropic.com>
…UDA Docker build

- Restore drain bandwidth tracking and speed_mbps fields
- Restore RELIABLE_MULTI_REPLICA fix, remount after reconnect
- Restore TransportHealth failover tracking and mock RDMA transport
- Restore stale segment reassignment and allocation strategy fix
- Include non-CUDA Dockerfile and CMake build configuration

Co-Authored-By: Claude <noreply@anthropic.com>
…ping operator features

- Reset master_service.cpp to mooncake/main base (removes incompatible operator code)
- Reset rpc_service.cpp to mooncake/main base (fixes HTTP handler compilation)
- Remove incompatible ReplicationTask custom constructor from master_service.h
- Retain operator-specific additions: speed_mbps, bandwidth_mbps, blocked_unit_keys
- Fix serializer.hpp → serializer.h include

Co-Authored-By: Claude <noreply@anthropic.com>
…vice

Co-Authored-By: Claude <noreply@anthropic.com>
…uilds

- Split COPY into two stages: dependencies (cached) then source (incremental)
- Add ccache with -DCMAKE_C_COMPILER_LAUNCHER + BuildKit cache mount
- Exclude stale build/ directories from Docker context
- Add --no-deps to pip install to avoid PyPI network timeout
- Incremental build: ~130s (was ~400s)

Co-Authored-By: Claude <noreply@anthropic.com>
root and others added 2 commits July 10, 2026 09:54
Remove nohup.out (log file) and docker-pip-cache/*.whl (Python wheel
binaries, 47MB total) from git tracking. These files are already matched
by .gitignore patterns and should never have been committed.

Co-Authored-By: Claude <noreply@anthropic.com>
This is a build artifact (ELF 64-bit, 8.9MB) already listed in .gitignore.

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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a Next.js Web UI and a Kubernetes Operator (mooncake-operator) to manage Mooncake clusters, alongside vLLM live-migration support and C++ transfer engine enhancements like mock RDMA and automatic RDMA-to-TCP failover. The review feedback is highly actionable and identifies several critical issues: a bug in the operator controller attempting to update a deleted ConfigMap, a body stream reuse error in the benchmark page, missing read timeouts in the proxy server, unhandled non-2xx HTTP status codes in the UI API handler, a failover threshold logic bug in the C++ multi-transport, an accidentally committed backup file, an in-memory session memory leak in the UI server, and missing query parameter stripping in the custom Python HTTP server.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +239 to +247
if !configMapDataEqual(created.Data, desired.Data) {
logger.Info("created ConfigMap has wrong data, deleting and retrying with Update")
logger.Info("desired data", "data", desired.Data)
logger.Info("created data", "data", created.Data)
_ = r.Delete(ctx, &created)
existing = *desired
existing.ResourceVersion = ""
return r.Update(ctx, &existing)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

In reconcileConfigMap, if the created ConfigMap has different data than desired, the code deletes the ConfigMap and then immediately calls r.Update on it. Since the resource has been deleted from the API server, r.Update will fail with a NotFound error. Instead of deleting and updating, you should directly update the existing ConfigMap's data and call r.Update.

		if !configMapDataEqual(created.Data, desired.Data) {
			logger.Info("created ConfigMap has wrong data, updating")
			created.Data = desired.Data
			return r.Update(ctx, &created)
		}

Comment on lines +77 to +84
let data
try {
data = await res.json()
} catch (parseErr: any) {
// Empty body or invalid JSON — try to get the raw text for debugging
const text = await res.text().catch(() => '')
throw new Error(`Invalid JSON response (${res.status}): ${parseErr.message}. Raw body (${text.length} bytes): ${text.substring(0, 200)}`)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

In the Fetch API, the response body stream can only be read once. Calling res.text() in the catch block of res.json() will throw a TypeError: body stream already read because the stream has already been consumed by res.json(). To safely log or handle non-JSON error responses, you should read the response as text first, and then parse it as JSON.

      let data
      const text = await res.text()
      try {
        data = JSON.parse(text)
      } catch (parseErr: any) {
        throw new Error(`Invalid JSON response (${res.status}): ${parseErr.message}. Raw body (${text.length} bytes): ${text.substring(0, 200)}`)
      }

Comment on lines +345 to +381
async def _proxy_http_request(
host: str, port: int, method: str, path: str,
body: dict | None = None, timeout: float = 10.0,
) -> dict:
"""Make an HTTP request to a migration endpoint."""
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(host, port), timeout=5.0
)
body_bytes = json.dumps(body).encode() if body else b""
request_line = f"{method} {path} HTTP/1.1\r\n".encode()
headers = (
f"Host: {host}:{port}\r\n"
f"Content-Length: {len(body_bytes)}\r\n"
f"Content-Type: application/json\r\n"
f"Connection: close\r\n\r\n"
).encode()
writer.write(request_line + headers + body_bytes)
await writer.drain()

while True:
line = await reader.readuntil(b"\r\n")
if line == b"\r\n":
break
resp_body = b""
while True:
chunk = await reader.read(4096)
if not chunk:
break
resp_body += chunk
writer.close()
await writer.wait_closed()
return json.loads(resp_body) if resp_body else {}
except Exception as e:
logger.error("Proxy HTTP request to %s:%d%s failed: %s",
host, port, path, e)
return {"error": str(e)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The _proxy_http_request function defines a timeout parameter but only applies a 5-second timeout to the connection phase. The subsequent read loop has no timeout, which can cause the proxy server to hang indefinitely if the target node is unresponsive. You should wrap the entire request execution (or at least the read loop) with asyncio.wait_for using the provided timeout parameter.

async def _proxy_http_request(
    host: str, port: int, method: str, path: str,
    body: dict | None = None, timeout: float = 10.0,
) -> dict:
    """Make an HTTP request to a migration endpoint."""
    try:
        async def _run():
            reader, writer = await asyncio.open_connection(host, port)
            body_bytes = json.dumps(body).encode() if body else b""
            request_line = f"{method} {path} HTTP/1.1\r\n".encode()
            headers = (
                f"Host: {host}:{port}\r\n"
                f"Content-Length: {len(body_bytes)}\r\n"
                f"Content-Type: application/json\r\n"
                f"Connection: close\r\n\r\n"
            ).encode()
            writer.write(request_line + headers + body_bytes)
            await writer.drain()

            while True:
                line = await reader.readuntil(b"\r\n")
                if line == b"\r\n":
                    break
            resp_body = b""
            while True:
                chunk = await reader.read(4096)
                if not chunk:
                    break
                resp_body += chunk
            writer.close()
            await writer.wait_closed()
            return json.loads(resp_body) if resp_body else {}

        return await asyncio.wait_for(_run(), timeout=timeout)
    except Exception as e:
        logger.error("Proxy HTTP request to %s:%d%s failed: %s",
                     host, port, path, e)
        return {"error": str(e)}

Comment on lines +189 to +205
std::lock_guard<std::mutex> lock(health_mutex_);
auto it = transport_health_.find(proto_str);
if (it != transport_health_.end()) {
it->second.consecutive_failures++;
if (it->second.consecutive_failures >= failover_threshold_) {
it->second.markUnhealthy(recovery_secs_);
LOG(WARNING) << "[MultiTransport] " << proto_str
<< " marked unhealthy after "
<< it->second.consecutive_failures
<< " consecutive failures. "
<< "Cooldown for " << recovery_secs_ << "s";
}
} else {
TransportHealth health;
health.consecutive_failures = 1;
transport_health_[proto_str] = health;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

If a transport fails and its entry does not exist in transport_health_ yet, it is initialized with consecutive_failures = 1 in the else branch, but the >= failover_threshold_ check is skipped. This means that if failover_threshold_ is configured to 1, the first failure will not trigger failover, requiring a second failure instead. You can simplify and fix this by initializing the entry first or checking the threshold in both cases.

                std::lock_guard<std::mutex> lock(health_mutex_);
                auto &health = transport_health_[proto_str];
                health.consecutive_failures++;
                if (health.consecutive_failures >= failover_threshold_) {
                    health.markUnhealthy(recovery_secs_);
                    LOG(WARNING) << "[MultiTransport] " << proto_str
                                << " marked unhealthy after "
                                << health.consecutive_failures
                                << " consecutive failures. "
                                << "Cooldown for " << recovery_secs_ << "s";
                }

@@ -0,0 +1,893 @@
package controller

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

A backup file resources.go.bak has been accidentally committed to the repository. Please remove this file to keep the repository clean and maintainable.

Comment on lines +21 to +23
const SESSION_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
const sessions = new Map() // token -> { createdAt }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Sessions are stored in an in-memory Map but are never periodically pruned. Expired sessions are only deleted when a user attempts to use them. If users log in and never return, their sessions will leak memory indefinitely. You should add a periodic setInterval to prune expired sessions.

// --- Session management ---
const SESSION_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
const sessions = new Map() // token -> { createdAt }

// Periodically prune expired sessions to prevent memory leaks
setInterval(() => {
  const now = Date.now()
  for (const [token, entry] of sessions.entries()) {
    if (now - entry.createdAt > SESSION_TTL_MS) {
      sessions.delete(token)
    }
  }
}, 60 * 60 * 1000)

Comment on lines +1285 to +1295
options.headers['Content-Length'] = Buffer.byteLength(bodyStr)
}
const req = http.request(options, (res) => {
let data = ''
res.on('data', (chunk) => data += chunk)
res.on('end', () => {
try {
resolve(JSON.parse(data))
} catch (e) {
resolve(data)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The callMasterAPI helper does not check the HTTP response status code. If the master API returns a non-200 error (e.g., 500 or 404), the function will still resolve successfully with the error body, which can cause unexpected behavior or crashes in the caller. You should reject the promise if the status code is not in the 2xx range.

    const req = http.request(options, (res) => {
      let data = ''
      res.on('data', (chunk) => data += chunk)
      res.on('end', () => {
        if (res.statusCode < 200 || res.statusCode >= 300) {
          reject(new Error(`Master API returned status ${res.statusCode}: ${data}`));
          return;
        }
        try {
          resolve(JSON.parse(data))
        } catch (e) {
          resolve(data)
        }
      })
    })

Comment on lines +890 to +891
method, path, _ = request_line.decode().strip().split(" ", 2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The custom HTTP server matches paths using exact matching or path.startswith, but does not strip query parameters. If a query parameter is appended (e.g., by a monitoring tool or proxy), the route matching will fail. You should strip query parameters from the path before routing.

Suggested change
method, path, _ = request_line.decode().strip().split(" ", 2)
request_line = await reader.readuntil(b"\r\n")
method, path, _ = request_line.decode().strip().split(" ", 2)
if "?" in path:
path, _ = path.split("?", 1)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant