Skip to content

[Router] vectorstore: add lifecycle root context and bounded Stop(ctx)#2517

Open
Peterren wants to merge 4 commits into
vllm-project:mainfrom
Peterren:feat/vectorstore-lifecycle-ctx
Open

[Router] vectorstore: add lifecycle root context and bounded Stop(ctx)#2517
Peterren wants to merge 4 commits into
vllm-project:mainfrom
Peterren:feat/vectorstore-lifecycle-ctx

Conversation

@Peterren

@Peterren Peterren commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Purpose

  • Give the vector-store ingestion pipeline a cancellable lifecycle root context and replace the unbounded Stop() with a bounded Stop(ctx) error.
  • Today processJob runs with a hardcoded context.Background() and Stop() waits on an unbounded wg.Wait(). If an embedder or backend wedges inside a stage, Stop() blocks forever and the process cannot shut down. This is the first, foundational slice of the larger vector-store hardening effort (vectorstore: make ingestion cancellable, bounded, and transactional #2474): make ingestion cancellable and shutdown bounded.
  • Module: Router (plus E2E-style integration tests within the vectorstore package).

What changed

  • IngestionPipeline gains a lifecycle root context created in Start() and cancelled on shutdown. Workers and processJob derive their work from it.
  • processJob checks ctx.Err() between stages (read → chunk → per-chunk embed → insert), so a cancelled lifecycle aborts in-flight jobs at the next checkpoint instead of running to completion. Cancelled jobs are marked failed with a cancelled error code; count/metadata updates use context.WithoutCancel so status stays consistent even under cancellation.
  • Stop()Stop(ctx context.Context) error: stops accepting jobs, fails queued jobs, then drains in-flight work bounded by ctx. If ctx elapses first, the root context is cancelled and Stop returns ctx.Err() without blocking further. A nil ctx uses a default 30s bound. Stop remains idempotent.
  • VectorStoreRuntime.Shutdown() now bounds pipeline drain with a 30s deadline and logs if the drain doesn't complete, so a wedged backend cannot hang process shutdown.

This is intentionally scoped as a standalone, foundational slice. It changes the Stop() signature to Stop(ctx context.Context) error (a source-level change for direct callers, all of which are updated here). Making individual stages (embedder/backend) natively ctx-aware is a deliberate follow-up; Stop's own contract (return within ctx) holds regardless.

Test Plan

cd src/semantic-router && go test -race ./pkg/vectorstore ./pkg/routerruntime

New tests:

  • Bounded Stop (pipeline_lifecycle_test.go): with a wedged embedder, Stop(ctx) returns context.DeadlineExceeded promptly (asserted < 3s) rather than hanging; Stop is idempotent and a second call returns nil.
  • Graceful drain + no goroutine leak (integration_test.go): a full ingest-to-search run, then Stop(context.Background()) returns nil, worker goroutines are reclaimed (goroutine count returns to baseline), and completed vectors remain searchable after shutdown.
  • Bounded stop under wedge, then clean unwind (integration_test.go): Stop(ctx) honors the deadline while a worker is parked inside Embed, and after the embedder is released no goroutines remain.

This validates the two behaviors the change targets — cancellability and bounded shutdown — under both the graceful and the wedged path, with -race and goroutine-leak assertions.

Test Result

  • go test -race ./pkg/vectorstore ./pkg/routerruntime passes; race detector clean.
  • Bounded-Stop and graceful-drain/leak tests ran repeatedly (-count=1 x3) with no flakiness.
  • Follow-up (not in this PR, tracked under vectorstore: make ingestion cancellable, bounded, and transactional #2474): make embedder/backend stages natively ctx-aware so a job wedged inside a single stage can be interrupted before the stage returns; streaming/bounded batches; status retention; transactional reconciliation.

  • PR title uses module-aligned prefix [Router]
  • Commits signed off with git commit -s
  • Purpose / Test Plan / Test Result reflect the actual scope, commands, and follow-ups

Closes #2602

Part of the umbrella #2474.

@Peterren
Peterren requested review from Xunzhuo and rootfs as code owners July 13, 2026 21:12
@netlify

netlify Bot commented Jul 13, 2026

Copy link
Copy Markdown

Deploy Preview for vllm-semantic-router ready!

Name Link
🔨 Latest commit f84a01f
🔍 Latest deploy log https://app.netlify.com/projects/vllm-semantic-router/deploys/6a5e3fa3a422bf0008bb45dd
😎 Deploy Preview https://deploy-preview-2517--vllm-semantic-router.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

👥 vLLM Semantic Team Notification

The following members have been identified for the changed files in this PR and have been automatically assigned when their GitHub accounts are assignable in this repository:

📁 config

Owners: @FAUST-BENCHOU, @shraderdm, @drivebyer, @ramkrishs, @WUKUNTAI-0211, @AayushSaini101, @siloteemu
Files changed:

  • config/config.yaml

📁 dashboard

Owners: @FAUST-BENCHOU, @shraderdm, @drivebyer, @ramkrishs, @WUKUNTAI-0211, @AayushSaini101, @siloteemu
Files changed:

  • dashboard/frontend/src/pages/configPageRouterDefaultsCatalog.ts
  • dashboard/frontend/src/pages/configPageRouterDefaultsSupport.ts
  • dashboard/frontend/src/pages/configPageSupport.ts

📁 deploy

Owners: @FAUST-BENCHOU, @shraderdm, @drivebyer, @ramkrishs, @WUKUNTAI-0211, @AayushSaini101, @siloteemu
Files changed:

  • deploy/helm/semantic-router/values.yaml

📁 e2e

Owners: @FAUST-BENCHOU, @shraderdm, @drivebyer, @ramkrishs, @WUKUNTAI-0211, @AayushSaini101, @siloteemu
Files changed:

  • e2e/profiles/rag-hybrid-search/values.yaml

📁 src/semantic-router

Owners: @FAUST-BENCHOU, @shraderdm, @drivebyer, @ramkrishs, @WUKUNTAI-0211, @AayushSaini101, @siloteemu
Files changed:

  • src/semantic-router/pkg/config/vectorstore.go
  • src/semantic-router/pkg/routerruntime/vectorstore_runtime.go
  • src/semantic-router/pkg/vectorstore/integration_test.go
  • src/semantic-router/pkg/vectorstore/pipeline.go
  • src/semantic-router/pkg/vectorstore/pipeline_lifecycle_test.go
  • src/semantic-router/pkg/vectorstore/pipeline_test.go

vLLM Semantic Router

🎉 Thanks for your contributions!

This comment was automatically generated based on the OWNER files in the repository.

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

✅ Supply Chain Security Report — All Clear

Scanner Status Findings
AST Codebase Scan (Py, Go, JS/TS, Rust) 29 finding(s) — MEDIUM: 22 · LOW: 7
AST PR Diff Scan No issues detected
Regex Fallback Scan No issues detected

Scanned at 2026-07-20T15:36:01.947Z · View full workflow logs

@Peterren

Copy link
Copy Markdown
Contributor Author

Good catch — you're right, 30s was a made-up constant. I've made it a config param.

To be precise about what it controls: it's not a per-file ingest budget (a single job can take much longer or shorter). It's the graceful-shutdown drain window — on shutdown the pipeline stops accepting new jobs, waits up to this long for currently-running jobs to finish, then cancels the rest so a wedged embedder/backend can't block process exit.

That means the right value is deployment-specific, bounded on both sides:

  • lower bound: comfortably above typical single-job latency (file size × chunk count × per-chunk embed + backend insert), so normal in-flight work drains cleanly on rollout/restart;
  • upper bound: below the platform's shutdown grace period — e.g. Kubernetes terminationGracePeriodSeconds (default 30s) — otherwise the platform SIGKILLs before the drain can run, defeating the purpose.

Now exposed as vector_store.ingestion_drain_timeout_seconds (default 30), sourced by the runtime, with a bounded fallback so Stop is never unbounded even if a runtime is hand-constructed without config. Added the key to the reference config, Helm values, and the RAG e2e profile. Pushed in the follow-up commit.

@Xunzhuo Xunzhuo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A lifecycle root and deadline-aware Stop(ctx) are the right foundation, but the current implementation still has work outside the deadline and leaves ownership races after a timeout. Please address the four inline lifecycle blockers. Also keep the new config contract in sync with the dashboard/config surface, and either set the default below the Kubernetes termination grace period or configure a larger pod grace period; both are currently 30s, leaving no cleanup margin. CI is otherwise green, but check-linked-issue is failing and the branch is behind main. make agent-report and git diff --check passed; local race tests were blocked by unavailable native Candle dylibs.

Comment thread src/semantic-router/pkg/vectorstore/pipeline.go Outdated
Comment thread src/semantic-router/pkg/vectorstore/pipeline.go Outdated
Comment thread src/semantic-router/pkg/routerruntime/vectorstore_runtime.go
Comment thread src/semantic-router/pkg/vectorstore/pipeline.go Outdated
Comment thread src/semantic-router/pkg/vectorstore/pipeline.go Outdated
@Peterren

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review — all four lifecycle blockers are addressed in d7b28da, replies inline.

Summary:

  • Per-generation resources (queue/stopCh/WaitGroup/root ctx) + tri-state lifecycle (stopped/running/stopping); a timed-out Stop stays stopping and a second Stop retries the join instead of returning nil.
  • failQueuedJobs and AttachFile no longer hold lifecycleMu across registry I/O; all cleanup persistence is bounded.
  • Shutdown closes registry/backend only once workers are quiescent, else leaves them open and returns the error.
  • Completed-path status/count committed coherently with a detached bounded context.

Also: synced the dashboard config surface with ingestion_drain_timeout_seconds, and lowered the default from 30s → 25s so it sits below the 30s Kubernetes default grace period with cleanup margin.

New regression tests cover timeout-then-restart, second-Stop honesty, and attach racing a restart; go test -race ./pkg/vectorstore ./pkg/routerruntime ./pkg/config is clean.

@github-actions github-actions Bot deleted a comment from codecov-commenter Jul 16, 2026
@Peterren
Peterren force-pushed the feat/vectorstore-lifecycle-ctx branch from d7b28da to bbd5823 Compare July 19, 2026 02:55
@Peterren

Copy link
Copy Markdown
Contributor Author

@Xunzhuo this is ready for another look. Since your review:

  • All 5 lifecycle P1s fixed (bbd5823a, previously d7b28da6): bounded queue-drain under Stop(ctx), per-generation queue/WaitGroup/root context, timeout-safe Shutdown ownership, coherent completed-status/count commit under a detached bounded cleanup context, and a tri-state stopped/running/stopping lifecycle so a second Stop after timeout no longer falsely reports success. Each is answered inline on the original threads with a timeout-then-restart and second-Stop-after-timeout test.
  • check-linked-issue now passes — added Closes #2602, a dedicated slice issue carved from the vectorstore: make ingestion cancellable, bounded, and transactional #2474 acceptance list (lifecycle root + bounded/idempotent Stop(ctx)), rather than prematurely closing the umbrella.
  • Removed "backward-compatible" from the description — Stop()Stop(ctx) error is a source-level signature change (all direct callers are updated here).
  • Config↔surface sync and grace-period margin: the drain default is 25s, below the 30s pod termination grace period, and ingestion_drain_timeout_seconds is present across config/config.yaml, both helm values.yaml blocks, and the dashboard config support/catalog.

Rebased onto latest main (was 36 behind). CI is green except the local-only Candle race tests, which link against native dylibs unavailable locally — test-and-build exercises those in CI.

@github-actions github-actions Bot deleted a comment from codecov-commenter Jul 19, 2026
@github-actions github-actions Bot deleted a comment from codecov-commenter Jul 19, 2026
@drivebyer

Copy link
Copy Markdown
Collaborator

Pushed 6202207 to fix a small classification gap this PR introduces.

processJob now passes the cancellable lifecycle root ctx into backend.InsertChunks (it was context.Background() before). So when a bounded Stop times out and cancels the root, a ctx-honoring backend like Milvus returns context.Canceled from an in-flight insert. That error was recorded as storage_error, which makes a normal shutdown look like a backend fault in status/metrics. The inter-stage checkpoints already record this case as cancelled, so the insert path was the one inconsistent spot.

The commit extracts a storeChunks helper that records the error as cancelled when the job ctx itself is cancelled and the error is context.Canceled/DeadlineExceeded. The ctx.Err() guard keeps a backend-internal timeout (job ctx still live) classified as a real storage_error.

Test: a new ctxHonoringBackend parks InsertChunks until its ctx is cancelled, modelling an insert interrupted by shutdown. The spec asserts a bounded Stop leaves the file failed with LastError.Code == "cancelled", not storage_error. go test -race ./pkg/vectorstore ./pkg/routerruntime passes, and the new spec is stable across repeated runs.

@github-actions github-actions Bot deleted a comment from codecov-commenter Jul 19, 2026
drivebyer
drivebyer previously approved these changes Jul 19, 2026
FAUST-BENCHOU
FAUST-BENCHOU previously approved these changes Jul 19, 2026
@drivebyer
drivebyer requested a review from Xunzhuo July 19, 2026 08:59
The ingestion pipeline processed each job with a hardcoded
context.Background() and shut down via an unbounded wg.Wait(). If an
embedder or backend wedged inside a stage, Stop() blocked forever and the
process could not be shut down.

Introduce a pipeline lifecycle root context created in Start() and
cancelled on shutdown. processJob now derives its work from that context
and checks cancellation between stages (read, chunk, per-chunk embed,
insert), so a cancelled lifecycle aborts in-flight jobs at the next
checkpoint instead of running to completion.

Stop() becomes Stop(ctx) error: it stops accepting jobs, fails queued
jobs, and drains in-flight work bounded by ctx. If ctx elapses before the
drain finishes, the root context is cancelled and Stop returns ctx.Err()
without blocking further. A nil ctx uses a default 30s bound. The runtime
Shutdown path now bounds pipeline drain with a 30s deadline so a wedged
backend cannot hang process shutdown.

Add lifecycle and integration tests covering: bounded Stop returning
within its deadline when a job is wedged, Stop idempotency, graceful
drain with no leaked worker goroutines, and post-shutdown searchability.

Part of vllm-project#2474

Signed-off-by: Yincheng Ren <27608815+Peterren@users.noreply.github.com>
Peterren added 3 commits July 20, 2026 11:31
Address review: the shutdown drain bound was a hardcoded 30s. It is not a
per-file ingest budget but a graceful-shutdown grace window (stop accepting
jobs, wait for in-flight jobs to drain, then cancel), so it must be sized
per deployment — above typical single-job latency but below the platform's
shutdown grace period (e.g. Kubernetes terminationGracePeriodSeconds).

Bubble it up as vector_store.ingestion_drain_timeout_seconds (default 30),
sourced by VectorStoreRuntime; keep a bounded default fallback so Stop is
never unbounded when constructed without config. Add the key to the
reference config, Helm values, and the RAG e2e profile.

Also check the error return of Stop(ctx) at all test call sites (errcheck),
which the pre-commit go-lint hook flagged after Stop's signature changed.

Part of vllm-project#2474

Signed-off-by: Yincheng Ren <27608815+Peterren@users.noreply.github.com>
processJob exceeded the agent lint funlen limit (41 > 40 statements).
Extract the per-chunk embedding loop (Step 5) into embedChunks, which
preserves the same per-chunk ctx cancellation checks and error handling
and returns ok=false to signal the caller to stop. No behavior change.

Signed-off-by: Yincheng Ren <27608815+Peterren@users.noreply.github.com>
…start

Address review feedback on the bounded Stop(ctx) work. The prior version
still had unbounded I/O outside the deadline and left ownership races after
a timed-out stop.

- Per-generation resources: each Start creates a generation with its own
  jobQueue, stopCh, WaitGroup, and root context. A timed-out Stop can never
  race a subsequent Start into reusing a live generation's WaitGroup/queue,
  and an old worker can no longer steal a new-generation job.
- Tri-state lifecycle (stopped/running/stopping): after a timed-out drain the
  generation stays stopping; a second Stop waits on the same generation
  instead of falsely returning nil, so callers can retry the join.
- failQueuedJobs is bounded by the Stop ctx and no longer runs unbounded
  persistence before the drain wait; it always sets the in-memory failed
  status so no queued job is left stuck in_progress.
- AttachFile no longer holds the lifecycle lock across registry I/O: the count
  is reserved with a bounded context before the brief enqueue critical
  section, with a compensation path when the pipeline is not running.
- Start no longer holds the lifecycle lock while waiting for a stopping
  generation to join, so a concurrent attach/stop is never blocked behind a
  restart parked on a wedged generation.
- processJob commits the completed status and count coherently with a bounded,
  cancellation-detached context and surfaces persistence failures.
- Shutdown only closes the registry and backend once workers are quiescent
  (Stop returned nil); on timeout it leaves them open to avoid use-after-close
  and returns the error.
- Lower the default ingestion drain timeout from 30s to 25s so it sits below
  the 30s Kubernetes default grace period, leaving cleanup margin. Sync the
  dashboard config surface and helm/config defaults.

Adds regression tests for timeout-then-restart, second-Stop honesty, and
attach racing a restart. go test -race is clean for pkg/vectorstore,
pkg/routerruntime, and pkg/config.

Signed-off-by: Yincheng Ren <27608815+Peterren@users.noreply.github.com>
@Peterren
Peterren dismissed stale reviews from FAUST-BENCHOU and drivebyer via f84a01f July 20, 2026 15:32
@Peterren
Peterren force-pushed the feat/vectorstore-lifecycle-ctx branch from 6202207 to f84a01f Compare July 20, 2026 15:32
@Peterren

Copy link
Copy Markdown
Contributor Author

Quick status: this now has approvals from @drivebyer and @FAUST-BENCHOU, CI is green (test-and-build + pre-commit), and I've rebased onto latest main. The child #2535 (embedder-context, stacked on this) is also approved and green.

@Xunzhuo whenever you get a chance — since all five of your lifecycle P1s are fixed and answered inline, could you re-review here so your earlier CHANGES_REQUESTED can be cleared? That's the only thing gating the merge of this slice (and the #2535 stack on top).

@github-actions github-actions Bot deleted a comment from codecov-commenter Jul 20, 2026
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.

vectorstore: lifecycle root context + bounded, idempotent Stop(ctx)

8 participants