{"record":{"id":"1b46ac719e723e10","repo":"apache/beam","slug":"stop-position-r-did-not-change-after-a-successful-split-of","errorCode":null,"errorMessage":"Stop position %r did not change after a successful split of source %r at fraction %r.","messagePattern":"Stop position %r did not change after a successful split of source %r at fraction %r\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"sdks/python/apache_beam/io/source_test_utils.py","lineNumber":321,"sourceCode":"      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:\n    if not split_result:\n      raise ValueError(\n          'Expected split of source %r at fraction %r to be '\n          'successful after reading %d elements. But '\n          'the split failed.' %\n          (source, split_fraction, num_items_to_read_before_split))\n  elif expected_outcome == ExpectedSplitOutcome.MUST_FAIL:\n    if split_result:\n      raise ValueError(\n          'Expected split of source %r at fraction %r after '\n          'reading %d elements to fail. But splitting '\n          'succeeded with result %r.' % (\n              source,","sourceCodeStart":303,"sourceCodeEnd":339,"githubUrl":"https://github.com/apache/beam/blob/12126d8942aaf848030c478b4c6a28c6af861c66/sdks/python/apache_beam/io/source_test_utils.py#L303-L339","documentation":"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).","triggerScenarios":"_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.","commonSituations":"Custom RangeTracker implementations with immutably-copied state, thread-unsafe updates lost on return, or implementations that forget to persist the shrunk range on self.","solutions":["Assign the new stop position to the tracker's own state inside try_split (self.stop_position = split_position).","Ensure try_split mutates the same instance that get_range_tracker handed to read().","Add thread-safety (locks) if try_split may be called from another thread, so the update isn't lost.","Write a unit test asserting stop_position() changes after a successful try_split."],"exampleFix":"# before\ndef try_split(self, fraction):\n  new_stop = self.position_at_fraction(fraction)\n  return (new_stop, fraction)  # state never updated\n# after\ndef try_split(self, fraction):\n  new_stop = self.position_at_fraction(fraction)\n  if new_stop is None:\n    return None\n  self._stop = new_stop\n  return (new_stop, fraction)","handlingStrategy":"validation","validationCode":"before = tracker.stop_position()\nres = tracker.try_split(pos)\nif res is not None:\n    assert tracker.stop_position() != before, \"split claimed success but range unchanged\"","typeGuard":"def split_mutates_state(tracker, pos):\n    before = tracker.stop_position()\n    res = tracker.try_split(pos)\n    return res is None or tracker.stop_position() != before","tryCatchPattern":"try:\n    source_test_utils.assert_split_at_fraction_behavior(src, n, f, outcome)\nexcept ValueError as e:\n    if \"did not change after a successful\" in str(e):\n        logger.error(\"try_split returned result without updating state: %s\", e)\n    raise","preventionTips":["Persist the shrunk range on the tracker instance inside try_split","Make try_split thread-safe so updates are never lost","Unit-test that stop_position() changes after every successful split"],"tags":["apache-beam","range-tracker","dynamic-splitting","state-mutation"],"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"}