Home > Mobile >  Thread-local instance-local variable in Ruby
Thread-local instance-local variable in Ruby

Time:04-20

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.

  •  Tags:  
  • ruby
  • Related