class ShareLockTest

Public Instance Methods

setup() click to toggle source
# File activesupport/test/share_lock_test.rb, line 8
def setup
  @lock = ActiveSupport::Concurrency::ShareLock.new
end
test_compatible_exclusives_cooperate_to_both_proceed() click to toggle source
# File activesupport/test/share_lock_test.rb, line 275
def test_compatible_exclusives_cooperate_to_both_proceed
  ready = Concurrent::CyclicBarrier.new(2)
  done = Concurrent::CyclicBarrier.new(2)

  threads = 2.times.map do
    Thread.new do
      @lock.sharing do
        ready.wait
        @lock.exclusive(purpose: :x, compatible: [:x], after_compatible: [:x]) {}
        done.wait
      end
    end
  end

  assert_threads_not_stuck threads
end
test_exclusive_blocks_sharing() click to toggle source
# File activesupport/test/share_lock_test.rb, line 34
def test_exclusive_blocks_sharing
  with_thread_waiting_in_lock_section(:exclusive) do |exclusive_thread_release_latch|
    sharing_thread = Thread.new { @lock.sharing {} }
    assert_threads_stuck_but_releasable_by_latch sharing_thread, exclusive_thread_release_latch
  end
end
test_exclusive_conflicting_purpose() click to toggle source
# File activesupport/test/share_lock_test.rb, line 115
def test_exclusive_conflicting_purpose
  [true, false].each do |use_upgrading|
    with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
      begin
        together = Concurrent::CyclicBarrier.new(2)
        conflicting_exclusive_threads = [
          Thread.new do
            @lock.send(use_upgrading ? :sharing : :tap) do
              together.wait
              @lock.exclusive(purpose: :red, compatible: [:green, :purple]) {}
            end
          end,
          Thread.new do
            @lock.send(use_upgrading ? :sharing : :tap) do
              together.wait
              @lock.exclusive(purpose: :blue, compatible: [:green]) {}
            end
          end
        ]

        assert_threads_stuck conflicting_exclusive_threads # wait for threads to get into their respective `exclusive {}` blocks

        # This thread will be stuck as long as any other thread is in
        # a sharing block. While it's blocked, it holds no lock, so it
        # doesn't interfere with any other attempts.
        no_purpose_thread = Thread.new do
          @lock.exclusive {}
        end
        assert_threads_stuck no_purpose_thread

        # This thread is compatible with both of the "primary"
        # attempts above. It's initially stuck on the outer share
        # lock, but as soon as that's released, it can run --
        # regardless of whether those threads hold share locks.
        compatible_thread = Thread.new do
          @lock.exclusive(purpose: :green, compatible: []) {}
        end
        assert_threads_stuck compatible_thread

        assert_threads_stuck conflicting_exclusive_threads

        sharing_thread_release_latch.count_down

        assert_threads_not_stuck compatible_thread # compatible thread is now able to squeak through

        if use_upgrading
          # The "primary" threads both each hold a share lock, and are
          # mutually incompatible; they're still stuck.
          assert_threads_stuck conflicting_exclusive_threads

          # The thread without a specified purpose is also stuck; it's
          # not compatible with anything.
          assert_threads_stuck no_purpose_thread
        else
          # As the primaries didn't hold a share lock, as soon as the
          # outer one was released, all the exclusive locks are free
          # to be acquired in turn.

          assert_threads_not_stuck conflicting_exclusive_threads
          assert_threads_not_stuck no_purpose_thread
        end
      ensure
        conflicting_exclusive_threads.each(&:kill)
        no_purpose_thread.kill
      end
    end
  end
end
test_exclusive_matching_purpose() click to toggle source
# File activesupport/test/share_lock_test.rb, line 78
def test_exclusive_matching_purpose
  [true, false].each do |use_upgrading|
    with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
      exclusive_threads = (1..2).map do
        Thread.new do
          @lock.send(use_upgrading ? :sharing : :tap) do
            @lock.exclusive(purpose: :load, compatible: [:load, :unload]) {}
          end
        end
      end

      assert_threads_stuck_but_releasable_by_latch exclusive_threads, sharing_thread_release_latch
    end
  end
end
test_exclusive_ordering() click to toggle source
# File activesupport/test/share_lock_test.rb, line 184
def test_exclusive_ordering
  scratch_pad       = []
  scratch_pad_mutex = Mutex.new

  load_params   = [:load,   [:load]]
  unload_params = [:unload, [:unload, :load]]

  all_sharing = Concurrent::CyclicBarrier.new(4)

  [load_params, load_params, unload_params, unload_params].permutation do |thread_params|
    with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
      threads = thread_params.map do |purpose, compatible|
        Thread.new do
          @lock.sharing do
            all_sharing.wait
            @lock.exclusive(purpose: purpose, compatible: compatible) do
              scratch_pad_mutex.synchronize { scratch_pad << purpose }
            end
          end
        end
      end

      sleep(0.01)
      scratch_pad_mutex.synchronize { assert_empty scratch_pad }

      sharing_thread_release_latch.count_down

      assert_threads_not_stuck threads
      scratch_pad_mutex.synchronize do
        assert_equal [:load, :load, :unload, :unload], scratch_pad
        scratch_pad.clear
      end
    end
  end
end
test_exclusive_upgrade_waits_for_other_sharers_to_leave() click to toggle source
# File activesupport/test/share_lock_test.rb, line 62
def test_exclusive_upgrade_waits_for_other_sharers_to_leave
  with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
    in_sharing = Concurrent::CountDownLatch.new

    upgrading_thread = Thread.new do
      @lock.sharing do
        in_sharing.count_down
        @lock.exclusive {}
      end
    end

    in_sharing.wait
    assert_threads_stuck_but_releasable_by_latch upgrading_thread, sharing_thread_release_latch
  end
end
test_in_shared_section_incompatible_non_upgrading_threads_cannot_preempt_upgrading_threads() click to toggle source
# File activesupport/test/share_lock_test.rb, line 459
def test_in_shared_section_incompatible_non_upgrading_threads_cannot_preempt_upgrading_threads
  scratch_pad       = []
  scratch_pad_mutex = Mutex.new

  upgrading_load_params       = [:load,   [:load],          true]
  non_upgrading_unload_params = [:unload, [:load, :unload], false]

  [upgrading_load_params, non_upgrading_unload_params].permutation do |thread_params|
    with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
      threads = thread_params.map do |purpose, compatible, use_upgrading|
        Thread.new do
          @lock.send(use_upgrading ? :sharing : :tap) do
            @lock.exclusive(purpose: purpose, compatible: compatible) do
              scratch_pad_mutex.synchronize { scratch_pad << purpose }
            end
          end
        end
      end

      assert_threads_stuck threads
      scratch_pad_mutex.synchronize { assert_empty scratch_pad }

      sharing_thread_release_latch.count_down

      assert_threads_not_stuck threads
      scratch_pad_mutex.synchronize do
        assert_equal [:load, :unload], scratch_pad
        scratch_pad.clear
      end
    end
  end
end
test_killed_thread_loses_lock() click to toggle source
# File activesupport/test/share_lock_test.rb, line 94
def test_killed_thread_loses_lock
  with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
    thread = Thread.new do
      @lock.sharing do
        @lock.exclusive {}
      end
    end

    assert_threads_stuck thread
    thread.kill

    sharing_thread_release_latch.count_down

    thread = Thread.new do
      @lock.exclusive {}
    end

    assert_threads_not_stuck thread
  end
end
test_manual_incompatible_yield() click to toggle source
# File activesupport/test/share_lock_test.rb, line 318
def test_manual_incompatible_yield
  ready = Concurrent::CyclicBarrier.new(2)
  done = Concurrent::CyclicBarrier.new(2)

  threads = [
    Thread.new do
      @lock.sharing do
        ready.wait
        @lock.exclusive(purpose: :x) {}
        done.wait
      end
    end,

    Thread.new do
      @lock.sharing do
        ready.wait
        @lock.yield_shares(compatible: [:y]) do
          done.wait
        end
      end
    end,
  ]

  assert_threads_stuck threads
ensure
  threads.each(&:kill) if threads
end
test_manual_recursive_yield() click to toggle source
# File activesupport/test/share_lock_test.rb, line 346
def test_manual_recursive_yield
  ready = Concurrent::CyclicBarrier.new(2)
  done = Concurrent::CyclicBarrier.new(2)
  do_nesting = Concurrent::CountDownLatch.new

  threads = [
    Thread.new do
      @lock.sharing do
        ready.wait
        @lock.exclusive(purpose: :x) {}
        done.wait
      end
    end,

    Thread.new do
      @lock.sharing do
        @lock.yield_shares(compatible: [:x]) do
          @lock.sharing do
            ready.wait
            do_nesting.wait
            @lock.yield_shares(compatible: [:x, :y]) do
              done.wait
            end
          end
        end
      end
    end
  ]

  assert_threads_stuck threads
  do_nesting.count_down

  assert_threads_not_stuck threads
end
test_manual_recursive_yield_cannot_expand_outer_compatible() click to toggle source
# File activesupport/test/share_lock_test.rb, line 381
def test_manual_recursive_yield_cannot_expand_outer_compatible
  ready = Concurrent::CyclicBarrier.new(2)
  do_compatible_nesting = Concurrent::CountDownLatch.new
  in_compatible_nesting = Concurrent::CountDownLatch.new

  incompatible_thread = Thread.new do
    @lock.sharing do
      ready.wait
      @lock.exclusive(purpose: :x) {}
    end
  end

  yield_shares_thread = Thread.new do
    @lock.sharing do
      ready.wait
      @lock.yield_shares(compatible: [:y]) do
        do_compatible_nesting.wait
        @lock.sharing do
          @lock.yield_shares(compatible: [:x, :y]) do
            in_compatible_nesting.wait
          end
        end
      end
    end
  end

  assert_threads_stuck incompatible_thread
  do_compatible_nesting.count_down
  assert_threads_stuck incompatible_thread
  in_compatible_nesting.count_down
  assert_threads_not_stuck [yield_shares_thread, incompatible_thread]
end
test_manual_recursive_yield_restores_previous_compatible() click to toggle source
# File activesupport/test/share_lock_test.rb, line 414
def test_manual_recursive_yield_restores_previous_compatible
  ready = Concurrent::CyclicBarrier.new(2)
  do_nesting = Concurrent::CountDownLatch.new
  after_nesting = Concurrent::CountDownLatch.new

  incompatible_thread = Thread.new do
    ready.wait
    @lock.exclusive(purpose: :z) {}
  end

  recursive_yield_shares_thread = Thread.new do
    @lock.sharing do
      ready.wait
      @lock.yield_shares(compatible: [:y]) do
        do_nesting.wait
        @lock.sharing do
          @lock.yield_shares(compatible: [:x, :y]) {}
        end
        after_nesting.wait
      end
    end
  end

  assert_threads_stuck incompatible_thread
  do_nesting.count_down
  assert_threads_stuck incompatible_thread

  compatible_thread = Thread.new do
    @lock.exclusive(purpose: :y) {}
  end
  assert_threads_not_stuck compatible_thread

  post_nesting_incompatible_thread = Thread.new do
    @lock.exclusive(purpose: :x) {}
  end
  assert_threads_stuck post_nesting_incompatible_thread

  after_nesting.count_down
  assert_threads_not_stuck recursive_yield_shares_thread
  # post_nesting_incompatible_thread can now proceed
  assert_threads_not_stuck post_nesting_incompatible_thread
  # assert_threads_not_stuck can now proceed
  assert_threads_not_stuck incompatible_thread
end
test_manual_yield() click to toggle source
# File activesupport/test/share_lock_test.rb, line 292
def test_manual_yield
  ready = Concurrent::CyclicBarrier.new(2)
  done = Concurrent::CyclicBarrier.new(2)

  threads = [
    Thread.new do
      @lock.sharing do
        ready.wait
        @lock.exclusive(purpose: :x) {}
        done.wait
      end
    end,

    Thread.new do
      @lock.sharing do
        ready.wait
        @lock.yield_shares(compatible: [:x]) do
          done.wait
        end
      end
    end,
  ]

  assert_threads_not_stuck threads
end
test_multiple_exlusives_are_able_to_progress() click to toggle source
# File activesupport/test/share_lock_test.rb, line 41
def test_multiple_exlusives_are_able_to_progress
  with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
    exclusive_threads = (1..2).map do
      Thread.new do
        @lock.exclusive {}
      end
    end

    assert_threads_stuck_but_releasable_by_latch exclusive_threads, sharing_thread_release_latch
  end
end
test_new_share_attempts_block_on_waiting_exclusive() click to toggle source
# File activesupport/test/share_lock_test.rb, line 220
def test_new_share_attempts_block_on_waiting_exclusive
  with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
    release_exclusive = Concurrent::CountDownLatch.new

    waiting_exclusive = Thread.new do
      @lock.sharing do
        @lock.exclusive do
          release_exclusive.wait
        end
      end
    end
    assert_threads_stuck waiting_exclusive

    late_share_attempt = Thread.new do
      @lock.sharing {}
    end
    assert_threads_stuck late_share_attempt

    sharing_thread_release_latch.count_down
    assert_threads_stuck late_share_attempt

    release_exclusive.count_down
    assert_threads_not_stuck late_share_attempt
  end
end
test_reentrancy() click to toggle source
# File activesupport/test/share_lock_test.rb, line 12
def test_reentrancy
  thread = Thread.new do
    @lock.sharing   { @lock.sharing   {} }
    @lock.exclusive { @lock.exclusive {} }
  end
  assert_threads_not_stuck thread
end
test_share_remains_reentrant_ignoring_a_waiting_exclusive() click to toggle source
# File activesupport/test/share_lock_test.rb, line 246
def test_share_remains_reentrant_ignoring_a_waiting_exclusive
  with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
    ready = Concurrent::CyclicBarrier.new(2)
    attempt_reentrancy = Concurrent::CountDownLatch.new

    sharer = Thread.new do
      @lock.sharing do
        ready.wait
        attempt_reentrancy.wait
        @lock.sharing {}
      end
    end

    exclusive = Thread.new do
      @lock.sharing do
        ready.wait
        @lock.exclusive {}
      end
    end

    assert_threads_stuck exclusive

    attempt_reentrancy.count_down

    assert_threads_not_stuck sharer
    assert_threads_stuck exclusive
  end
end
test_sharing_blocks_exclusive() click to toggle source
# File activesupport/test/share_lock_test.rb, line 26
def test_sharing_blocks_exclusive
  with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
    @lock.exclusive(no_wait: true) { flunk } # polling should fail
    exclusive_thread = Thread.new { @lock.exclusive {} }
    assert_threads_stuck_but_releasable_by_latch exclusive_thread, sharing_thread_release_latch
  end
end
test_sharing_doesnt_block() click to toggle source
# File activesupport/test/share_lock_test.rb, line 20
def test_sharing_doesnt_block
  with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_latch|
    assert_threads_not_stuck(Thread.new { @lock.sharing {} })
  end
end
test_sharing_is_upgradeable_to_exclusive() click to toggle source
# File activesupport/test/share_lock_test.rb, line 53
def test_sharing_is_upgradeable_to_exclusive
  upgrading_thread = Thread.new do
    @lock.sharing do
      @lock.exclusive {}
    end
  end
  assert_threads_not_stuck upgrading_thread
end

Private Instance Methods

with_thread_waiting_in_lock_section(lock_section) { |section_release| ... } click to toggle source
# File activesupport/test/share_lock_test.rb, line 562
def with_thread_waiting_in_lock_section(lock_section)
  in_section      = Concurrent::CountDownLatch.new
  section_release = Concurrent::CountDownLatch.new

  stuck_thread = Thread.new do
    @lock.send(lock_section) do
      in_section.count_down
      section_release.wait
    end
  end

  in_section.wait

  yield section_release
ensure
  section_release.count_down
  stuck_thread.join # clean up
end