Skip to content

feat(indexer): drain indexing pipelines on planned teardowns - #3

Draft
Totodore wants to merge 40 commits into
mainfrom
feat-pipeline-drain
Draft

Totodore wants to merge 40 commits into
mainfrom
feat-pipeline-drain

Conversation

@Totodore

@Totodore Totodore commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

What

Adds a generic pipeline drain mechanism so planned teardowns (plan-driven pipeline shutdowns, node shutdown) publish and settle in-flight batches instead of dropping them.

Why

Some sources dont support partitionned consumers or dont support checkpoints-based consumers. These sources need a way to acknowledge the incoming messages once they are indexed even when shutting-down a pipeline.
Without a graceful source drain, these ack-only sources are subjects to at-leat-once delivery whereas they could be in the exactly-once delivery scenario (except in case in of crashes).

Our specific nats case

We want to implement a Nats source. Nats does not support partitionned consumers, so there is two ways to make it work:

  • Use a Nats Ordered consumer with an AckPolicy set to "None" and use the quickwit Checkpoint mechanism. However this does not allow for pipeline scaling (because without named partitions we cant key checkpoints per-pipelines). Scaling is required for our nats implementation.
  • Skip the checkpoint mechanism and ack messages in suggest_truncate, with this solution it is possible to scale the number of pipelines as Nats dynamically spread load on every consumer instance (each pipelines). The tweak is that we must be able to keep the exactly-once delivery semantic in case of pipeline teardown, hence the drain mechanism.

Other source could benefit from that, the GCP pubsub source has a lot of todo related to these issues.

Design

Mirrors the FinishPendingMergesAndShutdownPipeline pattern: everything stays inside the actor system, no detached tasks, no caller-side timeouts.

  • Opt-in gate: sources opt into draining through Source::should_be_drained() (default false), captured once at spawn. Pipelines whose source does not opt in are torn down by the DrainPipeline handler exactly as a kill would (terminate the child actors, exit) — sources other than the future acknowledgment-based ones are guaranteed to keep their pre-existing teardown semantics.
  • Drain (SourceActor): stops emit_batches, pushes an empty force-commit batch so the indexer flushes its workbench, and the source exits with success on its own once everything it delivered is settled (Source::is_drained).
  • DrainPipeline (IndexingPipeline / MetricsPipeline): fire-and-forget initiation. The shared DrainState records the pipeline's drain deadline (own commit timeout + 30s grace) and guards against respawn; the pipeline's existing supervise loop enforces the deadline, and the source's success exit cascades until the pipeline exits with success by itself.
  • IndexingService: shutdown_pipelines sends DrainPipeline to every detached pipeline without awaiting; the supervise loop reaps the handles by actor state. Merge pipelines survive their draining indexing pipelines so the final force-committed splits still reach the merge planner (immature-split pickup is node-scoped).
  • DrainAllPipelines (node shutdown, before universe.quit()): deferred reply completed by the supervise loop once every draining pipeline — including ones a plan change already detached — has exited. Ordered after the ingester decommission, which needs pipelines still consuming, and bounded by the new indexer.shutdown_drain_timeout config (QW_INDEXER_SHUTDOWN_DRAIN_TIMEOUT, 300s default) so shutdown stays within the deployment's grace period.
  • Indexer: an empty batch with no open workbench is dropped instead of creating one just to commit it empty.

Tests

  • test_source_actor_drain: a source without ack state exits immediately on drain, after emitting the flush batch.
  • Full indexing_service + indexing_pipeline suites green (non-opt-in sources keep plain kill semantics).
  • End-to-end drain coverage lands with the first ack-based source (stacked PR feat(indexers): add a scalable NATS JetStream source #4).

🤖 Generated with Claude Code

https://claude.ai/code/session_01QHjEoDvmLtHNpEUbkUY56D

dependabot Bot and others added 17 commits August 26, 2026 09:14
Bumps the pip group with 1 update in the /quickwit/rest-api-tests directory: [idna](https://github.com/kjd/idna).


Updates `idna` from 3.11 to 3.15
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](kjd/idna@v3.11...v3.15)

---
updated-dependencies:
- dependency-name: idna
  dependency-version: '3.15'
  dependency-type: indirect
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…oss#6724)

* test(ci): validate full test workflow

* fix(ci): start services before full tests

* test(ci): restore maintainer trigger

* chore(ci): simplify full test job name
* build(deps): group Dependabot security updates

* fix(deps): point npm updater at UI manifest
…#6734)

All paths now fall under @quickwit-oss/quickwit-core so PRs that touch
Parquet/DataFusion code are no longer blocked on a second GitHub team.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ss#6723)

* Optimize single-file reads from split bundles

Reuse tail bytes when possible to avoid opening the full bundle and reduce object storage requests.

* Cover both legacy footer read paths

* Simplify split offset conversions for 64-bit targets

* Make split tail read sequence explicit

* nit: accept slice in locate_split_footer_range_in_tail

* Simplify split tail window bounds

* Clarify split file fetch return values
…search requests (quickwit-oss#6727)

* Use hash of the cache key to limit in-memory heap overhead for large search requests


Co-authored-by: Arthur <arthur.brongniart@datadoghq.com>
…kwit-oss#6742)

Bumps [serde_with](https://github.com/jonasbb/serde_with) from 3.21.0 to 3.22.0.
- [Release notes](https://github.com/jonasbb/serde_with/releases)
- [Commits](jonasbb/serde_with@v3.21.0...v3.22.0)

---
updated-dependencies:
- dependency-name: serde_with
  dependency-version: 3.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…ickwit-oss#6739)

Bumps the aws group with 4 updates in the /quickwit directory: [aws-sdk-s3](https://github.com/awslabs/aws-sdk-rust), [aws-smithy-mocks](https://github.com/smithy-lang/smithy-rs), [aws-smithy-runtime-api](https://github.com/smithy-lang/smithy-rs) and [aws-types](https://github.com/smithy-lang/smithy-rs).


Updates `aws-sdk-s3` from 1.138.0 to 1.138.1
- [Release notes](https://github.com/awslabs/aws-sdk-rust/releases)
- [Commits](https://github.com/awslabs/aws-sdk-rust/commits)

Updates `aws-smithy-mocks` from 0.2.6 to 0.3.0
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

Updates `aws-smithy-runtime-api` from 1.13.0 to 1.14.0
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

Updates `aws-types` from 1.4.0 to 1.5.0
- [Release notes](https://github.com/smithy-lang/smithy-rs/releases)
- [Changelog](https://github.com/smithy-lang/smithy-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smithy-lang/smithy-rs/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-version: 1.138.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: aws
- dependency-name: aws-smithy-mocks
  dependency-version: 0.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: aws-smithy-runtime-api
  dependency-version: 1.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
- dependency-name: aws-types
  dependency-version: 1.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: aws
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: David Yaffe <dayaffe99@gmail.com>
…wit-oss#6745)

Bumps [serial_test](https://github.com/palfrey/serial_test) from 3.5.0 to 4.0.1.
- [Release notes](https://github.com/palfrey/serial_test/releases)
- [Commits](palfrey/serial_test@v3.5.0...v4.0.1)

---
updated-dependencies:
- dependency-name: serial_test
  dependency-version: 4.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: David Yaffe <dayaffe99@gmail.com>
* Lower default field list size limit to 10k

* Document field list limit rationale

* Add per-request field list limit

Allow field capabilities and mappings requests to override the default field
limit, and propagate the limit through root and leaf merges.

* Restrict field limit override to protobuf requests

---------

Co-authored-by: Pascal Seitz <pascal.seitz@gmail.com>
…he warm when searcher pods restart (quickwit-oss#6711)

* feat(search): use stable node IDs for split affinity

Rendezvous hashing previously keyed off gRPC addresses, so a searcher
restart that kept QW_NODE_ID but changed listen address remapped every
split. Hash the cluster node ID instead, while keeping the searcher pool
keyed by address for RPC.

* refactor(search): rename SocketAddrAndClient to NodeIdAndClient

The helper no longer stores a socket address; rendezvous hashing uses
node_id only.

* refactor(common): drop unused SocketAddrLegacyHash

Rendezvous hashing now keys off node IDs, so the pre-Rust-1.81 socket
address hash wrapper is unused.
)

Size the search Rayon pool from Quickwit's configured CPU capacity instead
of all CPUs visible on the node. In Kubernetes, searcher pods can request
fewer CPUs than the node exposes, but Rayon otherwise creates a worker for
every visible CPU. CPU-heavy searches can then saturate the node and delay
load-reporting RPCs, causing searchers to be treated as unreachable.

Co-authored-by: Pascal Seitz <pascal.seitz@gmail.com>
Drain indexing pipelines on planned teardowns instead of killing them:
the source stops emitting, flushes the in-flight batches with a forced
commit, and the pipeline exits on its own once everything is published
and settled. Sources opt in via source_needs_drain; others keep the
plain kill semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Totodore and others added 9 commits September 4, 2026 18:15
Drain every pipeline on teardown (sources without acknowledgment state
exit as soon as their flush batch is pushed), share the drain state
machine between both pipeline flavors, and fix the drain accounting:
DrainAllPipelines now also waits for pipelines a plan change already
detached, merge pipelines survive their draining indexing pipelines,
and a paused draining pipeline is no longer reaped as exited.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bound the node-shutdown drain wait with a configurable indexer
shutdown_drain_timeout (QW_INDEXER_SHUTDOWN_DRAIN_TIMEOUT, 300s by
default) so a drain can no longer outlive the deployment's grace period.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Totodore Totodore changed the title Drain indexing pipelines on planned teardowns feat(indexer): drain indexing pipelines on planned teardowns Sep 4, 2026
Totodore and others added 12 commits September 4, 2026 20:33
…oss#6758)

Bumps [hyper](https://github.com/hyperium/hyper) from 1.10.1 to 1.11.1.
- [Release notes](https://github.com/hyperium/hyper/releases)
- [Changelog](https://github.com/hyperium/hyper/blob/master/CHANGELOG.md)
- [Commits](hyperium/hyper@v1.10.1...v1.11.1)

---
updated-dependencies:
- dependency-name: hyper
  dependency-version: 1.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: David Yaffe <dayaffe99@gmail.com>
…ss#6756)

Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.23.4 to 1.26.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](uuid-rs/uuid@v1.23.4...v1.26.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 1.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…lled synchronously to match previous semantics
…lled synchronously to match previous semantics
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.

7 participants