@@ -3159,6 +3159,7 @@ def test_given_unfinished_first_parent_partition_no_parent_state_update():
31593159 }
31603160 assert mock_cursor_1 .stream_slices .call_count == 1 # Called once for each partition
31613161 assert mock_cursor_2 .stream_slices .call_count == 1 # Called once for each partition
3162+ assert len (cursor ._semaphore_per_partition ) == 2
31623163
31633164
31643165def test_given_unfinished_last_parent_partition_with_partial_parent_state_update ():
@@ -3243,6 +3244,7 @@ def test_given_unfinished_last_parent_partition_with_partial_parent_state_update
32433244 }
32443245 assert mock_cursor_1 .stream_slices .call_count == 1 # Called once for each partition
32453246 assert mock_cursor_2 .stream_slices .call_count == 1 # Called once for each partition
3247+ assert len (cursor ._semaphore_per_partition ) == 1
32463248
32473249
32483250def test_given_all_partitions_finished_when_close_partition_then_final_state_emitted ():
@@ -3317,6 +3319,7 @@ def test_given_all_partitions_finished_when_close_partition_then_final_state_emi
33173319 assert final_state ["lookback_window" ] == 1
33183320 assert cursor ._message_repository .emit_message .call_count == 2
33193321 assert mock_cursor .stream_slices .call_count == 2 # Called once for each partition
3322+ assert len (cursor ._semaphore_per_partition ) == 1
33203323
33213324
33223325def test_given_partition_limit_exceeded_when_close_partition_then_switch_to_global_cursor ():
@@ -3377,3 +3380,75 @@ def test_given_partition_limit_exceeded_when_close_partition_then_switch_to_glob
33773380 assert "lookback_window" in final_state
33783381 assert len (cursor ._cursor_per_partition ) <= cursor .DEFAULT_MAX_PARTITIONS_NUMBER
33793382 assert mock_cursor .stream_slices .call_count == 3 # Called once for each partition
3383+
3384+
3385+ def test_semaphore_cleanup ():
3386+ # Create two mock cursors with different states for each partition
3387+ mock_cursor_1 = MagicMock ()
3388+ mock_cursor_1 .stream_slices .return_value = iter (
3389+ [
3390+ {"slice1" : "data1" },
3391+ {"slice2" : "data1" }, # First partition slices
3392+ ]
3393+ )
3394+ mock_cursor_1 .state = {"updated_at" : "2024-01-02T00:00:00Z" } # State for partition "1"
3395+
3396+ mock_cursor_2 = MagicMock ()
3397+ mock_cursor_2 .stream_slices .return_value = iter (
3398+ [
3399+ {"slice2" : "data2" },
3400+ {"slice2" : "data2" }, # Second partition slices
3401+ ]
3402+ )
3403+ mock_cursor_2 .state = {"updated_at" : "2024-01-03T00:00:00Z" } # State for partition "2"
3404+
3405+ # Configure cursor factory to return different mock cursors based on partition
3406+ cursor_factory_mock = MagicMock ()
3407+ cursor_factory_mock .create .side_effect = [mock_cursor_1 , mock_cursor_2 ]
3408+
3409+ cursor = ConcurrentPerPartitionCursor (
3410+ cursor_factory = cursor_factory_mock ,
3411+ partition_router = MagicMock (),
3412+ stream_name = "test_stream" ,
3413+ stream_namespace = None ,
3414+ stream_state = {},
3415+ message_repository = MagicMock (),
3416+ connector_state_manager = MagicMock (),
3417+ connector_state_converter = MagicMock (),
3418+ cursor_field = CursorField (cursor_field_key = "updated_at" ),
3419+ )
3420+
3421+ # Simulate partitions with unique parent states
3422+ slices = [
3423+ StreamSlice (partition = {"id" : "1" }, cursor_slice = {}),
3424+ StreamSlice (partition = {"id" : "2" }, cursor_slice = {}),
3425+ ]
3426+ cursor ._partition_router .stream_slices .return_value = iter (slices )
3427+ # Simulate unique parent states for each partition
3428+ cursor ._partition_router .get_stream_state .side_effect = [
3429+ {"parent" : {"state" : "state1" }}, # Parent state for partition "1"
3430+ {"parent" : {"state" : "state2" }}, # Parent state for partition "2"
3431+ ]
3432+
3433+ # Generate slices to populate semaphores and parent states
3434+ generated_slices = list (
3435+ cursor .stream_slices ()
3436+ ) # Populate _semaphore_per_partition and _partition_parent_state_map
3437+
3438+ # Verify initial state
3439+ assert len (cursor ._semaphore_per_partition ) == 2
3440+ assert len (cursor ._partition_parent_state_map ) == 2
3441+ assert cursor ._partition_parent_state_map ['{"id":"1"}' ] == {"parent" : {"state" : "state1" }}
3442+ assert cursor ._partition_parent_state_map ['{"id":"2"}' ] == {"parent" : {"state" : "state2" }}
3443+
3444+ # Close partitions to acquire semaphores (value back to 0)
3445+ for s in generated_slices :
3446+ cursor .close_partition (DeclarativePartition ("test_stream" , {}, MagicMock (), MagicMock (), s ))
3447+
3448+ # Check state after closing partitions
3449+ assert len (cursor ._finished_partitions ) == 2
3450+ assert len (cursor ._semaphore_per_partition ) == 0
3451+ assert '{"id":"1"}' not in cursor ._semaphore_per_partition
3452+ assert '{"id":"2"}' not in cursor ._semaphore_per_partition
3453+ assert len (cursor ._partition_parent_state_map ) == 0 # All parent states should be popped
3454+ assert cursor ._parent_state == {"parent" : {"state" : "state2" }} # Last parent state
0 commit comments