Skip to content

Commit

Permalink
Add ThreadLocalStorage abstraction to store thread-local data
Browse files Browse the repository at this point in the history
  • Loading branch information
markiz committed Mar 7, 2024
1 parent ad0dfa4 commit bcb0d5c
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 9 deletions.
1 change: 1 addition & 0 deletions lib/new_relic/agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module Agent
require 'new_relic/noticed_error'
require 'new_relic/agent/noticeable_error'
require 'new_relic/supportability_helper'
require 'new_relic/thread_local_storage'

require 'new_relic/agent/encoding_normalizer'
require 'new_relic/agent/stats'
Expand Down
3 changes: 1 addition & 2 deletions lib/new_relic/agent/threading/agent_thread.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ class AgentThread
def self.create(label, &blk)
::NewRelic::Agent.logger.debug("Creating AgentThread: #{label}")
wrapped_blk = proc do
if ::Thread.current[:newrelic_tracer_state] && Thread.current[:newrelic_tracer_state].current_transaction
txn = ::Thread.current[:newrelic_tracer_state].current_transaction
if (txn = ::NewRelic::ThreadLocalStorage[:newrelic_tracer_state]&.current_transaction)
::NewRelic::Agent.logger.warn("AgentThread created with current transaction #{txn.best_name}")
end
begin
Expand Down
10 changes: 5 additions & 5 deletions lib/new_relic/agent/tracer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -392,11 +392,11 @@ def start_message_broker_segment(action:,
#
# If ever exposed, this requires additional synchronization
def state_for(thread)
state = thread[:newrelic_tracer_state]
state = ThreadLocalStorage.get(thread, :newrelic_tracer_state)

if state.nil?
state = Tracer::State.new
thread[:newrelic_tracer_state] = state
ThreadLocalStorage.set(thread, :newrelic_tracer_state, state)
end

state
Expand All @@ -405,7 +405,7 @@ def state_for(thread)
alias_method :tl_state_for, :state_for

def clear_state
Thread.current[:newrelic_tracer_state] = nil
ThreadLocalStorage[:newrelic_tracer_state] = nil
end

alias_method :tl_clear, :clear_state
Expand All @@ -420,12 +420,12 @@ def thread_tracing_enabled?

def thread_block_with_current_transaction(segment_name: nil, parent: nil, &block)
parent ||= current_segment
current_txn = ::Thread.current[:newrelic_tracer_state]&.current_transaction if ::Thread.current[:newrelic_tracer_state]&.is_execution_traced?
current_txn = ThreadLocalStorage[:newrelic_tracer_state]&.current_transaction if ThreadLocalStorage[:newrelic_tracer_state]&.is_execution_traced?
proc do |*args|
begin
if current_txn && !current_txn.finished?
NewRelic::Agent::Tracer.state.current_transaction = current_txn
::Thread.current[:newrelic_thread_span_parent] = parent
ThreadLocalStorage[:newrelic_thread_span_parent] = parent
current_txn.async = true
segment_name = "#{segment_name}/Thread#{::Thread.current.object_id}/Fiber#{::Fiber.current.object_id}" if NewRelic::Agent.config[:'thread_ids_enabled']
segment = NewRelic::Agent::Tracer.start_segment(name: segment_name, parent: parent) if segment_name
Expand Down
4 changes: 2 additions & 2 deletions lib/new_relic/agent/transaction/tracing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ def add_segment(segment, parent = nil)

def thread_starting_span
# if the previous current segment was in another thread, use the thread local parent
if ::Thread.current[:newrelic_thread_span_parent] &&
if ThreadLocalStorage[:newrelic_thread_span_parent] &&
current_segment &&
current_segment.starting_segment_key != NewRelic::Agent::Tracer.current_segment_key

::Thread.current[:newrelic_thread_span_parent]
ThreadLocalStorage[:newrelic_thread_span_parent]
end
end

Expand Down
23 changes: 23 additions & 0 deletions lib/new_relic/thread_local_storage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# This file is distributed under New Relic's license terms.
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
# frozen_string_literal: true

module NewRelic
module ThreadLocalStorage
def self.get(thread, key)
thread[key]
end

def self.set(thread, key, value)
thread[key] = value
end

def self.[](key)
get(::Thread.current, key)
end

def self.[]=(key, value)
set(::Thread.current, key, value)
end
end
end
42 changes: 42 additions & 0 deletions test/new_relic/thread_local_storage_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# This file is distributed under New Relic's license terms.
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
# frozen_string_literal: true

require_relative '../test_helper'
require 'new_relic/thread_local_storage'

class NewRelic::ThreadLocalStorageTest < Minitest::Test
def test_basic_ops
assert_nil NewRelic::ThreadLocalStorage.get(Thread.current, :basic)
NewRelic::ThreadLocalStorage.set(Thread.current, :basic, 'foobar')

assert_equal('foobar', NewRelic::ThreadLocalStorage.get(Thread.current, :basic))
NewRelic::ThreadLocalStorage.set(Thread.current, :basic, 12345)

assert_equal(12345, NewRelic::ThreadLocalStorage.get(Thread.current, :basic))
end

def test_shortcut_ops
assert_nil NewRelic::ThreadLocalStorage[:shortcut]
NewRelic::ThreadLocalStorage[:shortcut] = 'baz'

assert_equal('baz', NewRelic::ThreadLocalStorage[:shortcut])
NewRelic::ThreadLocalStorage[:shortcut] = 98765

assert_equal(98765, NewRelic::ThreadLocalStorage[:shortcut])
end

def test_new_thread
NewRelic::ThreadLocalStorage[:new_thread] = :parent
thread = Thread.new do
assert_nil NewRelic::ThreadLocalStorage[:new_thread]
NewRelic::ThreadLocalStorage[:new_thread] = :child
sleep
end
sleep 0.2

assert_equal(:parent, NewRelic::ThreadLocalStorage[:new_thread])
assert_equal(:child, NewRelic::ThreadLocalStorage.get(thread, :new_thread))
thread.exit
end
end

0 comments on commit bcb0d5c

Please sign in to comment.