counting_semaphore 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +167 -21
- data/Rakefile +6 -1
- data/lib/counting_semaphore/local_semaphore.rb +129 -47
- data/lib/counting_semaphore/null_logger.rb +29 -2
- data/lib/counting_semaphore/redis_semaphore.rb +175 -86
- data/lib/counting_semaphore/version.rb +3 -1
- data/lib/counting_semaphore/with_lease_support.rb +60 -0
- data/lib/counting_semaphore.rb +39 -5
- data/rbi/counting_semaphore.rbi +517 -0
- data/sig/counting_semaphore.rbs +367 -0
- data/test/counting_semaphore/local_semaphore_test.rb +365 -3
- data/test/counting_semaphore/redis_semaphore_test.rb +423 -9
- metadata +19 -4
- data/Gemfile.lock +0 -76
- data/lib/counting_semaphore/shared_semaphore.rb +0 -381
|
@@ -17,11 +17,6 @@ class RedisSemaphoreTest < Minitest::Test
|
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def test_initializes_with_correct_capacity
|
|
21
|
-
semaphore = CountingSemaphore::RedisSemaphore.new(5, "test_namespace")
|
|
22
|
-
assert_equal 5, semaphore.instance_variable_get(:@capacity)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
20
|
def test_capacity_attribute_returns_the_initialized_capacity
|
|
26
21
|
semaphore = CountingSemaphore::RedisSemaphore.new(10, "test_namespace")
|
|
27
22
|
assert_equal 10, semaphore.capacity
|
|
@@ -158,7 +153,7 @@ class RedisSemaphoreTest < Minitest::Test
|
|
|
158
153
|
results << "client2_attempting_5_tokens"
|
|
159
154
|
end
|
|
160
155
|
|
|
161
|
-
semaphore2.with_lease(5,
|
|
156
|
+
semaphore2.with_lease(5, timeout: 2) do
|
|
162
157
|
results << "client2_acquired_5_tokens"
|
|
163
158
|
sleep 0.05
|
|
164
159
|
results << "client2_releasing_5_tokens"
|
|
@@ -264,7 +259,7 @@ class RedisSemaphoreTest < Minitest::Test
|
|
|
264
259
|
)
|
|
265
260
|
|
|
266
261
|
begin
|
|
267
|
-
semaphore2.with_lease(1,
|
|
262
|
+
semaphore2.with_lease(1, timeout: 0.5) do
|
|
268
263
|
# This should not execute
|
|
269
264
|
end
|
|
270
265
|
rescue CountingSemaphore::LeaseTimeout
|
|
@@ -326,7 +321,7 @@ class RedisSemaphoreTest < Minitest::Test
|
|
|
326
321
|
)
|
|
327
322
|
|
|
328
323
|
begin
|
|
329
|
-
semaphore2.with_lease(1,
|
|
324
|
+
semaphore2.with_lease(1, timeout: 0.5) do
|
|
330
325
|
# This should not execute
|
|
331
326
|
end
|
|
332
327
|
rescue CountingSemaphore::LeaseTimeout => e
|
|
@@ -401,7 +396,7 @@ class RedisSemaphoreTest < Minitest::Test
|
|
|
401
396
|
)
|
|
402
397
|
|
|
403
398
|
begin
|
|
404
|
-
semaphore2.with_lease(
|
|
399
|
+
semaphore2.with_lease(timeout: 0.5) do # Uses default token count of 1
|
|
405
400
|
# This should not execute
|
|
406
401
|
end
|
|
407
402
|
rescue CountingSemaphore::LeaseTimeout
|
|
@@ -417,6 +412,37 @@ class RedisSemaphoreTest < Minitest::Test
|
|
|
417
412
|
assert client2_timeout_raised, "Expected client2 to raise LeaseTimeout but it didn't"
|
|
418
413
|
end
|
|
419
414
|
|
|
415
|
+
def test_with_lease_yields_lease_to_block
|
|
416
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
417
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(5, namespace, redis: Redis.new(db: REDIS_DB))
|
|
418
|
+
yielded_lease = nil
|
|
419
|
+
|
|
420
|
+
result = semaphore.with_lease(3) do |lease|
|
|
421
|
+
yielded_lease = lease
|
|
422
|
+
"block result"
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
assert_equal "block result", result
|
|
426
|
+
refute_nil yielded_lease
|
|
427
|
+
assert_instance_of CountingSemaphore::Lease, yielded_lease
|
|
428
|
+
assert_equal 3, yielded_lease.permits
|
|
429
|
+
assert_equal semaphore, yielded_lease.semaphore
|
|
430
|
+
assert_equal 5, semaphore.available_permits
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def test_with_lease_yields_nil_for_zero_permits
|
|
434
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
435
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(5, namespace, redis: Redis.new(db: REDIS_DB))
|
|
436
|
+
yielded_lease = :not_set
|
|
437
|
+
|
|
438
|
+
semaphore.with_lease(0) do |lease|
|
|
439
|
+
yielded_lease = lease
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
assert_nil yielded_lease
|
|
443
|
+
assert_equal 5, semaphore.available_permits
|
|
444
|
+
end
|
|
445
|
+
|
|
420
446
|
def test_currently_leased_returns_zero_initially
|
|
421
447
|
semaphore = CountingSemaphore::RedisSemaphore.new(5, "test_namespace")
|
|
422
448
|
assert_equal 0, semaphore.currently_leased
|
|
@@ -483,4 +509,392 @@ class RedisSemaphoreTest < Minitest::Test
|
|
|
483
509
|
assert_equal 0, semaphore1.currently_leased
|
|
484
510
|
assert_equal 0, semaphore2.currently_leased
|
|
485
511
|
end
|
|
512
|
+
|
|
513
|
+
# Tests for concurrent-ruby compatible API
|
|
514
|
+
|
|
515
|
+
def test_acquire_and_release_single_permit
|
|
516
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
517
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(3, namespace, redis: Redis.new(db: REDIS_DB))
|
|
518
|
+
|
|
519
|
+
assert_equal 3, semaphore.available_permits
|
|
520
|
+
|
|
521
|
+
lease = semaphore.acquire(1)
|
|
522
|
+
assert_equal 2, semaphore.available_permits
|
|
523
|
+
semaphore.release(lease)
|
|
524
|
+
assert_equal 3, semaphore.available_permits
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def test_acquire_and_release_multiple_permits
|
|
528
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
529
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(5, namespace, redis: Redis.new(db: REDIS_DB))
|
|
530
|
+
|
|
531
|
+
lease = semaphore.acquire(3)
|
|
532
|
+
assert_equal 2, semaphore.available_permits
|
|
533
|
+
semaphore.release(lease)
|
|
534
|
+
assert_equal 5, semaphore.available_permits
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def test_acquire_defaults_to_one_permit
|
|
538
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
539
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(3, namespace, redis: Redis.new(db: REDIS_DB))
|
|
540
|
+
|
|
541
|
+
lease = semaphore.acquire
|
|
542
|
+
assert_equal 2, semaphore.available_permits
|
|
543
|
+
|
|
544
|
+
semaphore.release(lease)
|
|
545
|
+
assert_equal 3, semaphore.available_permits
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
test_with_timeout "acquire blocks until permits available" do
|
|
549
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
550
|
+
completed_order = []
|
|
551
|
+
mutex = Mutex.new
|
|
552
|
+
|
|
553
|
+
# Thread 1: Acquires permits and holds them
|
|
554
|
+
thread1 = Thread.new do
|
|
555
|
+
semaphore1 = CountingSemaphore::RedisSemaphore.new(2, namespace, redis: Redis.new(db: REDIS_DB))
|
|
556
|
+
lease1 = semaphore1.acquire(2)
|
|
557
|
+
mutex.synchronize { completed_order << :thread1_acquired }
|
|
558
|
+
sleep(0.2) # Hold briefly
|
|
559
|
+
semaphore1.release(lease1)
|
|
560
|
+
mutex.synchronize { completed_order << :thread1_released }
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
sleep(0.1) # Let thread1 acquire
|
|
564
|
+
|
|
565
|
+
# Thread 2: Tries to acquire - should block until thread1 releases
|
|
566
|
+
thread2 = Thread.new do
|
|
567
|
+
semaphore2 = CountingSemaphore::RedisSemaphore.new(2, namespace, redis: Redis.new(db: REDIS_DB))
|
|
568
|
+
mutex.synchronize { completed_order << :thread2_attempting }
|
|
569
|
+
lease2 = semaphore2.acquire(1)
|
|
570
|
+
mutex.synchronize { completed_order << :thread2_acquired }
|
|
571
|
+
semaphore2.release(lease2)
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
thread1.join
|
|
575
|
+
thread2.join
|
|
576
|
+
|
|
577
|
+
# Thread2 should acquire after thread1 releases
|
|
578
|
+
assert_equal [:thread1_acquired, :thread2_attempting, :thread1_released, :thread2_acquired], completed_order
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def test_acquire_raises_error_for_invalid_permits
|
|
582
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
583
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(5, namespace, redis: Redis.new(db: REDIS_DB))
|
|
584
|
+
|
|
585
|
+
assert_raises(ArgumentError) do
|
|
586
|
+
semaphore.acquire(0)
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
assert_raises(ArgumentError) do
|
|
590
|
+
semaphore.acquire(-1)
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def test_acquire_raises_error_for_permits_exceeding_capacity
|
|
595
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
596
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(3, namespace, redis: Redis.new(db: REDIS_DB))
|
|
597
|
+
|
|
598
|
+
assert_raises(ArgumentError) do
|
|
599
|
+
semaphore.acquire(5)
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def test_release_raises_error_for_invalid_lease
|
|
604
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
605
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(5, namespace, redis: Redis.new(db: REDIS_DB))
|
|
606
|
+
|
|
607
|
+
assert_raises(NoMethodError) do
|
|
608
|
+
semaphore.release("not a lease")
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
assert_raises(NoMethodError) do
|
|
612
|
+
semaphore.release(nil)
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def test_release_raises_error_for_lease_from_different_semaphore
|
|
617
|
+
namespace1 = "test_semaphore_#{SecureRandom.uuid}"
|
|
618
|
+
namespace2 = "test_semaphore_#{SecureRandom.uuid}"
|
|
619
|
+
semaphore1 = CountingSemaphore::RedisSemaphore.new(5, namespace1, redis: Redis.new(db: REDIS_DB))
|
|
620
|
+
semaphore2 = CountingSemaphore::RedisSemaphore.new(5, namespace2, redis: Redis.new(db: REDIS_DB))
|
|
621
|
+
|
|
622
|
+
lease = semaphore1.acquire(1)
|
|
623
|
+
|
|
624
|
+
assert_raises(ArgumentError) do
|
|
625
|
+
semaphore2.release(lease)
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
semaphore1.release(lease)
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def test_try_acquire_succeeds_when_permits_available
|
|
632
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
633
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(3, namespace, redis: Redis.new(db: REDIS_DB))
|
|
634
|
+
|
|
635
|
+
lease = semaphore.try_acquire(2)
|
|
636
|
+
refute_nil lease
|
|
637
|
+
assert_equal 1, semaphore.available_permits
|
|
638
|
+
|
|
639
|
+
semaphore.release(lease)
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def test_try_acquire_fails_when_permits_not_available
|
|
643
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
644
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(2, namespace, redis: Redis.new(db: REDIS_DB))
|
|
645
|
+
|
|
646
|
+
lease1 = semaphore.acquire(2)
|
|
647
|
+
lease2 = semaphore.try_acquire(1)
|
|
648
|
+
assert_nil lease2
|
|
649
|
+
|
|
650
|
+
semaphore.release(lease1)
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def test_try_acquire_with_nil_timeout_returns_quickly
|
|
654
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
655
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(1, namespace, redis: Redis.new(db: REDIS_DB))
|
|
656
|
+
|
|
657
|
+
lease1 = semaphore.acquire(1)
|
|
658
|
+
start_time = Time.now
|
|
659
|
+
|
|
660
|
+
lease2 = semaphore.try_acquire(1, timeout: nil)
|
|
661
|
+
elapsed_time = Time.now - start_time
|
|
662
|
+
|
|
663
|
+
assert_nil lease2
|
|
664
|
+
assert elapsed_time < 0.5, "Should return quickly, took #{elapsed_time}s"
|
|
665
|
+
|
|
666
|
+
semaphore.release(lease1)
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
test_with_timeout "try_acquire with timeout waits for permits" do
|
|
670
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
671
|
+
|
|
672
|
+
# Thread 1: Holds the permit temporarily
|
|
673
|
+
thread1 = Thread.new do
|
|
674
|
+
semaphore1 = CountingSemaphore::RedisSemaphore.new(1, namespace, redis: Redis.new(db: REDIS_DB))
|
|
675
|
+
lease1 = semaphore1.acquire(1)
|
|
676
|
+
sleep(0.3)
|
|
677
|
+
semaphore1.release(lease1)
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
sleep(0.1) # Let thread1 acquire
|
|
681
|
+
|
|
682
|
+
# Thread 2: Tries to acquire with timeout - should eventually succeed
|
|
683
|
+
lease2 = nil
|
|
684
|
+
elapsed_time = 0
|
|
685
|
+
thread2 = Thread.new do
|
|
686
|
+
semaphore2 = CountingSemaphore::RedisSemaphore.new(1, namespace, redis: Redis.new(db: REDIS_DB))
|
|
687
|
+
start_time = Time.now
|
|
688
|
+
lease2 = semaphore2.try_acquire(1, timeout: 1.0)
|
|
689
|
+
elapsed_time = Time.now - start_time
|
|
690
|
+
semaphore2.release(lease2) if lease2
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
thread1.join
|
|
694
|
+
thread2.join
|
|
695
|
+
|
|
696
|
+
refute_nil lease2
|
|
697
|
+
assert elapsed_time >= 0.2, "Should have waited for release"
|
|
698
|
+
assert elapsed_time < 1.0, "Should not have waited full timeout"
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def test_try_acquire_with_timeout_fails_when_timeout_exceeded
|
|
702
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
703
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(1, namespace, redis: Redis.new(db: REDIS_DB))
|
|
704
|
+
|
|
705
|
+
lease1 = semaphore.acquire(1)
|
|
706
|
+
start_time = Time.now
|
|
707
|
+
lease2 = semaphore.try_acquire(1, timeout: 0.3)
|
|
708
|
+
elapsed_time = Time.now - start_time
|
|
709
|
+
|
|
710
|
+
assert_nil lease2
|
|
711
|
+
assert elapsed_time >= 0.3, "Should have waited for timeout"
|
|
712
|
+
|
|
713
|
+
semaphore.release(lease1)
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
def test_try_acquire_defaults_to_one_permit
|
|
717
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
718
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(3, namespace, redis: Redis.new(db: REDIS_DB))
|
|
719
|
+
|
|
720
|
+
lease = semaphore.try_acquire
|
|
721
|
+
refute_nil lease
|
|
722
|
+
assert_equal 2, semaphore.available_permits
|
|
723
|
+
|
|
724
|
+
semaphore.release(lease)
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
def test_try_acquire_raises_error_for_invalid_permits
|
|
728
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
729
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(5, namespace, redis: Redis.new(db: REDIS_DB))
|
|
730
|
+
|
|
731
|
+
assert_raises(ArgumentError) do
|
|
732
|
+
semaphore.try_acquire(0)
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
assert_raises(ArgumentError) do
|
|
736
|
+
semaphore.try_acquire(-1)
|
|
737
|
+
end
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
def test_available_permits_returns_correct_count
|
|
741
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
742
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(5, namespace, redis: Redis.new(db: REDIS_DB))
|
|
743
|
+
|
|
744
|
+
assert_equal 5, semaphore.available_permits
|
|
745
|
+
|
|
746
|
+
lease1 = semaphore.acquire(2)
|
|
747
|
+
assert_equal 3, semaphore.available_permits
|
|
748
|
+
|
|
749
|
+
lease2 = semaphore.acquire(1)
|
|
750
|
+
assert_equal 2, semaphore.available_permits
|
|
751
|
+
|
|
752
|
+
# Release all acquired permits
|
|
753
|
+
semaphore.release(lease2)
|
|
754
|
+
semaphore.release(lease1)
|
|
755
|
+
assert_equal 5, semaphore.available_permits
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
test_with_timeout "available_permits with concurrent operations" do
|
|
759
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
760
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(5, namespace, redis: Redis.new(db: REDIS_DB))
|
|
761
|
+
|
|
762
|
+
threads = 3.times.map do
|
|
763
|
+
Thread.new do
|
|
764
|
+
lease = semaphore.acquire(1)
|
|
765
|
+
sleep(0.1)
|
|
766
|
+
semaphore.release(lease)
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
sleep(0.05) # Let threads acquire
|
|
771
|
+
available = semaphore.available_permits
|
|
772
|
+
|
|
773
|
+
# Should have 2 or fewer available (3 threads acquired)
|
|
774
|
+
assert available <= 2, "Expected <= 2 available, got #{available}"
|
|
775
|
+
|
|
776
|
+
threads.each(&:join)
|
|
777
|
+
|
|
778
|
+
# All should be available now
|
|
779
|
+
assert_equal 5, semaphore.available_permits
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def test_drain_permits_acquires_all_available
|
|
783
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
784
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(5, namespace, redis: Redis.new(db: REDIS_DB))
|
|
785
|
+
|
|
786
|
+
drained_lease = semaphore.drain_permits
|
|
787
|
+
|
|
788
|
+
refute_nil drained_lease
|
|
789
|
+
assert_equal 5, drained_lease.permits
|
|
790
|
+
assert_equal 0, semaphore.available_permits
|
|
791
|
+
|
|
792
|
+
# Release them back
|
|
793
|
+
semaphore.release(drained_lease)
|
|
794
|
+
assert_equal 5, semaphore.available_permits
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
def test_drain_permits_acquires_only_available
|
|
798
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
799
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(5, namespace, redis: Redis.new(db: REDIS_DB))
|
|
800
|
+
|
|
801
|
+
lease1 = semaphore.acquire(2)
|
|
802
|
+
drained_lease = semaphore.drain_permits
|
|
803
|
+
|
|
804
|
+
refute_nil drained_lease
|
|
805
|
+
assert_equal 3, drained_lease.permits
|
|
806
|
+
assert_equal 0, semaphore.available_permits
|
|
807
|
+
|
|
808
|
+
# Release them back
|
|
809
|
+
semaphore.release(drained_lease)
|
|
810
|
+
semaphore.release(lease1)
|
|
811
|
+
assert_equal 5, semaphore.available_permits
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
def test_drain_permits_returns_nil_when_none_available
|
|
815
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
816
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(3, namespace, redis: Redis.new(db: REDIS_DB))
|
|
817
|
+
|
|
818
|
+
lease = semaphore.acquire(3)
|
|
819
|
+
drained_lease = semaphore.drain_permits
|
|
820
|
+
|
|
821
|
+
assert_nil drained_lease
|
|
822
|
+
assert_equal 0, semaphore.available_permits
|
|
823
|
+
semaphore.release(lease)
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
test_with_timeout "acquire release maintain correct count under stress" do
|
|
827
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
828
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(10, namespace, redis: Redis.new(db: REDIS_DB))
|
|
829
|
+
iterations = 10
|
|
830
|
+
|
|
831
|
+
threads = 5.times.map do
|
|
832
|
+
Thread.new do
|
|
833
|
+
iterations.times do
|
|
834
|
+
lease = semaphore.acquire(1)
|
|
835
|
+
# No sleep - stress test
|
|
836
|
+
semaphore.release(lease)
|
|
837
|
+
end
|
|
838
|
+
end
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
threads.each(&:join)
|
|
842
|
+
|
|
843
|
+
# All permits should be available
|
|
844
|
+
assert_equal 10, semaphore.available_permits
|
|
845
|
+
assert_equal 0, semaphore.currently_leased
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
test_with_timeout "concurrent ruby api compatibility pattern" do
|
|
849
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
850
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(3, namespace, redis: Redis.new(db: REDIS_DB))
|
|
851
|
+
results = []
|
|
852
|
+
mutex = Mutex.new
|
|
853
|
+
|
|
854
|
+
threads = 5.times.map do |i|
|
|
855
|
+
Thread.new do
|
|
856
|
+
if (lease = semaphore.try_acquire(1, timeout: 1.0))
|
|
857
|
+
begin
|
|
858
|
+
mutex.synchronize { results << i }
|
|
859
|
+
sleep(0.1)
|
|
860
|
+
ensure
|
|
861
|
+
semaphore.release(lease)
|
|
862
|
+
end
|
|
863
|
+
end
|
|
864
|
+
end
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
threads.each(&:join)
|
|
868
|
+
|
|
869
|
+
# All threads should complete without hanging
|
|
870
|
+
assert results.length > 0, "Expected at least some threads to succeed"
|
|
871
|
+
assert results.length <= 5, "Expected no more threads than total (5)"
|
|
872
|
+
assert_equal 3, semaphore.available_permits
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
test_with_timeout "distributed acquire and release across multiple instances" do
|
|
876
|
+
namespace = "test_semaphore_#{SecureRandom.uuid}"
|
|
877
|
+
semaphore1 = CountingSemaphore::RedisSemaphore.new(5, namespace, redis: Redis.new(db: REDIS_DB))
|
|
878
|
+
semaphore2 = CountingSemaphore::RedisSemaphore.new(5, namespace, redis: Redis.new(db: REDIS_DB))
|
|
879
|
+
|
|
880
|
+
# Acquire from first instance
|
|
881
|
+
lease1 = semaphore1.acquire(2)
|
|
882
|
+
assert_equal 3, semaphore1.available_permits
|
|
883
|
+
assert_equal 3, semaphore2.available_permits
|
|
884
|
+
|
|
885
|
+
# Acquire from second instance
|
|
886
|
+
lease2 = semaphore2.acquire(2)
|
|
887
|
+
assert_equal 1, semaphore1.available_permits
|
|
888
|
+
assert_equal 1, semaphore2.available_permits
|
|
889
|
+
|
|
890
|
+
# Release from first instance
|
|
891
|
+
semaphore1.release(lease1)
|
|
892
|
+
assert_equal 3, semaphore1.available_permits
|
|
893
|
+
assert_equal 3, semaphore2.available_permits
|
|
894
|
+
|
|
895
|
+
# Release from second instance
|
|
896
|
+
semaphore2.release(lease2)
|
|
897
|
+
assert_equal 5, semaphore1.available_permits
|
|
898
|
+
assert_equal 5, semaphore2.available_permits
|
|
899
|
+
end
|
|
486
900
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: counting_semaphore
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Julik Tarkhanov
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-10-
|
|
10
|
+
date: 2025-10-19 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: minitest
|
|
@@ -79,6 +79,20 @@ dependencies:
|
|
|
79
79
|
- - "~>"
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '2.4'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: sord
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: 0.0.0
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: 0.0.0
|
|
82
96
|
description: Provides both local (in-process) and shared (Redis-based) counting semaphores
|
|
83
97
|
for controlling concurrent access to resources
|
|
84
98
|
email:
|
|
@@ -93,7 +107,6 @@ files:
|
|
|
93
107
|
- ".ruby-version"
|
|
94
108
|
- AGENTS.md
|
|
95
109
|
- Gemfile
|
|
96
|
-
- Gemfile.lock
|
|
97
110
|
- LICENSE
|
|
98
111
|
- README.md
|
|
99
112
|
- Rakefile
|
|
@@ -101,8 +114,10 @@ files:
|
|
|
101
114
|
- lib/counting_semaphore/local_semaphore.rb
|
|
102
115
|
- lib/counting_semaphore/null_logger.rb
|
|
103
116
|
- lib/counting_semaphore/redis_semaphore.rb
|
|
104
|
-
- lib/counting_semaphore/shared_semaphore.rb
|
|
105
117
|
- lib/counting_semaphore/version.rb
|
|
118
|
+
- lib/counting_semaphore/with_lease_support.rb
|
|
119
|
+
- rbi/counting_semaphore.rbi
|
|
120
|
+
- sig/counting_semaphore.rbs
|
|
106
121
|
- test/counting_semaphore/local_semaphore_test.rb
|
|
107
122
|
- test/counting_semaphore/redis_semaphore_test.rb
|
|
108
123
|
- test/test_helper.rb
|
data/Gemfile.lock
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
PATH
|
|
2
|
-
remote: .
|
|
3
|
-
specs:
|
|
4
|
-
counting_semaphore (0.1.0)
|
|
5
|
-
|
|
6
|
-
GEM
|
|
7
|
-
remote: https://rubygems.org/
|
|
8
|
-
specs:
|
|
9
|
-
ast (2.4.3)
|
|
10
|
-
connection_pool (2.5.4)
|
|
11
|
-
json (2.15.1)
|
|
12
|
-
language_server-protocol (3.17.0.5)
|
|
13
|
-
lint_roller (1.1.0)
|
|
14
|
-
minitest (5.26.0)
|
|
15
|
-
parallel (1.27.0)
|
|
16
|
-
parser (3.3.9.0)
|
|
17
|
-
ast (~> 2.4.1)
|
|
18
|
-
racc
|
|
19
|
-
prism (1.6.0)
|
|
20
|
-
racc (1.8.1)
|
|
21
|
-
rainbow (3.1.1)
|
|
22
|
-
rake (13.3.0)
|
|
23
|
-
redis (5.4.1)
|
|
24
|
-
redis-client (>= 0.22.0)
|
|
25
|
-
redis-client (0.26.1)
|
|
26
|
-
connection_pool
|
|
27
|
-
regexp_parser (2.11.3)
|
|
28
|
-
rubocop (1.80.2)
|
|
29
|
-
json (~> 2.3)
|
|
30
|
-
language_server-protocol (~> 3.17.0.2)
|
|
31
|
-
lint_roller (~> 1.1.0)
|
|
32
|
-
parallel (~> 1.10)
|
|
33
|
-
parser (>= 3.3.0.2)
|
|
34
|
-
rainbow (>= 2.2.2, < 4.0)
|
|
35
|
-
regexp_parser (>= 2.9.3, < 3.0)
|
|
36
|
-
rubocop-ast (>= 1.46.0, < 2.0)
|
|
37
|
-
ruby-progressbar (~> 1.7)
|
|
38
|
-
unicode-display_width (>= 2.4.0, < 4.0)
|
|
39
|
-
rubocop-ast (1.47.1)
|
|
40
|
-
parser (>= 3.3.7.2)
|
|
41
|
-
prism (~> 1.4)
|
|
42
|
-
rubocop-performance (1.25.0)
|
|
43
|
-
lint_roller (~> 1.1)
|
|
44
|
-
rubocop (>= 1.75.0, < 2.0)
|
|
45
|
-
rubocop-ast (>= 1.38.0, < 2.0)
|
|
46
|
-
ruby-progressbar (1.13.0)
|
|
47
|
-
standard (1.51.1)
|
|
48
|
-
language_server-protocol (~> 3.17.0.2)
|
|
49
|
-
lint_roller (~> 1.0)
|
|
50
|
-
rubocop (~> 1.80.2)
|
|
51
|
-
standard-custom (~> 1.0.0)
|
|
52
|
-
standard-performance (~> 1.8)
|
|
53
|
-
standard-custom (1.0.2)
|
|
54
|
-
lint_roller (~> 1.0)
|
|
55
|
-
rubocop (~> 1.50)
|
|
56
|
-
standard-performance (1.8.0)
|
|
57
|
-
lint_roller (~> 1.1)
|
|
58
|
-
rubocop-performance (~> 1.25.0)
|
|
59
|
-
unicode-display_width (3.2.0)
|
|
60
|
-
unicode-emoji (~> 4.1)
|
|
61
|
-
unicode-emoji (4.1.0)
|
|
62
|
-
|
|
63
|
-
PLATFORMS
|
|
64
|
-
arm64-darwin-24
|
|
65
|
-
ruby
|
|
66
|
-
|
|
67
|
-
DEPENDENCIES
|
|
68
|
-
connection_pool (~> 2.4)
|
|
69
|
-
counting_semaphore!
|
|
70
|
-
minitest (~> 5.0)
|
|
71
|
-
rake (~> 13.0)
|
|
72
|
-
redis (~> 5.0)
|
|
73
|
-
standard (>= 1.35.1)
|
|
74
|
-
|
|
75
|
-
BUNDLED WITH
|
|
76
|
-
2.6.9
|