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
|
@@ -168,7 +168,7 @@ class LocalSemaphoreTest < Minitest::Test
|
|
|
168
168
|
semaphore = CountingSemaphore::LocalSemaphore.new(1)
|
|
169
169
|
|
|
170
170
|
# Test that timeout parameter is accepted
|
|
171
|
-
result = semaphore.with_lease(1,
|
|
171
|
+
result = semaphore.with_lease(1, timeout: 5) do
|
|
172
172
|
"success"
|
|
173
173
|
end
|
|
174
174
|
|
|
@@ -182,7 +182,7 @@ class LocalSemaphoreTest < Minitest::Test
|
|
|
182
182
|
semaphore.with_lease(1) do
|
|
183
183
|
# Try to acquire another token with a very short timeout
|
|
184
184
|
assert_raises(CountingSemaphore::LeaseTimeout) do
|
|
185
|
-
semaphore.with_lease(1,
|
|
185
|
+
semaphore.with_lease(1, timeout: 0.1) do
|
|
186
186
|
"should not reach here"
|
|
187
187
|
end
|
|
188
188
|
end
|
|
@@ -197,7 +197,7 @@ class LocalSemaphoreTest < Minitest::Test
|
|
|
197
197
|
semaphore.with_lease(1) do
|
|
198
198
|
# Try to acquire another token with a very short timeout
|
|
199
199
|
|
|
200
|
-
semaphore.with_lease(1,
|
|
200
|
+
semaphore.with_lease(1, timeout: 0.1) do
|
|
201
201
|
"should not reach here"
|
|
202
202
|
end
|
|
203
203
|
rescue CountingSemaphore::LeaseTimeout => e
|
|
@@ -251,6 +251,35 @@ class LocalSemaphoreTest < Minitest::Test
|
|
|
251
251
|
assert (Time.now - start_time) >= 0.1
|
|
252
252
|
end
|
|
253
253
|
|
|
254
|
+
def test_with_lease_yields_lease_to_block
|
|
255
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(5)
|
|
256
|
+
yielded_lease = nil
|
|
257
|
+
|
|
258
|
+
result = semaphore.with_lease(3) do |lease|
|
|
259
|
+
yielded_lease = lease
|
|
260
|
+
"block result"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
assert_equal "block result", result
|
|
264
|
+
refute_nil yielded_lease
|
|
265
|
+
assert_instance_of CountingSemaphore::Lease, yielded_lease
|
|
266
|
+
assert_equal 3, yielded_lease.permits
|
|
267
|
+
assert_equal semaphore, yielded_lease.semaphore
|
|
268
|
+
assert_equal 5, semaphore.available_permits
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def test_with_lease_yields_nil_for_zero_permits
|
|
272
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(5)
|
|
273
|
+
yielded_lease = :not_set
|
|
274
|
+
|
|
275
|
+
semaphore.with_lease(0) do |lease|
|
|
276
|
+
yielded_lease = lease
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
assert_nil yielded_lease
|
|
280
|
+
assert_equal 5, semaphore.available_permits
|
|
281
|
+
end
|
|
282
|
+
|
|
254
283
|
def test_currently_leased_returns_zero_initially
|
|
255
284
|
semaphore = CountingSemaphore::LocalSemaphore.new(5)
|
|
256
285
|
assert_equal 0, semaphore.currently_leased
|
|
@@ -301,4 +330,337 @@ class LocalSemaphoreTest < Minitest::Test
|
|
|
301
330
|
assert usage_values.any? { |usage| usage <= 3 }
|
|
302
331
|
assert_equal 0, semaphore.currently_leased
|
|
303
332
|
end
|
|
333
|
+
|
|
334
|
+
# Tests for Lease-based API
|
|
335
|
+
|
|
336
|
+
def test_acquire_returns_lease_object
|
|
337
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(3)
|
|
338
|
+
|
|
339
|
+
lease = semaphore.acquire(1)
|
|
340
|
+
assert_instance_of CountingSemaphore::Lease, lease
|
|
341
|
+
assert_equal semaphore, lease.semaphore
|
|
342
|
+
assert_equal 1, lease.permits
|
|
343
|
+
refute_nil lease.id
|
|
344
|
+
semaphore.release(lease)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def test_acquire_and_release_single_permit
|
|
348
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(3)
|
|
349
|
+
|
|
350
|
+
assert_equal 3, semaphore.available_permits
|
|
351
|
+
|
|
352
|
+
lease = semaphore.acquire(1)
|
|
353
|
+
assert_equal 2, semaphore.available_permits
|
|
354
|
+
semaphore.release(lease)
|
|
355
|
+
assert_equal 3, semaphore.available_permits
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def test_acquire_and_release_multiple_permits
|
|
359
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(5)
|
|
360
|
+
|
|
361
|
+
lease = semaphore.acquire(3)
|
|
362
|
+
assert_equal 2, semaphore.available_permits
|
|
363
|
+
semaphore.release(lease)
|
|
364
|
+
assert_equal 5, semaphore.available_permits
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def test_acquire_defaults_to_one_permit
|
|
368
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(3)
|
|
369
|
+
|
|
370
|
+
lease = semaphore.acquire
|
|
371
|
+
assert_equal 2, semaphore.available_permits
|
|
372
|
+
|
|
373
|
+
semaphore.release(lease)
|
|
374
|
+
assert_equal 3, semaphore.available_permits
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def test_acquire_blocks_until_permits_available
|
|
378
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(2)
|
|
379
|
+
completed_order = []
|
|
380
|
+
mutex = Mutex.new
|
|
381
|
+
|
|
382
|
+
# Fill the semaphore
|
|
383
|
+
lease = semaphore.acquire(2)
|
|
384
|
+
thread1 = Thread.new do
|
|
385
|
+
lease = semaphore.acquire(1)
|
|
386
|
+
mutex.synchronize { completed_order << :thread1 }
|
|
387
|
+
semaphore.release(lease)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
sleep(0.1) # Give thread1 time to block
|
|
391
|
+
|
|
392
|
+
thread2 = Thread.new do
|
|
393
|
+
mutex.synchronize { completed_order << :main_release }
|
|
394
|
+
semaphore.release(lease)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
thread1.join
|
|
398
|
+
thread2.join
|
|
399
|
+
|
|
400
|
+
# Thread1 should complete after the release
|
|
401
|
+
assert_equal [:main_release, :thread1], completed_order
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def test_acquire_raises_error_for_invalid_permits
|
|
405
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(5)
|
|
406
|
+
|
|
407
|
+
assert_raises(ArgumentError) do
|
|
408
|
+
semaphore.acquire(0)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
assert_raises(ArgumentError) do
|
|
412
|
+
semaphore.acquire(-1)
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def test_acquire_raises_error_for_permits_exceeding_capacity
|
|
417
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(3)
|
|
418
|
+
|
|
419
|
+
assert_raises(ArgumentError) do
|
|
420
|
+
semaphore.acquire(5)
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def test_release_raises_error_for_invalid_lease
|
|
425
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(5)
|
|
426
|
+
|
|
427
|
+
assert_raises(NoMethodError) do
|
|
428
|
+
semaphore.release("not a lease")
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
assert_raises(NoMethodError) do
|
|
432
|
+
semaphore.release(nil)
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def test_release_raises_error_for_lease_from_different_semaphore
|
|
437
|
+
semaphore1 = CountingSemaphore::LocalSemaphore.new(5)
|
|
438
|
+
semaphore2 = CountingSemaphore::LocalSemaphore.new(5)
|
|
439
|
+
|
|
440
|
+
lease = lease1 = semaphore1.acquire(1)
|
|
441
|
+
assert_raises(ArgumentError) do
|
|
442
|
+
semaphore2.release(lease)
|
|
443
|
+
end
|
|
444
|
+
semaphore1.release(lease1)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def test_try_acquire_succeeds_when_permits_available
|
|
448
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(3)
|
|
449
|
+
|
|
450
|
+
lease = semaphore.try_acquire(2)
|
|
451
|
+
refute_nil lease
|
|
452
|
+
assert_instance_of CountingSemaphore::Lease, lease
|
|
453
|
+
assert_equal 1, semaphore.available_permits
|
|
454
|
+
|
|
455
|
+
semaphore.release(lease)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def test_try_acquire_fails_when_permits_not_available
|
|
459
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(2)
|
|
460
|
+
|
|
461
|
+
lease = semaphore.acquire(2)
|
|
462
|
+
lease2 = semaphore.try_acquire(1)
|
|
463
|
+
assert_nil lease2
|
|
464
|
+
semaphore.release(lease)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def test_try_acquire_with_nil_timeout_returns_immediately
|
|
468
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(1)
|
|
469
|
+
|
|
470
|
+
lease1 = semaphore.acquire(1)
|
|
471
|
+
start_time = Time.now
|
|
472
|
+
lease2 = semaphore.try_acquire(1, timeout: nil)
|
|
473
|
+
elapsed_time = Time.now - start_time
|
|
474
|
+
|
|
475
|
+
assert_nil lease2
|
|
476
|
+
assert elapsed_time < 0.1, "Should return immediately, took #{elapsed_time}s"
|
|
477
|
+
|
|
478
|
+
semaphore.release(lease1)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def test_try_acquire_with_timeout_waits_for_permits
|
|
482
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(1)
|
|
483
|
+
|
|
484
|
+
lease = semaphore.acquire(1)
|
|
485
|
+
thread = Thread.new do
|
|
486
|
+
sleep(0.2)
|
|
487
|
+
semaphore.release(lease)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
start_time = Time.now
|
|
491
|
+
lease = semaphore.try_acquire(1, timeout: 0.5)
|
|
492
|
+
elapsed_time = Time.now - start_time
|
|
493
|
+
|
|
494
|
+
refute_nil lease
|
|
495
|
+
assert elapsed_time >= 0.2, "Should have waited for release"
|
|
496
|
+
assert elapsed_time < 0.5, "Should not have waited full timeout"
|
|
497
|
+
|
|
498
|
+
semaphore.release(lease)
|
|
499
|
+
thread.join
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def test_try_acquire_with_timeout_fails_when_timeout_exceeded
|
|
503
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(1)
|
|
504
|
+
|
|
505
|
+
lease1 = semaphore.acquire(1)
|
|
506
|
+
start_time = Time.now
|
|
507
|
+
|
|
508
|
+
lease2 = semaphore.try_acquire(1, timeout: 0.2)
|
|
509
|
+
elapsed_time = Time.now - start_time
|
|
510
|
+
|
|
511
|
+
assert_nil lease2
|
|
512
|
+
assert elapsed_time >= 0.2, "Should have waited for timeout"
|
|
513
|
+
|
|
514
|
+
semaphore.release(lease1)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def test_try_acquire_defaults_to_one_permit
|
|
518
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(3)
|
|
519
|
+
|
|
520
|
+
lease = semaphore.try_acquire
|
|
521
|
+
refute_nil lease
|
|
522
|
+
assert_equal 2, semaphore.available_permits
|
|
523
|
+
|
|
524
|
+
semaphore.release(lease)
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def test_try_acquire_raises_error_for_invalid_permits
|
|
528
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(5)
|
|
529
|
+
|
|
530
|
+
assert_raises(ArgumentError) do
|
|
531
|
+
semaphore.try_acquire(0)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
assert_raises(ArgumentError) do
|
|
535
|
+
semaphore.try_acquire(-1)
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def test_available_permits_returns_correct_count
|
|
540
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(5)
|
|
541
|
+
|
|
542
|
+
assert_equal 5, semaphore.available_permits
|
|
543
|
+
|
|
544
|
+
lease1 = semaphore.acquire(2)
|
|
545
|
+
assert_equal 3, semaphore.available_permits
|
|
546
|
+
|
|
547
|
+
lease2 = semaphore.acquire(1)
|
|
548
|
+
assert_equal 2, semaphore.available_permits
|
|
549
|
+
|
|
550
|
+
semaphore.release(lease2)
|
|
551
|
+
semaphore.release(lease1)
|
|
552
|
+
assert_equal 5, semaphore.available_permits
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def test_available_permits_with_concurrent_operations
|
|
556
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(5)
|
|
557
|
+
|
|
558
|
+
threads = 3.times.map do
|
|
559
|
+
Thread.new do
|
|
560
|
+
lease = semaphore.acquire(1)
|
|
561
|
+
sleep(0.1)
|
|
562
|
+
semaphore.release(lease)
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
sleep(0.05) # Let threads acquire
|
|
567
|
+
available = semaphore.available_permits
|
|
568
|
+
|
|
569
|
+
# Should have 2 or fewer available (3 threads acquired)
|
|
570
|
+
assert available <= 2, "Expected <= 2 available, got #{available}"
|
|
571
|
+
|
|
572
|
+
threads.each(&:join)
|
|
573
|
+
|
|
574
|
+
# All should be available now
|
|
575
|
+
assert_equal 5, semaphore.available_permits
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def test_drain_permits_acquires_all_available
|
|
579
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(5)
|
|
580
|
+
|
|
581
|
+
drained_lease = semaphore.drain_permits
|
|
582
|
+
|
|
583
|
+
refute_nil drained_lease
|
|
584
|
+
assert_equal 5, drained_lease.permits
|
|
585
|
+
assert_equal 0, semaphore.available_permits
|
|
586
|
+
|
|
587
|
+
# Release them back
|
|
588
|
+
semaphore.release(drained_lease)
|
|
589
|
+
assert_equal 5, semaphore.available_permits
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def test_drain_permits_acquires_only_available
|
|
593
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(5)
|
|
594
|
+
|
|
595
|
+
lease1 = semaphore.acquire(2)
|
|
596
|
+
drained_lease = semaphore.drain_permits
|
|
597
|
+
|
|
598
|
+
refute_nil drained_lease
|
|
599
|
+
assert_equal 3, drained_lease.permits
|
|
600
|
+
assert_equal 0, semaphore.available_permits
|
|
601
|
+
|
|
602
|
+
# Release them back
|
|
603
|
+
semaphore.release(drained_lease)
|
|
604
|
+
semaphore.release(lease1)
|
|
605
|
+
assert_equal 5, semaphore.available_permits
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def test_drain_permits_returns_nil_when_none_available
|
|
609
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(3)
|
|
610
|
+
|
|
611
|
+
lease = semaphore.acquire(3)
|
|
612
|
+
drained_lease = semaphore.drain_permits
|
|
613
|
+
|
|
614
|
+
assert_nil drained_lease
|
|
615
|
+
assert_equal 0, semaphore.available_permits
|
|
616
|
+
semaphore.release(lease)
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def test_acquire_release_maintain_correct_count_under_stress
|
|
620
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(10)
|
|
621
|
+
iterations = 50
|
|
622
|
+
|
|
623
|
+
threads = 10.times.map do
|
|
624
|
+
Thread.new do
|
|
625
|
+
iterations.times do
|
|
626
|
+
lease = semaphore.acquire(1)
|
|
627
|
+
# No sleep - stress test
|
|
628
|
+
semaphore.release(lease)
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
threads.each(&:join)
|
|
634
|
+
|
|
635
|
+
# All permits should be available
|
|
636
|
+
assert_equal 10, semaphore.available_permits
|
|
637
|
+
assert_equal 0, semaphore.currently_leased
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def test_concurrent_ruby_api_compatibility_pattern
|
|
641
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(3)
|
|
642
|
+
results = []
|
|
643
|
+
mutex = Mutex.new
|
|
644
|
+
|
|
645
|
+
threads = 5.times.map do |i|
|
|
646
|
+
Thread.new do
|
|
647
|
+
if (lease = semaphore.try_acquire(1, timeout: 1.0))
|
|
648
|
+
begin
|
|
649
|
+
mutex.synchronize { results << i }
|
|
650
|
+
sleep(0.1)
|
|
651
|
+
ensure
|
|
652
|
+
semaphore.release(lease)
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
threads.each(&:join)
|
|
659
|
+
|
|
660
|
+
# Only 3 threads should have succeeded (capacity is 3)
|
|
661
|
+
# But all should complete without hanging
|
|
662
|
+
assert results.length > 0, "Expected at least some threads to succeed"
|
|
663
|
+
assert results.length <= 5, "Expected no more threads than total (5)"
|
|
664
|
+
assert_equal 3, semaphore.available_permits
|
|
665
|
+
end
|
|
304
666
|
end
|