@@ -3159,6 +3159,7 @@ def test_given_unfinished_first_parent_partition_no_parent_state_update():
3159
3159
}
3160
3160
assert mock_cursor_1 .stream_slices .call_count == 1 # Called once for each partition
3161
3161
assert mock_cursor_2 .stream_slices .call_count == 1 # Called once for each partition
3162
+ assert len (cursor ._semaphore_per_partition ) == 2
3162
3163
3163
3164
3164
3165
def 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
3243
3244
}
3244
3245
assert mock_cursor_1 .stream_slices .call_count == 1 # Called once for each partition
3245
3246
assert mock_cursor_2 .stream_slices .call_count == 1 # Called once for each partition
3247
+ assert len (cursor ._semaphore_per_partition ) == 1
3246
3248
3247
3249
3248
3250
def 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
3317
3319
assert final_state ["lookback_window" ] == 1
3318
3320
assert cursor ._message_repository .emit_message .call_count == 2
3319
3321
assert mock_cursor .stream_slices .call_count == 2 # Called once for each partition
3322
+ assert len (cursor ._semaphore_per_partition ) == 1
3320
3323
3321
3324
3322
3325
def 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
3377
3380
assert "lookback_window" in final_state
3378
3381
assert len (cursor ._cursor_per_partition ) <= cursor .DEFAULT_MAX_PARTITIONS_NUMBER
3379
3382
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