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.
@@ -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, timeout_seconds: 5) do
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, timeout_seconds: 0.1) do
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, timeout_seconds: 0.1) do
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