From: "nevans (Nicholas Evans)" <noreply@...>
Date: 2022-12-02T16:23:32+00:00
Subject: [ruby-core:111159] [Ruby master Feature#19078] Introduce `Fiber#storage` for inheritable fiber-scoped variables.

Issue #19078 has been updated by nevans (Nicholas Evans).


@Dan0042 (Daniel DeLorme) wrote in #note-21:
> What about when we want per-ractor storage... a fourth type?

FWIW, `Ractor#[]` has existed since 3.0: https://docs.ruby-lang.org/en/3.0/Ractor.html#method-i-5B-5D :)

> Per-ractor inheritable storage... a fifth type?

In my opinion, absolutely not.  The following is just a hypothetical--if and when I have an actual proposal, I'll post it in another ticket.  :)

This feature is brand-new so it has some "rough edges", but I think should be updated to automatically share all ractor-sharable values.  Of course, the big question is: what to do for non-ractor-sharable values?  Hypothetically:

1) By default, simple raise an exception from Ractor.new, just like it does any other time you accidentally attempt to send non-sharable state into a new Ractor.
2) Add a kwarg to Ractor.new, and a method to the fiber storage, both of which will simply drop all non-ractor sharable storage.  Either way: non-inheritance should always be an explicit opt-out.
3) Add an API for more fine-grained control at certain points, so that library authors can designate different behaviors via callback.  Because often the desired behavior is more complicated than simply dropping or inheriting a variable: e.g. rather than inherit the current task id, a new fiber/thread/ractor might automatically create a brand new task which is a child of the current task.

For inspiration here, we might look to Kotlin, which delegates a great deal of functionality to the current `coroutineContext[CoroutineDispatcher]` (this would be handled entirely by library code, and invisible to most users), to `coroutineContext[Job]` (which combines with `CoroutineScope` and a few others to handle almost everything related to structured concurrency), and any [ThreadContextElement] that's been added to the context.  That last one is mostly used for backward compatibility with libraries that expect thread-locals.

Similarly, go contexts delegate internally to `context.Value(cancelContextKey)`, where `cancelContextKey` is a private package var which is used purely for comparison-by-identity.  This is essentially the `@key = Object.new` trick demonstrated by @ioquatix in #note-10, and it's the recommended usage pattern for context keys in go.

[ThreadContextElement]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-thread-context-element/

Anyway: my goal here isn't to propose any particular approach, but to show this feature might be used as a foundation for other future features.  IMO, fine-grained inheritance control might be out-of-scope for 3.2 (at least, any API we put together now would need to be marked "experimental").  I simply wanted to show how other languages build on a similar foundation to handle the problems you mentioned.

----------------------------------------
Feature #19078: Introduce `Fiber#storage` for inheritable fiber-scoped variables.
https://bugs.ruby-lang.org/issues/19078#change-100443

* Author: ioquatix (Samuel Williams)
* Status: Closed
* Priority: Normal
* Assignee: ioquatix (Samuel Williams)
----------------------------------------
Pull Request: https://github.com/ruby/ruby/pull/6612

This is an evolution of the previous ideas:

- https://bugs.ruby-lang.org/issues/19058
- https://bugs.ruby-lang.org/issues/19062

This PR introduces fiber scoped variables, and is a solution for problems like <https://github.com/ioquatix/ioquatix/discussions/17>.

The main interface is:

```ruby
Fiber[key] = value
Fiber[key] # => value
```

The variables are scoped (local to) a fiber and inherited into child fibers and threads.

```ruby
Fiber[:request_id] = SecureRandom.hex(16)

Fiber.new do
  p Fiber[:request_id] # prints the above request id
end
```

The fiber scoped variables are stored and can be accessed:

```ruby
Fiber.current.storage # => returns a Hash (copy) of the internal storage.
Fiber.current.storage= # => assigns a Hash (copy) to the internal storage.
```

Fiber itself has one new keyword argument:

```
Fiber.new(..., storage: hash, false, undef, nil)
```

This can control how the fiber variables are setup in a child context.

To minimise the performance overhead of some of the implementation choices, we are also simultaneously implementing <https://bugs.ruby-lang.org/issues/19077>.

## Examples

### Request loop

```ruby
Thread.new do
  while request = queue.pop
    Fiber.new(storage: {id: SecureRandom.hex(16)}) do
      handle_request.call(request)
    end
  end
end
```

OR

```ruby
Thread.new do
  while request = queue.pop
    Fiber.current.storage = {id: SecureRandom.hex(16)}
    handle_request.call(request)
  end
end
```




-- 
https://bugs.ruby-lang.org/
 ______________________________________________
 ruby-core mailing list -- ruby-core@ml.ruby-lang.org
 To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org
 ruby-core info -- https://ml.ruby-lang.org/mailman3/postorius/lists/ruby-core.ml.ruby-lang.org/