Skip to content

SAI-6231: Bucketed segment flush handling on only single doc iterator#41

Merged
patsonluk merged 34 commits into
fs/branch_9_11from
patsonluk/single-doc-bucketed-flush
Jun 25, 2026
Merged

SAI-6231: Bucketed segment flush handling on only single doc iterator#41
patsonluk merged 34 commits into
fs/branch_9_11from
patsonluk/single-doc-bucketed-flush

Conversation

@patsonluk

@patsonluk patsonluk commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Description

Built on top of this #40, with changes to only support delegating to a bucketed DocumentWriterPerThread (DWPT) if the input to DocumentWriter#updateDocuments is a single doc. This simplifies the logic flow

The bucket key will be the value of the input doc's field (defined by sysprop lucene.temporalField.name) mapped to the boundary of the bucket. The default boundaries are (in days) [-9, 3, 9, 32, 94, 184]. For example for -Dlucene.temporalField.name=EventStart,SessionStart,UserLastIndexedEventStart,UserTipLastEventStart, a doc with EventStart a day ago will be mapped to the bucket 3 (up to 3 days ago)

This is related to the TemporalMergePolicy work which group segments of the same bucket (using the same boundaries as defined in here) for merges.

Solution

Mostly work from @magibney from this PR

  1. Logic to read sysprop lucene.temporalField.name and map input doc into bucket key
  2. Changes to DocumentWriterFlushControl and DocumentWriterPerThreadPool to support creating/managing DWPT instances with a bucket key
  3. PeekIterable to peek first (and only doc) to delegate to corresponding bucket DWPT
  4. Change in MergePolicy to expose bucket mapping logic to higher layer (Solr TemporalMergePolicy etc)

Take note that we only support bucket mapping for DocumentWriter#updateDocuments with single doc for now. As supporting multiple document get complicated, as demonstrated in this original PR:

  1. Need to delegate to multiple DWPTs while maintaining execute order across all the instances
  2. Extra threads to support multiple DWPTs per updateDocuments processing

Note

Medium Risk
Changes the hot indexing path (DWPT acquisition and pooling) when routing is on, though behavior is opt-in via system properties and limited to single-document updates without a parent field.

Overview
Adds time-bucket routing at flush time so single-document updateDocuments paths can pin in-RAM segments to a bucket derived from a document’s temporal numeric field (primary + optional fallbacks), driven by JVM properties such as lucene.temporalField.name, lucene.temporalField.boundaries, and lucene.temporalField.adjustNow. Multi-document batches and indexes with a parent field skip routing and use the default bucket.

DocumentsWriter peeks the lone document (via PeekIterable) when routing is enabled, maps it with SegmentRoutingUtil, and calls obtainAndLock(bucket). DocumentsWriterPerThreadPool keeps separate free queues per bucket; each DocumentsWriterPerThread carries its bucket for checkout/return. MergePolicy.mapToBucket re-exports the boundary logic for Solr-style temporal merge policies.

A concurrent testUpdateDocumentRouting checks that live docs land in one temporal bucket per leaf after adds/updates; merge-policy reflection tests ignore the new static helper.

Reviewed by Cursor Bugbot for commit 5d28831. Bugbot is set up for automated code reviews on this repo. Configure here.

@patsonluk patsonluk marked this pull request as ready for review June 9, 2026 19:27
Comment thread lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java
Comment thread lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java Outdated
@patsonluk patsonluk changed the title Patsonluk/single doc bucketed flush SAI-6231: Bucketed segment flush handling on only single doc iterator Jun 9, 2026
…Now, which now takes a date string and use that directly as now

@magibney magibney 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.

Looking good I think!

I wonder if it'd be easy/worth pulling out as much as possible into its own class, as opposed to static fields/methods on DWPT (and PeekIterable from DocumentsWriter as well). As it stands it's kinda obvious where the changes are, but putting in a dedicated file would make it a bit clearer/cleaner?

Comment thread lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java Outdated
Comment thread lucene/core/src/java/org/apache/lucene/index/DocumentsWriterFlushControl.java Outdated

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 39a3420. Configure here.

Comment thread lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java
@patsonluk

Copy link
Copy Markdown
Collaborator Author

Looking good I think!

I wonder if it'd be easy/worth pulling out as much as possible into its own class, as opposed to static fields/methods on DWPT (and PeekIterable from DocumentsWriter as well). As it stands it's kinda obvious where the changes are, but putting in a dedicated file would make it a bit clearer/cleaner?

Thanks! Did a quick refactor at here . For now keeping PeekIterable in where it is, cause the coupling is quite strong? 😁 Lemme know!

Comment thread lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java
Comment thread lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java
Comment thread lucene/core/src/test/org/apache/lucene/index/TestNoMergePolicy.java
Comment thread lucene/core/src/test/org/apache/lucene/index/TestIndexWriter.java Outdated
2. Enable routing for unit testing
3. Added comments on parent field handling
Comment thread lucene/core/src/test/org/apache/lucene/search/TestLRUQueryCache.java Outdated
Comment thread lucene/core/src/java/org/apache/lucene/index/SegmentRoutingUtil.java Outdated
}

static long mapToBucket(Iterable<? extends IndexableField> doc) {
return mapToBucket(doc, TEMPORAL_ADJUST_NOW != null ? TEMPORAL_ADJUST_NOW : System.currentTimeMillis(), defaultBucket());

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.

we shouldn't really need to repeateedly invoke System.currentTimeMillis(). Ideally we could set a TEMPORAL_ADJUST_NANOS based on diff between System.nanoTime() and configured sysprop lucene.temporalField.adjustNow?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

are u thinking about optimization of calling System.nanoTime() instead of System.currentTimeMillis() all the time? like calculate the base diff at init time (vs System.currentTimeMillis() or TEMPORAL_ADJUST_NOW). Then later on we can just use base diff + System.nanoTime() (ie cheaper?)

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.

exactly that, yes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I have made the changes here, can u please 👀 ? e9809bc

It's slightly different for the handling of lucene.temporalField.adjustNow. In such case I think we don't even want the time to advance at all, since having a static now time will be the most consistent for testing.

Comment thread lucene/core/src/test/org/apache/lucene/search/TestLRUQueryCache.java Outdated
@patsonluk

patsonluk commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator Author

ping @hiteshk25 before we merge (think it should be close to approval from @magibney), please let us know if there are any concerns. A TL;DR of the changes:

  1. The new routing logic are all in new class SegmentRoutingUtil
  2. Changes to existing classes (DocumentsWriter...) are mainly:
    1. Allow lookups of DocumentWriterPerThread using a "bucket key" which is mapped from the hard-coded day range boundaries
    2. The routing logic only get triggered if sysprop lucene.temporalField.name is defined. If undefined, it should run the old logic, using the "default" bucket key for everything and w/o any routing.

@hiteshk25

Copy link
Copy Markdown
Collaborator

👁️

if (ADJUST_NOW != null) { //explicitly defined a static now time. Use it for all calls
return ADJUST_NOW;
} else {
return NOW_BASE_IN_MILLI_SEC + TimeUnit.NANOSECONDS.toMillis(System.nanoTime());

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.

I think this is not formally correct. System.nanoTime() can rollover and be negative. I think if we're going to do this, we have to convert fully to nanos -- e.g.

NOW_BASE_MILLIS = ADJUST_NOW == null ? System.currentTimeMillis() : ADJUST_NOW;
NOW_BASE_NANOS = System.nanoTime();

getNow() {
  return NOW_BASE_MILLIS + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - NOW_BASE_NANOS);
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I was trying to save one arithmetic but good point on the wrap-around issue!

Im not going to care about ADJUST_NOW, since if it's defined, we will NOT do any computation and use the value directly

@magibney magibney 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

@patsonluk patsonluk merged commit 928bce3 into fs/branch_9_11 Jun 25, 2026
2 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.

3 participants