apache/beam · error · ValueError

After a successful split, the stop position of the RangeTrac

Error message

After a successful split, the stop position of the RangeTracker must be the same as the returned split position. Observed %r and %r which are different.

What it means

Raised when, after a successful try_split, the RangeTracker's stop_position() does not equal the returned split position. Per the Beam RangeTracker contract, a successful split must set the current range's stop to the returned split position (the remainder becomes the other split). A mismatch means the custom RangeTracker updated its range inconsistently. Note the error-formatting line itself has a % typo in this Beam version, but the condition checked is the stop/split-position mismatch.

Source

Thrown at sdks/python/apache_beam/io/source_test_utils.py:308

  reader = source.read(range_tracker)
  # Reading 'num_items_to_read_before_split' items.
  reader_iter = iter(reader)
  for _ in range(num_items_to_read_before_split):
    current_items.append(next(reader_iter))

  suggested_split_position = range_tracker.position_at_fraction(split_fraction)

  stop_position_before_split = range_tracker.stop_position()
  split_result = range_tracker.try_split(suggested_split_position)

  if split_result is not None:
    if len(split_result) != 2:
      raise ValueError(
          'Split result must be a tuple that contains split '
          'position and split fraction. Received: %r' % (split_result, ))

    if range_tracker.stop_position() != split_result[0]:
      raise ValueError(
          'After a successful split, the stop position of the '
          'RangeTracker must be the same as the returned split '
          'position. Observed %r and %r which are different.' %
          (range_tracker.stop_position() % (split_result[0], )))

    if split_fraction < 0 or split_fraction > 1:
      raise ValueError(
          'Split fraction must be within the range [0,1]',
          'Observed split fraction was %r.' % (split_result[1], ))

  stop_position_after_split = range_tracker.stop_position()
  if split_result and stop_position_after_split == stop_position_before_split:
    raise ValueError(
        'Stop position %r did not change after a successful '
        'split of source %r at fraction %r.' %
        (stop_position_before_split, source, split_fraction))

  if expected_outcome == ExpectedSplitOutcome.MUST_SUCCEED_AND_BE_CONSISTENT:

View on GitHub (pinned to 12126d8942)

Solutions

  1. Inside try_split, set the tracker's stop position to the returned split position before returning.
  2. Use iobase.RangeTracker's built-in implementation rather than a from-scratch RangeTracker.
  3. Verify position_at_fraction and try_split agree on position semantics (same position space).
  4. Check for an off-by-one where the range keeps one extra/missing record after the split.

Example fix

# before
def try_split(self, fraction):
  pos = self.position_at_fraction(fraction)
  return (pos, fraction)
# after
def try_split(self, fraction):
  pos = self.position_at_fraction(fraction)
  if pos is None:
    return None
  old_stop = self.stop_position
  self.stop_position = pos
  return (pos, fraction)
Defensive patterns

Strategy: validation

Validate before calling

before = tracker.stop_position()
res = tracker.try_split(pos)
if res is not None:
    assert tracker.stop_position() == res[0], (tracker.stop_position(), res[0])

Type guard

def split_updates_stop(tracker, pos):
    before = tracker.stop_position()
    res = tracker.try_split(pos)
    return res is None or tracker.stop_position() == res[0]

Try / catch

try:
    source_test_utils.assert_split_at_fraction_behavior(src, n, f, outcome)
except ValueError as e:
    if "stop position of the RangeTracker" in str(e):
        logger.error("try_split did not shrink range: %s", e)
    raise

Prevention

When it happens

Trigger: Custom RangeTracker.try_split returns (split_position, fraction) but does not call _stop_position = split_position (or calls it with a different value), then _assert_split_at_fraction_behavior re-reads stop_position().

Common situations: Hand-rolled RangeTracker implementations that forget to shrink the range after yielding the split, or that shrink by the wrong amount (off-by-one position).

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of apache/beam@12126d8942 (2026-09-13). Data as JSON: /api/errors/a49e9f7f4c35112a. Report an issue: GitHub.