{"record":{"id":"a49e9f7f4c35112a","repo":"apache/beam","slug":"after-a-successful-split-the-stop-position-of-the","errorCode":null,"errorMessage":"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.","messagePattern":"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\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"sdks/python/apache_beam/io/source_test_utils.py","lineNumber":308,"sourceCode":"  reader = source.read(range_tracker)\n  # Reading 'num_items_to_read_before_split' items.\n  reader_iter = iter(reader)\n  for _ in range(num_items_to_read_before_split):\n    current_items.append(next(reader_iter))\n\n  suggested_split_position = range_tracker.position_at_fraction(split_fraction)\n\n  stop_position_before_split = range_tracker.stop_position()\n  split_result = range_tracker.try_split(suggested_split_position)\n\n  if split_result is not None:\n    if len(split_result) != 2:\n      raise ValueError(\n          'Split result must be a tuple that contains split '\n          'position and split fraction. Received: %r' % (split_result, ))\n\n    if range_tracker.stop_position() != split_result[0]:\n      raise ValueError(\n          'After a successful split, the stop position of the '\n          'RangeTracker must be the same as the returned split '\n          'position. Observed %r and %r which are different.' %\n          (range_tracker.stop_position() % (split_result[0], )))\n\n    if split_fraction < 0 or split_fraction > 1:\n      raise ValueError(\n          'Split fraction must be within the range [0,1]',\n          'Observed split fraction was %r.' % (split_result[1], ))\n\n  stop_position_after_split = range_tracker.stop_position()\n  if split_result and stop_position_after_split == stop_position_before_split:\n    raise ValueError(\n        'Stop position %r did not change after a successful '\n        'split of source %r at fraction %r.' %\n        (stop_position_before_split, source, split_fraction))\n\n  if expected_outcome == ExpectedSplitOutcome.MUST_SUCCEED_AND_BE_CONSISTENT:","sourceCodeStart":290,"sourceCodeEnd":326,"githubUrl":"https://github.com/apache/beam/blob/12126d8942aaf848030c478b4c6a28c6af861c66/sdks/python/apache_beam/io/source_test_utils.py#L290-L326","documentation":"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.","triggerScenarios":"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().","commonSituations":"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).","solutions":["Inside try_split, set the tracker's stop position to the returned split position before returning.","Use iobase.RangeTracker's built-in implementation rather than a from-scratch RangeTracker.","Verify position_at_fraction and try_split agree on position semantics (same position space).","Check for an off-by-one where the range keeps one extra/missing record after the split."],"exampleFix":"# before\ndef try_split(self, fraction):\n  pos = self.position_at_fraction(fraction)\n  return (pos, fraction)\n# after\ndef try_split(self, fraction):\n  pos = self.position_at_fraction(fraction)\n  if pos is None:\n    return None\n  old_stop = self.stop_position\n  self.stop_position = pos\n  return (pos, fraction)","handlingStrategy":"validation","validationCode":"before = tracker.stop_position()\nres = tracker.try_split(pos)\nif res is not None:\n    assert tracker.stop_position() == res[0], (tracker.stop_position(), res[0])","typeGuard":"def split_updates_stop(tracker, pos):\n    before = tracker.stop_position()\n    res = tracker.try_split(pos)\n    return res is None or tracker.stop_position() == res[0]","tryCatchPattern":"try:\n    source_test_utils.assert_split_at_fraction_behavior(src, n, f, outcome)\nexcept ValueError as e:\n    if \"stop position of the RangeTracker\" in str(e):\n        logger.error(\"try_split did not shrink range: %s\", e)\n    raise","preventionTips":["Always set self stop position to the split position inside try_split","Mirror OffsetRangeTracker's try_split implementation","Assert stop_position() == returned split position in your own tests"],"tags":["apache-beam","range-tracker","dynamic-splitting","contract-violation"],"backgroundTag":"internal-invariant-violation","analyzedSha":"12126d8942aaf848030c478b4c6a28c6af861c66","analyzedAt":"2026-09-13T01:50:10.254Z","contentChangedAt":"2026-09-13T01:50:10.254Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}