socketry / async

An awesome asynchronous event-driven reactor for Ruby.
MIT License
2.09k stars 86 forks source link

Rspec spec does not terminate #164

Closed klobuczek closed 1 year ago

klobuczek commented 2 years ago

I cannot figure out why the spec in the 2nd context does not terminate:

RSpec.describe Spec do
  context 'terminates' do
    let(:s) { Sync { 1 } }

    it 'eq' do
      expect(s).to eq 1
    end
  end

  context 'does not terminate' do
    let(:s) { Sync { param } }
    let(:param) { 1 }

    it 'eq' do
      expect(s).to eq 1
    end
  end
end

Using ruby 3.1.2 and async 2.0.2.

ioquatix commented 2 years ago

IIRC, this is because let blocks are "thread safe" and have an implicit shared mutex around every request. It's supposed to be reentrant but because Sync { param } happens on a different fiber, it's effectively a deadlock, i.e. it's not that different from:

m = Thread::Monitor.new # recursive mutex

param = lambda{m.synchronize{1}}
s = lambda{m.synchronise{Thread.new{param.call}.value}}

s.call

To resolve s it locks the mutex for the current (main) thread. Then it creates a new thread (Sync block creates a new fiber) and then tries to lock the mutex again and this deadlocks.

The real issue here is whether RSpec should be "thread safe" in this way. I personally think it's wrong.

ioquatix commented 1 year ago

I got pretty frustrated with RSpec for this reason as well as others, and implemented https://github.com/ioquatix/sus which explicitly avoids this problem. I don't have a good answer and can't fix RSpec, maybe they will avoid having a mutex around their let variables.