apache/beam · error · ValueError

Stop position %r did not change after a successful split of

Error message

Stop position %r did not change after a successful split of source %r at fraction %r.

What it means

Raised when try_split reports success (returns a non-None result) but the RangeTracker's stop_position() is unchanged after the split. A successful dynamic split must shrink the current range, so its stop position must move to the split position; an unchanged stop position means the tracker claims a split happened without actually updating its range, which would double-read records in a real pipeline. This catches RangeTrackers that return a split result but mutate no state (or mutate only a copy).

Source

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

      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:
    if not split_result:
      raise ValueError(
          'Expected split of source %r at fraction %r to be '
          'successful after reading %d elements. But '
          'the split failed.' %
          (source, split_fraction, num_items_to_read_before_split))
  elif expected_outcome == ExpectedSplitOutcome.MUST_FAIL:
    if split_result:
      raise ValueError(
          'Expected split of source %r at fraction %r after '
          'reading %d elements to fail. But splitting '
          'succeeded with result %r.' % (
              source,

View on GitHub (pinned to 12126d8942)

Solutions

  1. Assign the new stop position to the tracker's own state inside try_split (self.stop_position = split_position).
  2. Ensure try_split mutates the same instance that get_range_tracker handed to read().
  3. Add thread-safety (locks) if try_split may be called from another thread, so the update isn't lost.
  4. Write a unit test asserting stop_position() changes after a successful try_split.

Example fix

# before
def try_split(self, fraction):
  new_stop = self.position_at_fraction(fraction)
  return (new_stop, fraction)  # state never updated
# after
def try_split(self, fraction):
  new_stop = self.position_at_fraction(fraction)
  if new_stop is None:
    return None
  self._stop = new_stop
  return (new_stop, 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() != before, "split claimed success but range unchanged"

Type guard

def split_mutates_state(tracker, pos):
    before = tracker.stop_position()
    res = tracker.try_split(pos)
    return res is None or tracker.stop_position() != before

Try / catch

try:
    source_test_utils.assert_split_at_fraction_behavior(src, n, f, outcome)
except ValueError as e:
    if "did not change after a successful" in str(e):
        logger.error("try_split returned result without updating state: %s", e)
    raise

Prevention

When it happens

Trigger: _assert_split_at_fraction_behavior compares stop_position() before and after try_split; split_result is truthy but stop_position_after_split == stop_position_before_split, e.g. try_split computes and returns (pos, fraction) but writes the new stop to a local variable or a different tracker instance.

Common situations: Custom RangeTracker implementations with immutably-copied state, thread-unsafe updates lost on return, or implementations that forget to persist the shrunk range on self.

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/1b46ac719e723e10. Report an issue: GitHub.