async icon indicating copy to clipboard operation
async copied to clipboard

Allow running an Async block inside a Ractor

Open beatmadsen opened this issue 4 years ago • 5 comments

Using Ruby 3.0.2 I'm trying the following in Async 1.30.1 and 2.0.0:

require 'async'
Ractor.new { Async { 42 } }

This is the distilled means of reproducing the issue, and the larger context is that I wish to run two Fiber schedulers in parallel, one for handling input I/O and one for output.

For both mentioned versions you get an exception because you can't access ENV from inside the non-main ractor.

Error message in 1.30.1 :

#<Thread:0x00007f9016131048 run> terminated with exception (report_on_exception is true):
/<gems path>/async-1.30.1/lib/async/reactor.rb:63:in `selector': can not access non-shareable objects in constant Async::Reactor::ENV by non-main Ractor. (Ractor::IsolationError)
	from /<gems path>/async-1.30.1/lib/async/reactor.rb:74:in `initialize'
	from /<gems path>/async-1.30.1/lib/async/reactor.rb:52:in `new'
	from /<gems path>/3.0.0/gems/async-1.30.1/lib/async/reactor.rb:52:in `run'
	from /<gems path>/async-1.30.1/lib/kernel/async.rb:28:in `Async'

Error message in 2.0.0:

#<Thread:0x00007fc7b496eff0 run> terminated with exception (report_on_exception is true):
/<gems path>/event-1.0.2/lib/event/selector.rb:30:in `default': can not access non-shareable objects in constant Event::Selector::ENV by non-main Ractor. (Ractor::IsolationError)
	from /<gems path>/event-1.0.2/lib/event/selector.rb:51:in `new'
	from /<gems path>/async-d43e99344446/lib/async/scheduler.rb:41:in `initialize'
	from /<gems path>/async-d43e99344446/lib/async/reactor.rb:32:in `initialize'

I can obviously work around the problem by using threads instead, and I'll also add that arguably this is more of a defect in the Ractor API, but on the other hand ENV is shared mutable state.

beatmadsen avatar Oct 10 '21 10:10 beatmadsen

One potential solution would be to allow the injection of a sharable duplicate of ENV like so:

Ractor.new(ENV) { |env| Async(env) { 42 } }

If you like that solution, I would be happy to put together a pull request.

beatmadsen avatar Oct 10 '21 10:10 beatmadsen

There are lots of places where ENV can provide an override from a default model, such as which event loop backend to use, etc. ENV is being used as a proxy for global defaults.

To be frank, I agree with your position that we should avoid using ENV because you are right it is shared mutable state. In addition to your points, mutating ENV is considered risky at best (thread unsafe) - but it's main advantage is a homogenous interface to control default implementation behaviours.

If we were to offer an alternative model, it would be something which still provided the same set of hooks but did so in a way which was Ractor compatible. For example:

module Event
  env_accessor :default_backend
  # would default to `EVENT_DEFAULT_BACKEND` or something.
end

We could do this at load time rather than lazy initialisation, but the down side to that is extra overhead at load time - maybe not a bad trade off. Then, we can define default_backend in a way that is Ractor compatible... but we might still run into the same issues.

ioquatix avatar Oct 10 '21 21:10 ioquatix

What about something like this:

#!/usr/bin/env ruby

module Kernel
	def env_accessor(name, key:, default: nil, &block)
		self.singleton_class.attr_accessor(name)
		
		if value = ENV[key]
			value = yield(value) if block_given?

			self.send(:"#{name}=", value)
		else
			self.send(:"#{name}=", default)
		end
	end
end

module Console
	env_accessor :output, key: 'CONSOLE_OUTPUT' do |value|
		value.split(',')
	end
end

pp Console.output

It resolves the environment variables at load time which should be more predictable. The only missing piece here is making the value immutable, of which the closest thing we have is Ractor.make_shareable.

ioquatix avatar Nov 17 '21 23:11 ioquatix

Very nice. Don't think you need the &block arg since you're never interacting with it.

beatmadsen avatar Dec 04 '21 13:12 beatmadsen

I believe ENV is now Ractor safe, so this isn't an issue. I tried your example and it failed in other areas. I made some small progress in those areas, and then ran into an issue I don't know how to fix (define_method with locally defined Proc instances).

ioquatix avatar Jun 04 '23 10:06 ioquatix