Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow running an Async block inside a Ractor #128

Open
beatmadsen opened this issue Oct 10, 2021 · 5 comments
Open

Allow running an Async block inside a Ractor #128

beatmadsen opened this issue Oct 10, 2021 · 5 comments
Assignees

Comments

@beatmadsen
Copy link

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
Copy link
Author

beatmadsen commented Oct 10, 2021

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.

@ioquatix
Copy link
Member

ioquatix commented Oct 10, 2021

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 ioquatix self-assigned this Oct 13, 2021
@ioquatix
Copy link
Member

ioquatix commented Nov 17, 2021

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.

@beatmadsen
Copy link
Author

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

@ioquatix
Copy link
Member

ioquatix commented Jun 4, 2023

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants