@@ -3793,14 +3793,14 @@ def test_given_global_state_when_read_then_state_is_not_per_partition() -> None:
3793
3793
def _make_inner_cursor (ts : str ) -> MagicMock :
3794
3794
"""Return an inner cursor that yields exactly one slice and has a proper state."""
3795
3795
inner = MagicMock ()
3796
- inner .stream_slices .return_value = iter ([{"dummy" : "slice" }])
3796
+ inner .stream_slices .side_effect = lambda : iter ([{"dummy" : "slice" }])
3797
3797
inner .state = {"updated_at" : ts }
3798
3798
inner .close_partition .return_value = None
3799
3799
inner .observe .return_value = None
3800
3800
return inner
3801
3801
3802
3802
3803
- def test_duplicate_partition_after_cleanup ():
3803
+ def test_duplicate_partition_after_closing_partition_cursor_deleted ():
3804
3804
inner_cursors = [
3805
3805
_make_inner_cursor ("2024-01-01T00:00:00Z" ), # for first "1"
3806
3806
_make_inner_cursor ("2024-01-02T00:00:00Z" ), # for "2"
@@ -3861,6 +3861,74 @@ def test_duplicate_partition_after_cleanup():
3861
3861
assert len (cursor ._semaphore_per_partition ) == 0
3862
3862
assert len (cursor ._processing_partitions_indexes ) == 0
3863
3863
assert len (cursor ._partition_key_to_index ) == 0
3864
+ assert len (cursor ._partitions_done_generating_stream_slices ) == 0
3865
+
3866
+
3867
+ def test_duplicate_partition_after_closing_partition_cursor_exists ():
3868
+ inner_cursors = [
3869
+ _make_inner_cursor ("2024-01-01T00:00:00Z" ), # for first "1"
3870
+ _make_inner_cursor ("2024-01-02T00:00:00Z" ), # for "2"
3871
+ _make_inner_cursor ("2024-01-03T00:00:00Z" ), # for second "1"
3872
+ ]
3873
+ cursor_factory_mock = MagicMock ()
3874
+ cursor_factory_mock .create .side_effect = inner_cursors
3875
+
3876
+ converter = CustomFormatConcurrentStreamStateConverter (
3877
+ datetime_format = "%Y-%m-%dT%H:%M:%SZ" ,
3878
+ input_datetime_formats = ["%Y-%m-%dT%H:%M:%SZ" ],
3879
+ is_sequential_state = True ,
3880
+ cursor_granularity = timedelta (0 ),
3881
+ )
3882
+
3883
+ cursor = ConcurrentPerPartitionCursor (
3884
+ cursor_factory = cursor_factory_mock ,
3885
+ partition_router = MagicMock (),
3886
+ stream_name = "dup_stream" ,
3887
+ stream_namespace = None ,
3888
+ stream_state = {},
3889
+ message_repository = MagicMock (),
3890
+ connector_state_manager = MagicMock (),
3891
+ connector_state_converter = converter ,
3892
+ cursor_field = CursorField (cursor_field_key = "updated_at" ),
3893
+ )
3894
+
3895
+ # ── Partition sequence: 1 → 2 → 1 ──────────────────────────────────
3896
+ partitions = [
3897
+ StreamSlice (partition = {"id" : "1" }, cursor_slice = {}, extra_fields = {}),
3898
+ StreamSlice (partition = {"id" : "2" }, cursor_slice = {}, extra_fields = {}),
3899
+ StreamSlice (partition = {"id" : "1" }, cursor_slice = {}, extra_fields = {}),
3900
+ ]
3901
+ pr = cursor ._partition_router
3902
+ pr .stream_slices .return_value = iter (partitions )
3903
+ pr .get_stream_state .return_value = {}
3904
+
3905
+ # Iterate lazily so that the first "1" gets cleaned before
3906
+ # the second "1" arrives.
3907
+ slice_gen = cursor .stream_slices ()
3908
+
3909
+ first_1 = next (slice_gen )
3910
+ cursor .close_partition (
3911
+ DeclarativePartition ("dup_stream" , {}, MagicMock (), MagicMock (), first_1 )
3912
+ )
3913
+
3914
+ two = next (slice_gen )
3915
+ cursor .close_partition (DeclarativePartition ("dup_stream" , {}, MagicMock (), MagicMock (), two ))
3916
+
3917
+ # Second “1” should appear because the semaphore was cleaned up
3918
+ second_1 = next (slice_gen )
3919
+ cursor .close_partition (
3920
+ DeclarativePartition ("dup_stream" , {}, MagicMock (), MagicMock (), second_1 )
3921
+ )
3922
+
3923
+ with pytest .raises (StopIteration ):
3924
+ next (slice_gen )
3925
+
3926
+ assert cursor ._IS_PARTITION_DUPLICATION_LOGGED is False # no duplicate warning
3927
+ assert len (cursor ._cursor_per_partition ) == 2 # only “1” & “2” kept
3928
+ assert len (cursor ._semaphore_per_partition ) == 0 # all semaphores cleaned
3929
+ assert len (cursor ._processing_partitions_indexes ) == 0
3930
+ assert len (cursor ._partition_key_to_index ) == 0
3931
+ assert len (cursor ._partitions_done_generating_stream_slices ) == 0
3864
3932
3865
3933
3866
3934
def test_duplicate_partition_while_processing ():
@@ -3912,3 +3980,4 @@ def test_duplicate_partition_while_processing():
3912
3980
assert len (cursor ._semaphore_per_partition ) == 0
3913
3981
assert len (cursor ._processing_partitions_indexes ) == 0
3914
3982
assert len (cursor ._partition_key_to_index ) == 0
3983
+ assert len (cursor ._partitions_done_generating_stream_slices ) == 0
0 commit comments