I would like a thread-local (in the context of a Rails app, per request), instance local var, by way of example imagine I have instance of an object which is globally accessible.
module Event
def self.bus
@bus ||= Bus.new
end
end
The bus
object can have metadata set per request:
class Bus
def with_metadata(metadata, &block)
@metadata = metadata
block.call
end
# ...
end
The problem is that in a threaded environment, like a Rails application on Puma, @metadata
is shared globally between the different threads (i.e. requests), so if the metadata contains an IP address, for example, it will be incorrect for only some requests.
We could use Thread.current
which is thread local:
class Bus
def with_metadata(metadata, &block)
Thread.current["with_metadata"] = metadata
block.call
ensure
Thread.current["with_metadata"] = nil
end
# ...
end
However this introduces a problem, if another instance of the bus object is initialized in the same thread, i.e. Event.bus
and Bus.new
, they incorrectly share the "with_metadata" state.
My thought was to include the object_id
in the key:
class Bus
def with_metadata(metadata, &block)
Thread.current["#{object_id}_with_metadata"] = metadata
block.call
ensure
Thread.current["#{object_id}_with_metadata"] = nil
end
# ...
end
Does this sound reasonable, is there an idiomatic way of having a thread-local, instance-local var? I do not want a dependency on Rails, or any other library.
CodePudding user response:
This may be what you are looking for https://github.com/steveklabnik/request_store
CodePudding user response:
Rails depends on the concurrent-ruby gem which brings you a lot of tools to deal with multi-threaded and concurrent environments.
Specifically, you can use the Concurrent::ThreadLocalVar
class here:
class Bus
def initialize
@metadata = Concurrent::ThreadLocalVar.new
end
def metadata
@metadata.value
end
def with_metadata(metadata, &block)
@metadata.value = metadata
yield
ensure
@metadata.value = nil
end
# ...
end
Please refer to the documentation linked above for more usage examples.