Skip to content

fix(batch): require both counts unchanged before the idle flush (#2105) - #2109

Open
Anai-Guo wants to merge 1 commit into
weaviate:mainfrom
Anai-Guo:fix/batch-idle-guard-and
Open

fix(batch): require both counts unchanged before the idle flush (#2105)#2109
Anai-Guo wants to merge 1 commit into
weaviate:mainfrom
Anai-Guo:fix/batch-idle-guard-and

Conversation

@Anai-Guo

Copy link
Copy Markdown

Fixes #2105.

The bug

The wait loop that lets a batch accumulate before it is sent breaks out early once a second has passed and nothing new has arrived. The guard for "nothing new has arrived" ORs the two unchanged-checks:

while (len_o := len(self.__batch_objects)) + (len_r := len(self.__batch_references)) < self.__batch_size:
    # wait for more objects to be added up to the batch size
    time.sleep(refresh_time)
    if time.time() - start >= 1 and (
        len_o == len(self.__batch_objects) or len_r == len(self.__batch_references)
    ):
        # no new objects were added in the last second, exit the loop
        break

In an objects-only batch — what add_object without add_reference produces, i.e. the common case — len_r and len(self.__batch_references) are both permanently 0, so the right operand is always True and the whole condition collapses to if time.time() - start >= 1. The loop then stops waiting on the first tick past one second regardless of how fast objects are still streaming in, and the batch is sent well short of its configured size. A references-only batch has the mirror-image problem.

The comment on the break states the intent — the batch is idle — which holds only when both counts are unchanged.

The fix

orand at the three places that share this loop shape:

  • weaviate/collections/batch/sync.py:180
  • weaviate/collections/batch/async_.py:228
  • weaviate/collections/batch/base.py:437

Measured effect

The loop needs a live connection and a background thread, so I reproduced it standalone: the exact wait-loop body, a fake object queue, and a producer adding objects at a steady rate with no references. Both variants run the same producer for 8 s; the only difference is or vs and.

batch_size  rate/s | variant  flushes   sent  avg batch
-------------------------------------------------------
      1000     300 | or             8   2254      281.8
      1000     300 | and            3   2255      751.7

      1000     500 | or             8   3234      404.2
      1000     500 | and            4   3223      805.8

       200     120 | or             8    935      116.9
       200     120 | and            5    932      186.4

Same objects delivered, but and reaches a batch size near the configured limit instead of whatever landed in the first second — 2.7× fewer round trips in the first row. The rates that matter are those where filling a batch takes longer than the one-second threshold; above that the batch fills before the guard is ever consulted and the two variants are identical, which is why the default batch_size=100 at high throughput does not show it.

The and form cannot stall: the outer while still bounds the wait by batch_size, and the guard still fires as soon as a tick passes with no new objects.

On tests

I did not add one. The guard lives inside a private background-thread loop behind a live gRPC connection, and it is keyed on wall-clock (time.time() - start >= 1), so a test that exercises it would be timing-dependent and flaky in CI — the existing test/collection/test_batch.py is pure-function tests only. Happy to add one under mock_tests/ if you would like it and can point me at the shape you would accept.

Verified ruff format --check and ruff check clean with the pinned ruff 0.14.7 and the repo's ruff.toml.

🤖 Generated with Claude Code

The wait loop in the batching threads breaks out early when a second has
passed and nothing new arrived, but the guard ORs the two "unchanged"
checks:

    if time.time() - start >= 1 and (
        len_o == len(self.__batch_objects) or len_r == len(self.__batch_references)
    ):

In an objects-only batch -- what `add_object` without `add_reference`
produces -- `len_r` and `len(self.__batch_references)` are both always 0,
so the right operand is always True and the whole guard collapses to
`if time.time() - start >= 1`. The loop then stops waiting on the first
tick past one second no matter how fast objects are still streaming in,
so the batch is flushed well short of its configured size. A
references-only batch has the mirror-image problem.

The comment above the break says "no new objects were added in the last
second", i.e. the batch is idle -- which is true only when *both* counts
are unchanged. Switch `or` to `and` at the three places that share this
loop shape (sync.py, async_.py, base.py).

Fixes weaviate#2105

@orca-security-eu orca-security-eu 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.

Orca Security Scan Summary

Status Check Issues by priority
Passed Passed Infrastructure as Code high 0   medium 0   low 0   info 0 View in Orca
Passed Passed SAST high 0   medium 0   low 0   info 0 View in Orca
Passed Passed Secrets high 0   medium 0   low 0   info 0 View in Orca
Passed Passed Vulnerabilities high 0   medium 0   low 0   info 0 View in Orca

@weaviate-git-bot

Copy link
Copy Markdown

To avoid any confusion in the future about your contribution to Weaviate, we work with a Contributor License Agreement. If you agree, you can simply add a comment to this PR that you agree with the CLA so that we can merge.

beep boop - the Weaviate bot 👋🤖

PS:
Are you already a member of the Weaviate Forum?

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.

Batch idle guard uses or instead of and — premature flush in objects-only batches, throughput degraded

2 participants