From: "ioquatix (Samuel Williams)" Date: 2022-10-18T11:20:19+00:00 Subject: [ruby-core:110397] [Ruby master Bug#19062] Introduce `Fiber#locals` for shared inheritable state. Issue #19062 has been updated by ioquatix (Samuel Williams). Tracker changed from Feature to Bug Backport set to 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN > It's extremely negligible. Okay, good to know. ```ruby require 'benchmark' locals = {a: 1, b: 2, c: 3} Benchmark.bm do |b| b.report("dup") {1_000_000.times{x = locals.dup}} b.report("assign") {1_000_000.times{x = locals}} end user system total real dup 0.175779 0.001153 0.176932 ( 0.176988) assign 0.023896 0.000084 0.023980 ( 0.023998) ``` So, like, about 10x slower than doing nothing, but it's also a pretty small overhead per fiber, so it might be acceptable to describe it as negligible, unless there are worse cases we should consider. That being said, I'd prefer to avoid doing things we don't need to do. > Should a child fiber be able to flip the log level in its parent and sibblings? Definitely not. If all fibers are part of the same request, I have no problem with that model. This proposal is two parts: a way to inherit state to child execution contexts, and a way to share that state between execution contexts within the same request or operation. > No, again, new fibers are initialized with a dup of the locals hash, so they have access to the same ConnectionPool instance, which can be mutable if you desire so. Not if it wasn't created in the parent first. If you have code to lazy initialize the connection pool, and it happens in the child fiber before it happens in the parent, it won't be shared if the locals are duped. > I really don't see how. I gave an example showing exactly this. ``` Fiber[:count] = 0 enumerator = Enumerator.new do |y| 10.times do |i| Fiber[:count] += 1 y << i end end enumerator.next p Fiber[:count] # 1 enumerator.to_a p Fiber[:count] # 11 ``` If the locals are duped, the hidden fiber will dup the locals and the results will be 0 and 10 respectively. To me, this is confusing, since the fiber is an implementation detail. One of the problems I'm trying to solve is behavioural changes due to the internal fiber of enumerator. Can you propose some other way to fix this? > If you don't dup, there this is absolutely not "locals" it's just some fairly contrived global variable. I mean, if you want to do contrived, everything is basically just a global variable if you squint hard enough :) > It is thread unsafe and could crash other Ruby implementations for instance. It's not thread unsafe to share a hash table between fibers. In my PR, the locals are duped between threads to prevent any thread safety issues. > This badly breaks the application because the fd is incorrectly shared between threads. I like this example, probably sharing across thread boundaries is dangerous. The solution you proposed feels far to complex for Ruby. The model I'm inspired by is "dynamically scoped free variables" such as those from LISP. I think that's a pretty good model, but it's still a little too cumbersome for Ruby. > I guess the name is the fundamental problem here. I believe what you propose is not related to what most people understand by "Fiber locals". Yes, it can be. Actually, the entire way thread local and fiber local is named and used in Ruby is a total mess. > BTW Java has: https://openjdk.org/jeps/429 is a nice proposal I linked in the other issue. If everyone feels strongly about `dup` in every case, I'm okay with it (assuming the performance is acceptable), but it means that Enumerator's internal fiber has user visible side effects, which was something I wanted to avoid as it's caused a lot of pain in the past. Maybe Fibers can default to with `dup` but enumerator's hidden fiber can default to without dup to avoid this problem. What do you think? ---------------------------------------- Bug #19062: Introduce `Fiber#locals` for shared inheritable state. https://bugs.ruby-lang.org/issues/19062#change-99704 * Author: ioquatix (Samuel Williams) * Status: Open * Priority: Normal * Assignee: ioquatix (Samuel Williams) * Backport: 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN ---------------------------------------- After exploring , I felt uncomfortable about the performance of copying lots of inheritable attributes. Please review that issue for the background and summary of the problem. ## Proposal Introduce `Fiber#locals` which is a hash table of local attributes which are inherited by child fibers. ```ruby Fiber.current.locals[:x] = 10 Fiber.new do pp Fiber.current.locals[:x] # => 10 end ``` It's possible to reset `Fiber.current.locals`, e.g. ```ruby def accept_connection(peer) Fiber.new(locals: nil) do # This causes a new hash table to be allocated. # Generate a new request id for all fibers nested in this one: Fiber[:request_id] = SecureRandom.hex(32) @app.call(env) end.resume end ``` A high level overview of the proposed changes: ```ruby class Fiber def initialize(..., locals: Fiber.current.locals) @locals = locals || Hash.new end attr_accessor :locals def self.[] key self.current.locals[key] end def self.[]= key, value self.current.locals[key] = value end end ``` See the pull request for the full proposed implementation. ## Expected Usage Currently, a lot of libraries use `Thread.current[:x]` which is unexpectedly "fiber local". A common bug shows up when lazy enumerators are used, because it may create an internal fiber. Because `locals` are inherited, code which uses `Fiber[:x]` will not suffer from this problem. Any program that uses true thread locals for per-request state, can adopt the proposed `Fiber#locals` and get similar behaviour, without breaking on per-fiber servers like Falcon, because Falcon can "reset" `Fiber.current.locals` for each request fiber, while servers like Puma won't have to do that and will retain thread-local behaviour. Libraries like ActiveRecord can adopt `Fiber#locals` to avoid the need for users to opt into different "IsolatedExecutionState" models, since it can be transparently handled by the web server (see for more details). We hope by introducing `Fiber#locals`, we can avoid all the confusion and bugs of the past designs. -- https://bugs.ruby-lang.org/ Unsubscribe: