From 60aa1a086f417945182aa3d95ef89919d876ac33 Mon Sep 17 00:00:00 2001 From: Francis Bogsanyi Date: Wed, 5 Feb 2025 18:52:01 -0500 Subject: [PATCH 1/9] Use a Fiber attribute for Context --- api/lib/opentelemetry/context.rb | 13 +++-- api/test/opentelemetry/context_test.rb | 66 ++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/api/lib/opentelemetry/context.rb b/api/lib/opentelemetry/context.rb index 05e96ed6bd..83a6fa8d51 100644 --- a/api/lib/opentelemetry/context.rb +++ b/api/lib/opentelemetry/context.rb @@ -8,11 +8,12 @@ require 'opentelemetry/context/propagation' module OpenTelemetry + Fiber.attr_accessor :opentelemetry_context + # Manages context on a per-fiber basis class Context EMPTY_ENTRIES = {}.freeze - STACK_KEY = :__opentelemetry_context_storage__ - private_constant :EMPTY_ENTRIES, :STACK_KEY + private_constant :EMPTY_ENTRIES DetachError = Class.new(OpenTelemetry::Error) @@ -113,11 +114,9 @@ def value(key) current.value(key) end - # Clears the fiber-local Context stack. This allocates a new array for the - # stack, which is important in some use-cases to avoid sharing the backing - # array between fibers. + # Clears the fiber-local Context stack. def clear - Thread.current[STACK_KEY] = [] + Fiber.current.opentelemetry_context = [] end def empty @@ -127,7 +126,7 @@ def empty private def stack - Thread.current[STACK_KEY] ||= [] + Fiber.current.opentelemetry_context ||= [] end end diff --git a/api/test/opentelemetry/context_test.rb b/api/test/opentelemetry/context_test.rb index eb91a907b1..6d86d26871 100644 --- a/api/test/opentelemetry/context_test.rb +++ b/api/test/opentelemetry/context_test.rb @@ -289,7 +289,73 @@ end end + describe 'fibers' do + it 'is isolated with respect to Fiber-local variable manipulation' do + OpenTelemetry::TestHelpers.with_test_logger do |log_stream| + ctx = new_context + Context.with_current(ctx) do + # This is based on code in ActionController::Live#process: + # https://github.com/rails/rails/blob/ad0105c13f61d145a659004efb928a643104e973/actionpack/lib/action_controller/metal/live.rb#L270 + t1 = Thread.current + locals = t1.keys.map { |key| [key, t1[key]] } + Fiber.new do + t2 = Thread.current + locals.each { |k, v| t2[k] = v } + # Manipulate _this fiber's_ context stack. + Context.attach(new_context) + ensure + locals.each { |k, _| t2[k] = nil } + end.resume + end + _(log_stream.string).must_be_empty + end + end + + it 'is isolated with respect to Fiber-local storage manipulation' do + OpenTelemetry::TestHelpers.with_test_logger do |log_stream| + ctx = new_context + Context.with_current(ctx) do + # This is based on code in ActionController::Live#process, modified to use Fiber-local storage: + # https://github.com/rails/rails/blob/ad0105c13f61d145a659004efb928a643104e973/actionpack/lib/action_controller/metal/live.rb#L270 + f1 = Fiber.current + Fiber[:foo] = :bar + locals = f1.storage + Fiber.new do + f2 = Fiber.current + locals.each { |k, v| f2.storage[k] = v } + # Manipulate _this fiber's_ context stack. + Context.attach(new_context) + ensure + locals.each { |k, _| f2.storage[k] = nil } + end.resume + end + _(log_stream.string).must_be_empty + end + end + end + describe 'threading' do + it 'is isolated with respect to Fiber-local variable manipulation' do + OpenTelemetry::TestHelpers.with_test_logger do |log_stream| + ctx = new_context + Context.with_current(ctx) do + # This is based on code in ActionController::Live#process: + # https://github.com/rails/rails/blob/ad0105c13f61d145a659004efb928a643104e973/actionpack/lib/action_controller/metal/live.rb#L270 + t1 = Thread.current + locals = t1.keys.map { |key| [key, t1[key]] } + Thread.new do + t2 = Thread.current + locals.each { |k, v| t2[k] = v } + # Manipulate _this thread's_ context stack. + Context.attach(new_context) + ensure + locals.each { |k, _| t2[k] = nil } + end.join + end + _(log_stream.string).must_be_empty + end + end + it 'unwinds the stack on each thread' do ctx = new_context t1_ctx_before = Context.current From 4bfc03fb3413cbc0c2f8cc4d835f09d55261b369 Mon Sep 17 00:00:00 2001 From: Francis Bogsanyi Date: Thu, 6 Feb 2025 16:42:54 -0500 Subject: [PATCH 2/9] Add benchmarks --- api/benchmarks/context_bench.rb | 994 ++++++++++++++++++++++++++++++++ api/opentelemetry-api.gemspec | 1 + 2 files changed, 995 insertions(+) create mode 100644 api/benchmarks/context_bench.rb diff --git a/api/benchmarks/context_bench.rb b/api/benchmarks/context_bench.rb new file mode 100644 index 0000000000..fa9616f85d --- /dev/null +++ b/api/benchmarks/context_bench.rb @@ -0,0 +1,994 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'benchmark/ipsa' +require 'concurrent-ruby' +require 'opentelemetry' + +class FiberLocalVarContext + EMPTY_ENTRIES = {}.freeze + VAR = Concurrent::FiberLocalVar.new { [] } + private_constant :EMPTY_ENTRIES, :VAR + + DetachError = Class.new(StandardError) + + class << self + def current + stack.last || ROOT + end + + def attach(context) + s = stack + s.push(context) + s.size + end + + def detach(token) + s = stack + calls_matched = (token == s.size) + OpenTelemetry.handle_error(exception: DetachError.new('calls to detach should match corresponding calls to attach.')) unless calls_matched + + s.pop + calls_matched + end + + def with_current(ctx) + token = attach(ctx) + yield ctx + ensure + detach(token) + end + + def with_value(key, value) + ctx = current.set_value(key, value) + token = attach(ctx) + yield ctx, value + ensure + detach(token) + end + + def with_values(values) + ctx = current.set_values(values) + token = attach(ctx) + yield ctx, values + ensure + detach(token) + end + + def value(key) + current.value(key) + end + + def clear + stack.clear + end + + def empty + new(EMPTY_ENTRIES) + end + + private + + def stack + VAR.value + end + end + + def initialize(entries) + @entries = entries.freeze + end + + def value(key) + @entries[key] + end + + alias [] value + + def set_value(key, value) + new_entries = @entries.dup + new_entries[key] = value + FiberLocalVarContext.new(new_entries) + end + + def set_values(values) # rubocop:disable Naming/AccessorMethodName: + FiberLocalVarContext.new(@entries.merge(values)) + end + + ROOT = empty.freeze +end + +Fiber.attr_accessor :opentelemetry_context + +class FiberAttributeContext + EMPTY_ENTRIES = {}.freeze + private_constant :EMPTY_ENTRIES + + DetachError = Class.new(StandardError) + + class << self + def current + stack.last || ROOT + end + + def attach(context) + s = stack + s.push(context) + s.size + end + + def detach(token) + s = stack + calls_matched = (token == s.size) + OpenTelemetry.handle_error(exception: DetachError.new('calls to detach should match corresponding calls to attach.')) unless calls_matched + + s.pop + calls_matched + end + + def with_current(ctx) + token = attach(ctx) + yield ctx + ensure + detach(token) + end + + def with_value(key, value) + ctx = current.set_value(key, value) + token = attach(ctx) + yield ctx, value + ensure + detach(token) + end + + def with_values(values) + ctx = current.set_values(values) + token = attach(ctx) + yield ctx, values + ensure + detach(token) + end + + def value(key) + current.value(key) + end + + def clear + stack.clear + end + + def empty + new(EMPTY_ENTRIES) + end + + private + + def stack + Fiber.current.opentelemetry_context ||= [] + end + end + + def initialize(entries) + @entries = entries.freeze + end + + def value(key) + @entries[key] + end + + alias [] value + + def set_value(key, value) + new_entries = @entries.dup + new_entries[key] = value + FiberAttributeContext.new(new_entries) + end + + def set_values(values) # rubocop:disable Naming/AccessorMethodName: + FiberAttributeContext.new(@entries.merge(values)) + end + + ROOT = empty.freeze +end + +class LinkedListContext + EMPTY_ENTRIES = {}.freeze + STACK_KEY = :__linked_list_context_storage__ + + class Token + def initialize(context, next_token) + @context = context + @next_token = next_token + end + + def context = @context + def next_token = @next_token + end + + private_constant :EMPTY_ENTRIES, :STACK_KEY, :Token + + DetachError = Class.new(StandardError) + + class << self + def current + Thread.current[STACK_KEY]&.context || ROOT + end + + def attach(context) + next_token = Thread.current[STACK_KEY] + token = Token.new(context, next_token) + Thread.current[STACK_KEY] = token + token + end + + def detach(token) + current = Thread.current[STACK_KEY] + calls_matched = (token == current) + OpenTelemetry.handle_error(exception: DetachError.new('calls to detach should match corresponding calls to attach.')) unless calls_matched + + Thread.current[STACK_KEY] = current&.next_token + calls_matched + end + + def with_current(ctx) + token = attach(ctx) + yield ctx + ensure + detach(token) + end + + def with_value(key, value) + ctx = current.set_value(key, value) + token = attach(ctx) + yield ctx, value + ensure + detach(token) + end + + def with_values(values) + ctx = current.set_values(values) + token = attach(ctx) + yield ctx, values + ensure + detach(token) + end + def value(key) + current.value(key) + end + + def clear + Thread.current[STACK_KEY] = nil + end + + def empty + new(EMPTY_ENTRIES) + end + end + + def initialize(entries) + @entries = entries.freeze + end + + def value(key) + @entries[key] + end + + alias [] value + + def set_value(key, value) + new_entries = @entries.dup + new_entries[key] = value + LinkedListContext.new(new_entries) + end + + def set_values(values) # rubocop:disable Naming/AccessorMethodName: + LinkedListContext.new(@entries.merge(values)) + end + + ROOT = empty.freeze +end + +class FiberLocalLinkedListContext < Hash + EMPTY_ENTRIES = {}.freeze + STACK_KEY = :__fiber_local_linked_list_context_storage__ + + class Token + def initialize(context, next_token) + @context = context + @next_token = next_token + end + + def context = @context + def next_token = @next_token + end + + private_constant :EMPTY_ENTRIES, :STACK_KEY, :Token + + DetachError = Class.new(StandardError) + + class << self + def current + Fiber[STACK_KEY]&.context || ROOT + end + + def attach(context) + Fiber[STACK_KEY] = Token.new(context, Fiber[STACK_KEY]) + end + + def detach(token) + current = Fiber[STACK_KEY] + calls_matched = (token == current) + OpenTelemetry.handle_error(exception: DetachError.new('calls to detach should match corresponding calls to attach.')) unless calls_matched + + Fiber[STACK_KEY] = current&.next_token + calls_matched + end + + def with_current(ctx) + token = attach(ctx) + yield ctx + ensure + detach(token) + end + + def with_value(key, value) + ctx = current.set_value(key, value) + token = attach(ctx) + yield ctx, value + ensure + detach(token) + end + + def with_values(values) + ctx = current.set_values(values) + token = attach(ctx) + yield ctx, values + ensure + detach(token) + end + def value(key) + current.value(key) + end + + def clear + Fiber[STACK_KEY] = nil + end + + def empty + new(EMPTY_ENTRIES) + end + end + + def initialize(entries) + super.merge!(entries) + end + + alias value [] + + def set_value(key, value) + new_entries = dup + new_entries[key] = value + new_entries + end + + def set_values(values) # rubocop:disable Naming/AccessorMethodName: + merge(values) + end + + ROOT = empty.freeze +end + +class ArrayContext + EMPTY_ENTRIES = {}.freeze + STACK_KEY = :__array_context_storage__ + private_constant :EMPTY_ENTRIES, :STACK_KEY + + DetachError = Class.new(StandardError) + + class << self + def current + stack.last || ROOT + end + + def attach(context) + s = stack + s.push(context) + s.size + end + + def detach(token) + s = stack + calls_matched = (token == s.size) + OpenTelemetry.handle_error(exception: DetachError.new('calls to detach should match corresponding calls to attach.')) unless calls_matched + + s.pop + calls_matched + end + + def with_current(ctx) + token = attach(ctx) + yield ctx + ensure + detach(token) + end + + def with_value(key, value) + ctx = current.set_value(key, value) + token = attach(ctx) + yield ctx, value + ensure + detach(token) + end + + def with_values(values) + ctx = current.set_values(values) + token = attach(ctx) + yield ctx, values + ensure + detach(token) + end + + def value(key) + current.value(key) + end + + def clear + stack.clear + end + + def empty + new(EMPTY_ENTRIES) + end + + private + + def stack + Thread.current[STACK_KEY] ||= [] + end + end + + def initialize(entries) + @entries = entries.freeze + end + + def value(key) + @entries[key] + end + + alias [] value + + def set_value(key, value) + new_entries = @entries.dup + new_entries[key] = value + ArrayContext.new(new_entries) + end + + def set_values(values) # rubocop:disable Naming/AccessorMethodName: + ArrayContext.new(@entries.merge(values)) + end + + ROOT = empty.freeze +end + +class FiberLocalArrayContext + EMPTY_ENTRIES = {}.freeze + STACK_KEY = :__fiber_local_array_context_storage__ + + # NOTE: This is cool, but is isn't safe for concurrent use because it allows the + # owner to modify the stack after it has been shared with another fiber. + class Stack < Array + def self.current + s = Fiber[STACK_KEY] ||= new + s.correct_owner! + end + + def initialize + super + @owner = Fiber.current + end + + def correct_owner! + if @owner != Fiber.current + Fiber[STACK_KEY] = self.class.new.replace(self) + else + self + end + end + end + + private_constant :EMPTY_ENTRIES, :STACK_KEY, :Stack + + DetachError = Class.new(StandardError) + + class << self + def current + Stack.current.last || ROOT + end + + def attach(context) + s = Stack.current + s.push(context) + s.size + end + + def detach(token) + s = Stack.current + calls_matched = (token == s.size) + OpenTelemetry.handle_error(exception: DetachError.new('calls to detach should match corresponding calls to attach.')) unless calls_matched + + s.pop + calls_matched + end + + def with_current(ctx) + s = Stack.current + s.push(ctx) + token = s.size + yield ctx + ensure + OpenTelemetry.handle_error(exception: DetachError.new('calls to detach should match corresponding calls to attach.')) unless token == s.size + s.pop + end + + def with_value(key, value) + s = Stack.current + ctx = (s.last || ROOT).set_value(key, value) + s.push(ctx) + token = s.size + yield ctx, value + ensure + OpenTelemetry.handle_error(exception: DetachError.new('calls to detach should match corresponding calls to attach.')) unless token == s.size + s.pop + end + + def with_values(values) + s = Stack.current + ctx = (s.last || ROOT).set_values(values) + s.push(ctx) + token = s.size + yield ctx, values + ensure + OpenTelemetry.handle_error(exception: DetachError.new('calls to detach should match corresponding calls to attach.')) unless token == s.size + s.pop + end + + def value(key) + current.value(key) + end + + def clear + Stack.current.clear + end + + def empty + new(EMPTY_ENTRIES) + end + end + + def initialize(entries) + @entries = entries.freeze + end + + def value(key) + @entries[key] + end + + alias [] value + + def set_value(key, value) + new_entries = @entries.dup + new_entries[key] = value + FiberLocalArrayContext.new(new_entries) + end + + def set_values(values) # rubocop:disable Naming/AccessorMethodName: + FiberLocalArrayContext.new(@entries.merge(values)) + end + + ROOT = empty.freeze +end + +class ImmutableArrayContext + EMPTY_ENTRIES = {}.freeze + STACK_KEY = :__immutable_array_context_storage__ + private_constant :EMPTY_ENTRIES, :STACK_KEY + + DetachError = Class.new(StandardError) + + class << self + def current + stack.last || ROOT + end + + def attach(context) + new_stack = stack + [context] + Thread.current[STACK_KEY] = new_stack + new_stack.size + end + + def detach(token) + s = stack + calls_matched = (token == s.size) + OpenTelemetry.handle_error(exception: DetachError.new('calls to detach should match corresponding calls to attach.')) unless calls_matched + + Thread.current[STACK_KEY] = s[...-1] || [] + calls_matched + end + + def with_current(ctx) + token = attach(ctx) + yield ctx + ensure + detach(token) + end + + def with_value(key, value) + ctx = current.set_value(key, value) + token = attach(ctx) + yield ctx, value + ensure + detach(token) + end + + def with_values(values) + ctx = current.set_values(values) + token = attach(ctx) + yield ctx, values + ensure + detach(token) + end + + def value(key) + current.value(key) + end + + def clear + stack.clear + end + + def empty + new(EMPTY_ENTRIES) + end + + private + + def stack + Thread.current[STACK_KEY] ||= [] + end + end + + def initialize(entries) + @entries = entries.freeze + end + + def value(key) + @entries[key] + end + + alias [] value + + def set_value(key, value) + new_entries = @entries.dup + new_entries[key] = value + ImmutableArrayContext.new(new_entries) + end + + def set_values(values) # rubocop:disable Naming/AccessorMethodName: + ImmutableArrayContext.new(@entries.merge(values)) + end + + ROOT = empty.freeze +end + +class FiberLocalImmutableArrayContext + EMPTY_ENTRIES = {}.freeze + STACK_KEY = :__fiber_local_immutable_array_context_storage__ + private_constant :EMPTY_ENTRIES, :STACK_KEY + + DetachError = Class.new(StandardError) + + class << self + def current + stack.last || ROOT + end + + def attach(context) + new_stack = stack + [context] + Fiber[STACK_KEY] = new_stack + new_stack.size + end + + def detach(token) + s = stack + calls_matched = (token == s.size) + OpenTelemetry.handle_error(exception: DetachError.new('calls to detach should match corresponding calls to attach.')) unless calls_matched + + Fiber[STACK_KEY] = s[...-1] || [] + calls_matched + end + + def with_current(ctx) + token = attach(ctx) + yield ctx + ensure + detach(token) + end + + def with_value(key, value) + ctx = current.set_value(key, value) + token = attach(ctx) + yield ctx, value + ensure + detach(token) + end + + def with_values(values) + ctx = current.set_values(values) + token = attach(ctx) + yield ctx, values + ensure + detach(token) + end + + def value(key) + current.value(key) + end + + def clear + stack.clear + end + + def empty + new(EMPTY_ENTRIES) + end + + private + + def stack + Fiber[STACK_KEY] ||= [] + end + end + + def initialize(entries) + @entries = entries.freeze + end + + def value(key) + @entries[key] + end + + alias [] value + + def set_value(key, value) + new_entries = @entries.dup + new_entries[key] = value + FiberLocalImmutableArrayContext.new(new_entries) + end + + def set_values(values) # rubocop:disable Naming/AccessorMethodName: + FiberLocalImmutableArrayContext.new(@entries.merge(values)) + end + + ROOT = empty.freeze +end + +values = { 'key' => 'value' } +context = LinkedListContext.empty.set_values(values) + +Benchmark.ipsa do |x| + x.report 'FiberAttributeContext.with_value' do + FiberAttributeContext.with_value('key', 'value') { |ctx, value| ctx } + end + + x.report 'LinkedListContext.with_value' do + LinkedListContext.with_value('key', 'value') { |ctx, value| ctx } + end + + x.report 'ArrayContext.with_value' do + ArrayContext.with_value('key', 'value') { |ctx, value| ctx } + end + + x.report 'ImmutableArrayContext.with_value' do + ImmutableArrayContext.with_value('key', 'value') { |ctx, value| ctx } + end + + x.report 'FiberLocalVarContext.with_value' do + FiberLocalVarContext.with_value('key', 'value') { |ctx, value| ctx } + end + + x.report 'FiberLocalLinkedListContext.with_value' do + FiberLocalLinkedListContext.with_value('key', 'value') { |ctx, value| ctx } + end + + x.report 'FiberLocalImmutableArrayContext.with_value' do + FiberLocalImmutableArrayContext.with_value('key', 'value') { |ctx, value| ctx } + end + + x.report 'FiberLocalArrayContext.with_value' do + FiberLocalArrayContext.with_value('key', 'value') { |ctx, value| ctx } + end + + x.compare! +end + +Benchmark.ipsa do |x| + x.report 'LinkedListContext.with_value recursive' do + LinkedListContext.with_value('key', 'value') do + LinkedListContext.with_value('key', 'value') do + LinkedListContext.with_value('key', 'value') do + LinkedListContext.with_value('key', 'value') do + LinkedListContext.with_value('key', 'value') do + LinkedListContext.with_value('key', 'value') do + LinkedListContext.with_value('key', 'value') do + LinkedListContext.with_value('key', 'value') do + LinkedListContext.with_value('key', 'value') do + LinkedListContext.with_value('key', 'value') do |ctx, value| ctx end + end + end + end + end + end + end + end + end + end + end + + x.report 'ArrayContext.with_value recursive' do + ArrayContext.with_value('key', 'value') do + ArrayContext.with_value('key', 'value') do + ArrayContext.with_value('key', 'value') do + ArrayContext.with_value('key', 'value') do + ArrayContext.with_value('key', 'value') do + ArrayContext.with_value('key', 'value') do + ArrayContext.with_value('key', 'value') do + ArrayContext.with_value('key', 'value') do + ArrayContext.with_value('key', 'value') do + ArrayContext.with_value('key', 'value') do |ctx, value| ctx end + end + end + end + end + end + end + end + end + end + end + + x.report 'ImmutableArrayContext.with_value recursive' do + ImmutableArrayContext.with_value('key', 'value') do + ImmutableArrayContext.with_value('key', 'value') do + ImmutableArrayContext.with_value('key', 'value') do + ImmutableArrayContext.with_value('key', 'value') do + ImmutableArrayContext.with_value('key', 'value') do + ImmutableArrayContext.with_value('key', 'value') do + ImmutableArrayContext.with_value('key', 'value') do + ImmutableArrayContext.with_value('key', 'value') do + ImmutableArrayContext.with_value('key', 'value') do + ImmutableArrayContext.with_value('key', 'value') do |ctx, value| ctx end + end + end + end + end + end + end + end + end + end + end + + x.report 'FiberAttributeContext.with_value recursive' do + FiberAttributeContext.with_value('key', 'value') do + FiberAttributeContext.with_value('key', 'value') do + FiberAttributeContext.with_value('key', 'value') do + FiberAttributeContext.with_value('key', 'value') do + FiberAttributeContext.with_value('key', 'value') do + FiberAttributeContext.with_value('key', 'value') do + FiberAttributeContext.with_value('key', 'value') do + FiberAttributeContext.with_value('key', 'value') do + FiberAttributeContext.with_value('key', 'value') do + FiberAttributeContext.with_value('key', 'value') do |ctx, value| ctx end + end + end + end + end + end + end + end + end + end + end + + x.report 'FiberLocalVarContext.with_value recursive' do + FiberLocalVarContext.with_value('key', 'value') do + FiberLocalVarContext.with_value('key', 'value') do + FiberLocalVarContext.with_value('key', 'value') do + FiberLocalVarContext.with_value('key', 'value') do + FiberLocalVarContext.with_value('key', 'value') do + FiberLocalVarContext.with_value('key', 'value') do + FiberLocalVarContext.with_value('key', 'value') do + FiberLocalVarContext.with_value('key', 'value') do + FiberLocalVarContext.with_value('key', 'value') do + FiberLocalVarContext.with_value('key', 'value') do |ctx, value| ctx end + end + end + end + end + end + end + end + end + end + end + + x.report 'FiberLocalLinkedListContext.with_value recursive' do + FiberLocalLinkedListContext.with_value('key', 'value') do + FiberLocalLinkedListContext.with_value('key', 'value') do + FiberLocalLinkedListContext.with_value('key', 'value') do + FiberLocalLinkedListContext.with_value('key', 'value') do + FiberLocalLinkedListContext.with_value('key', 'value') do + FiberLocalLinkedListContext.with_value('key', 'value') do + FiberLocalLinkedListContext.with_value('key', 'value') do + FiberLocalLinkedListContext.with_value('key', 'value') do + FiberLocalLinkedListContext.with_value('key', 'value') do + FiberLocalLinkedListContext.with_value('key', 'value') do |ctx, value| ctx end + end + end + end + end + end + end + end + end + end + end + + x.report 'FiberLocalImmutableArrayContext.with_value recursive' do + FiberLocalImmutableArrayContext.with_value('key', 'value') do + FiberLocalImmutableArrayContext.with_value('key', 'value') do + FiberLocalImmutableArrayContext.with_value('key', 'value') do + FiberLocalImmutableArrayContext.with_value('key', 'value') do + FiberLocalImmutableArrayContext.with_value('key', 'value') do + FiberLocalImmutableArrayContext.with_value('key', 'value') do + FiberLocalImmutableArrayContext.with_value('key', 'value') do + FiberLocalImmutableArrayContext.with_value('key', 'value') do + FiberLocalImmutableArrayContext.with_value('key', 'value') do + FiberLocalImmutableArrayContext.with_value('key', 'value') do |ctx, value| ctx end + end + end + end + end + end + end + end + end + end + end + + x.report 'FiberLocalArrayContext.with_value recursive' do + FiberLocalArrayContext.with_value('key', 'value') do + FiberLocalArrayContext.with_value('key', 'value') do + FiberLocalArrayContext.with_value('key', 'value') do + FiberLocalArrayContext.with_value('key', 'value') do + FiberLocalArrayContext.with_value('key', 'value') do + FiberLocalArrayContext.with_value('key', 'value') do + FiberLocalArrayContext.with_value('key', 'value') do + FiberLocalArrayContext.with_value('key', 'value') do + FiberLocalArrayContext.with_value('key', 'value') do + FiberLocalArrayContext.with_value('key', 'value') do |ctx, value| ctx end + end + end + end + end + end + end + end + end + end + end + + x.compare! +end diff --git a/api/opentelemetry-api.gemspec b/api/opentelemetry-api.gemspec index 50681eb1f4..f19e9918ea 100644 --- a/api/opentelemetry-api.gemspec +++ b/api/opentelemetry-api.gemspec @@ -35,6 +35,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'simplecov', '~> 0.17' spec.add_development_dependency 'yard', '~> 0.9' spec.add_development_dependency 'yard-doctest', '~> 0.1.6' + spec.add_development_dependency 'concurrent-ruby', '~> 1.3' if spec.respond_to?(:metadata) spec.metadata['changelog_uri'] = "https://open-telemetry.github.io/opentelemetry-ruby/opentelemetry-api/v#{OpenTelemetry::VERSION}/file.CHANGELOG.html" From a70f9cef3f7a8d274ff3e2df7fe27102fa48ae9b Mon Sep 17 00:00:00 2001 From: Francis Bogsanyi Date: Thu, 6 Feb 2025 17:29:58 -0500 Subject: [PATCH 3/9] Skip Fiber storage test on Ruby 3.1 --- api/test/opentelemetry/context_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/api/test/opentelemetry/context_test.rb b/api/test/opentelemetry/context_test.rb index 6d86d26871..54e5bf783d 100644 --- a/api/test/opentelemetry/context_test.rb +++ b/api/test/opentelemetry/context_test.rb @@ -312,6 +312,7 @@ end it 'is isolated with respect to Fiber-local storage manipulation' do + skip unless Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2.0') OpenTelemetry::TestHelpers.with_test_logger do |log_stream| ctx = new_context Context.with_current(ctx) do From bb3f5e90ca749c6dbe683cb15b2469587080a553 Mon Sep 17 00:00:00 2001 From: Francis Bogsanyi Date: Thu, 6 Feb 2025 17:43:05 -0500 Subject: [PATCH 4/9] Appease the cop --- api/.rubocop.yml | 4 ++++ api/opentelemetry-api.gemspec | 2 +- api/test/opentelemetry/context_test.rb | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/api/.rubocop.yml b/api/.rubocop.yml index 57f00774a0..452a05d703 100644 --- a/api/.rubocop.yml +++ b/api/.rubocop.yml @@ -1,5 +1,9 @@ inherit_from: ../contrib/rubocop.yml +AllCops: + Exclude: + - "benchmarks/context_bench.rb" + Naming/FileName: Exclude: - "lib/opentelemetry-api.rb" diff --git a/api/opentelemetry-api.gemspec b/api/opentelemetry-api.gemspec index f19e9918ea..dab247f30d 100644 --- a/api/opentelemetry-api.gemspec +++ b/api/opentelemetry-api.gemspec @@ -27,6 +27,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'benchmark-ipsa', '~> 0.2.0' spec.add_development_dependency 'bundler', '>= 1.17' + spec.add_development_dependency 'concurrent-ruby', '~> 1.3' spec.add_development_dependency 'faraday', '~> 0.13' spec.add_development_dependency 'minitest', '~> 5.0' spec.add_development_dependency 'opentelemetry-test-helpers' @@ -35,7 +36,6 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'simplecov', '~> 0.17' spec.add_development_dependency 'yard', '~> 0.9' spec.add_development_dependency 'yard-doctest', '~> 0.1.6' - spec.add_development_dependency 'concurrent-ruby', '~> 1.3' if spec.respond_to?(:metadata) spec.metadata['changelog_uri'] = "https://open-telemetry.github.io/opentelemetry-ruby/opentelemetry-api/v#{OpenTelemetry::VERSION}/file.CHANGELOG.html" diff --git a/api/test/opentelemetry/context_test.rb b/api/test/opentelemetry/context_test.rb index 54e5bf783d..45ebd07684 100644 --- a/api/test/opentelemetry/context_test.rb +++ b/api/test/opentelemetry/context_test.rb @@ -304,7 +304,7 @@ # Manipulate _this fiber's_ context stack. Context.attach(new_context) ensure - locals.each { |k, _| t2[k] = nil } + locals.each_key { |k| t2.delete(k) } end.resume end _(log_stream.string).must_be_empty @@ -327,7 +327,7 @@ # Manipulate _this fiber's_ context stack. Context.attach(new_context) ensure - locals.each { |k, _| f2.storage[k] = nil } + locals.each_key { |k| f2.storage.delete(k) } end.resume end _(log_stream.string).must_be_empty @@ -350,7 +350,7 @@ # Manipulate _this thread's_ context stack. Context.attach(new_context) ensure - locals.each { |k, _| t2[k] = nil } + locals.each_key { |k| t2.delete(k) } end.join end _(log_stream.string).must_be_empty From b62e9a326d430913cc5ec02963a629565ebf8610 Mon Sep 17 00:00:00 2001 From: Francis Bogsanyi Date: Thu, 6 Feb 2025 17:47:18 -0500 Subject: [PATCH 5/9] Cop lied to me --- api/test/opentelemetry/context_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/test/opentelemetry/context_test.rb b/api/test/opentelemetry/context_test.rb index 45ebd07684..deca405032 100644 --- a/api/test/opentelemetry/context_test.rb +++ b/api/test/opentelemetry/context_test.rb @@ -304,7 +304,7 @@ # Manipulate _this fiber's_ context stack. Context.attach(new_context) ensure - locals.each_key { |k| t2.delete(k) } + locals.each { |k, _| t2.delete(k) } end.resume end _(log_stream.string).must_be_empty @@ -350,7 +350,7 @@ # Manipulate _this thread's_ context stack. Context.attach(new_context) ensure - locals.each_key { |k| t2.delete(k) } + locals.each { |k, _| t2.delete(k) } end.join end _(log_stream.string).must_be_empty From 7278ef65f3cca821090592dbf21261d89550ff46 Mon Sep 17 00:00:00 2001 From: Francis Bogsanyi Date: Thu, 6 Feb 2025 17:49:18 -0500 Subject: [PATCH 6/9] Sigh --- api/test/opentelemetry/context_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/test/opentelemetry/context_test.rb b/api/test/opentelemetry/context_test.rb index deca405032..75f69b0334 100644 --- a/api/test/opentelemetry/context_test.rb +++ b/api/test/opentelemetry/context_test.rb @@ -304,7 +304,7 @@ # Manipulate _this fiber's_ context stack. Context.attach(new_context) ensure - locals.each { |k, _| t2.delete(k) } + locals.each { |k, _| t2[k] = nil } end.resume end _(log_stream.string).must_be_empty @@ -350,7 +350,7 @@ # Manipulate _this thread's_ context stack. Context.attach(new_context) ensure - locals.each { |k, _| t2.delete(k) } + locals.each { |k, _| t2[k] = nil } end.join end _(log_stream.string).must_be_empty From 48a02a7464568b90811fad5f935d4ca1dfc1533a Mon Sep 17 00:00:00 2001 From: Francis Bogsanyi Date: Fri, 7 Feb 2025 09:41:25 -0500 Subject: [PATCH 7/9] Find another way --- api/.rubocop.yml | 4 -- api/benchmarks/context_bench.rb | 59 ++++++++++++++------------ api/test/opentelemetry/context_test.rb | 4 +- 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/api/.rubocop.yml b/api/.rubocop.yml index 452a05d703..57f00774a0 100644 --- a/api/.rubocop.yml +++ b/api/.rubocop.yml @@ -1,9 +1,5 @@ inherit_from: ../contrib/rubocop.yml -AllCops: - Exclude: - - "benchmarks/context_bench.rb" - Naming/FileName: Exclude: - "lib/opentelemetry-api.rb" diff --git a/api/benchmarks/context_bench.rb b/api/benchmarks/context_bench.rb index fa9616f85d..185d393567 100644 --- a/api/benchmarks/context_bench.rb +++ b/api/benchmarks/context_bench.rb @@ -8,6 +8,7 @@ require 'concurrent-ruby' require 'opentelemetry' +# Manages context on a per-fiber basis class FiberLocalVarContext EMPTY_ENTRIES = {}.freeze VAR = Concurrent::FiberLocalVar.new { [] } @@ -102,6 +103,7 @@ def set_values(values) # rubocop:disable Naming/AccessorMethodName: Fiber.attr_accessor :opentelemetry_context +# Manages context on a per-fiber basis class FiberAttributeContext EMPTY_ENTRIES = {}.freeze private_constant :EMPTY_ENTRIES @@ -193,18 +195,19 @@ def set_values(values) # rubocop:disable Naming/AccessorMethodName: ROOT = empty.freeze end +# Manages context on a per-fiber basis class LinkedListContext EMPTY_ENTRIES = {}.freeze STACK_KEY = :__linked_list_context_storage__ + # @api private class Token + attr_reader :context, :next_token + def initialize(context, next_token) @context = context @next_token = next_token end - - def context = @context - def next_token = @next_token end private_constant :EMPTY_ENTRIES, :STACK_KEY, :Token @@ -254,6 +257,7 @@ def with_values(values) ensure detach(token) end + def value(key) current.value(key) end @@ -290,18 +294,19 @@ def set_values(values) # rubocop:disable Naming/AccessorMethodName: ROOT = empty.freeze end +# Manages context on a per-fiber basis class FiberLocalLinkedListContext < Hash EMPTY_ENTRIES = {}.freeze STACK_KEY = :__fiber_local_linked_list_context_storage__ + # @api private class Token + attr_reader :context, :next_token + def initialize(context, next_token) @context = context @next_token = next_token end - - def context = @context - def next_token = @next_token end private_constant :EMPTY_ENTRIES, :STACK_KEY, :Token @@ -348,6 +353,7 @@ def with_values(values) ensure detach(token) end + def value(key) current.value(key) end @@ -380,6 +386,7 @@ def set_values(values) # rubocop:disable Naming/AccessorMethodName: ROOT = empty.freeze end +# Manages context on a per-fiber basis class ArrayContext EMPTY_ENTRIES = {}.freeze STACK_KEY = :__array_context_storage__ @@ -472,6 +479,7 @@ def set_values(values) # rubocop:disable Naming/AccessorMethodName: ROOT = empty.freeze end +# Manages context on a per-fiber basis class FiberLocalArrayContext EMPTY_ENTRIES = {}.freeze STACK_KEY = :__fiber_local_array_context_storage__ @@ -590,6 +598,7 @@ def set_values(values) # rubocop:disable Naming/AccessorMethodName: ROOT = empty.freeze end +# Manages context on a per-fiber basis class ImmutableArrayContext EMPTY_ENTRIES = {}.freeze STACK_KEY = :__immutable_array_context_storage__ @@ -682,6 +691,7 @@ def set_values(values) # rubocop:disable Naming/AccessorMethodName: ROOT = empty.freeze end +# Manages context on a per-fiber basis class FiberLocalImmutableArrayContext EMPTY_ENTRIES = {}.freeze STACK_KEY = :__fiber_local_immutable_array_context_storage__ @@ -774,46 +784,43 @@ def set_values(values) # rubocop:disable Naming/AccessorMethodName: ROOT = empty.freeze end -values = { 'key' => 'value' } -context = LinkedListContext.empty.set_values(values) - Benchmark.ipsa do |x| x.report 'FiberAttributeContext.with_value' do - FiberAttributeContext.with_value('key', 'value') { |ctx, value| ctx } + FiberAttributeContext.with_value('key', 'value') { |ctx, _| ctx } end x.report 'LinkedListContext.with_value' do - LinkedListContext.with_value('key', 'value') { |ctx, value| ctx } + LinkedListContext.with_value('key', 'value') { |ctx, _| ctx } end x.report 'ArrayContext.with_value' do - ArrayContext.with_value('key', 'value') { |ctx, value| ctx } + ArrayContext.with_value('key', 'value') { |ctx, _| ctx } end x.report 'ImmutableArrayContext.with_value' do - ImmutableArrayContext.with_value('key', 'value') { |ctx, value| ctx } + ImmutableArrayContext.with_value('key', 'value') { |ctx, _| ctx } end x.report 'FiberLocalVarContext.with_value' do - FiberLocalVarContext.with_value('key', 'value') { |ctx, value| ctx } + FiberLocalVarContext.with_value('key', 'value') { |ctx, _| ctx } end x.report 'FiberLocalLinkedListContext.with_value' do - FiberLocalLinkedListContext.with_value('key', 'value') { |ctx, value| ctx } + FiberLocalLinkedListContext.with_value('key', 'value') { |ctx, _| ctx } end x.report 'FiberLocalImmutableArrayContext.with_value' do - FiberLocalImmutableArrayContext.with_value('key', 'value') { |ctx, value| ctx } + FiberLocalImmutableArrayContext.with_value('key', 'value') { |ctx, _| ctx } end x.report 'FiberLocalArrayContext.with_value' do - FiberLocalArrayContext.with_value('key', 'value') { |ctx, value| ctx } + FiberLocalArrayContext.with_value('key', 'value') { |ctx, _| ctx } end x.compare! end -Benchmark.ipsa do |x| +Benchmark.ipsa do |x| # rubocop:disable Metrics/BlockLength x.report 'LinkedListContext.with_value recursive' do LinkedListContext.with_value('key', 'value') do LinkedListContext.with_value('key', 'value') do @@ -824,7 +831,7 @@ def set_values(values) # rubocop:disable Naming/AccessorMethodName: LinkedListContext.with_value('key', 'value') do LinkedListContext.with_value('key', 'value') do LinkedListContext.with_value('key', 'value') do - LinkedListContext.with_value('key', 'value') do |ctx, value| ctx end + LinkedListContext.with_value('key', 'value') { |ctx, _| ctx } end end end @@ -846,7 +853,7 @@ def set_values(values) # rubocop:disable Naming/AccessorMethodName: ArrayContext.with_value('key', 'value') do ArrayContext.with_value('key', 'value') do ArrayContext.with_value('key', 'value') do - ArrayContext.with_value('key', 'value') do |ctx, value| ctx end + ArrayContext.with_value('key', 'value') { |ctx, _| ctx } end end end @@ -868,7 +875,7 @@ def set_values(values) # rubocop:disable Naming/AccessorMethodName: ImmutableArrayContext.with_value('key', 'value') do ImmutableArrayContext.with_value('key', 'value') do ImmutableArrayContext.with_value('key', 'value') do - ImmutableArrayContext.with_value('key', 'value') do |ctx, value| ctx end + ImmutableArrayContext.with_value('key', 'value') { |ctx, _| ctx } end end end @@ -890,7 +897,7 @@ def set_values(values) # rubocop:disable Naming/AccessorMethodName: FiberAttributeContext.with_value('key', 'value') do FiberAttributeContext.with_value('key', 'value') do FiberAttributeContext.with_value('key', 'value') do - FiberAttributeContext.with_value('key', 'value') do |ctx, value| ctx end + FiberAttributeContext.with_value('key', 'value') { |ctx, _| ctx } end end end @@ -912,7 +919,7 @@ def set_values(values) # rubocop:disable Naming/AccessorMethodName: FiberLocalVarContext.with_value('key', 'value') do FiberLocalVarContext.with_value('key', 'value') do FiberLocalVarContext.with_value('key', 'value') do - FiberLocalVarContext.with_value('key', 'value') do |ctx, value| ctx end + FiberLocalVarContext.with_value('key', 'value') { |ctx, _| ctx } end end end @@ -934,7 +941,7 @@ def set_values(values) # rubocop:disable Naming/AccessorMethodName: FiberLocalLinkedListContext.with_value('key', 'value') do FiberLocalLinkedListContext.with_value('key', 'value') do FiberLocalLinkedListContext.with_value('key', 'value') do - FiberLocalLinkedListContext.with_value('key', 'value') do |ctx, value| ctx end + FiberLocalLinkedListContext.with_value('key', 'value') { |ctx, _| ctx } end end end @@ -956,7 +963,7 @@ def set_values(values) # rubocop:disable Naming/AccessorMethodName: FiberLocalImmutableArrayContext.with_value('key', 'value') do FiberLocalImmutableArrayContext.with_value('key', 'value') do FiberLocalImmutableArrayContext.with_value('key', 'value') do - FiberLocalImmutableArrayContext.with_value('key', 'value') do |ctx, value| ctx end + FiberLocalImmutableArrayContext.with_value('key', 'value') { |ctx, _| ctx } end end end @@ -978,7 +985,7 @@ def set_values(values) # rubocop:disable Naming/AccessorMethodName: FiberLocalArrayContext.with_value('key', 'value') do FiberLocalArrayContext.with_value('key', 'value') do FiberLocalArrayContext.with_value('key', 'value') do - FiberLocalArrayContext.with_value('key', 'value') do |ctx, value| ctx end + FiberLocalArrayContext.with_value('key', 'value') { |ctx, _| ctx } end end end diff --git a/api/test/opentelemetry/context_test.rb b/api/test/opentelemetry/context_test.rb index 75f69b0334..12918809af 100644 --- a/api/test/opentelemetry/context_test.rb +++ b/api/test/opentelemetry/context_test.rb @@ -304,7 +304,7 @@ # Manipulate _this fiber's_ context stack. Context.attach(new_context) ensure - locals.each { |k, _| t2[k] = nil } + locals.each { |k, _| t2[k] = nil } # rubocop:disable Style/HashEachMethods (locals is not a Hash) end.resume end _(log_stream.string).must_be_empty @@ -350,7 +350,7 @@ # Manipulate _this thread's_ context stack. Context.attach(new_context) ensure - locals.each { |k, _| t2[k] = nil } + locals.each { |k, _| t2[k] = nil } # rubocop:disable Style/HashEachMethods (locals is not a Hash) end.join end _(log_stream.string).must_be_empty From 006319bf7fb5a2c947f5d53a57035c2be8c27acc Mon Sep 17 00:00:00 2001 From: Francis Bogsanyi Date: Fri, 7 Feb 2025 09:45:54 -0500 Subject: [PATCH 8/9] Last one? --- api/lib/opentelemetry/context.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/lib/opentelemetry/context.rb b/api/lib/opentelemetry/context.rb index 83a6fa8d51..de4e2565b8 100644 --- a/api/lib/opentelemetry/context.rb +++ b/api/lib/opentelemetry/context.rb @@ -7,7 +7,7 @@ require 'opentelemetry/context/key' require 'opentelemetry/context/propagation' -module OpenTelemetry +module OpenTelemetry # rubocop:disable Style/Documentation Fiber.attr_accessor :opentelemetry_context # Manages context on a per-fiber basis From 731f158c5a9a6dfe981cfaf8bfecde7719e5c062 Mon Sep 17 00:00:00 2001 From: Francis Bogsanyi Date: Fri, 7 Feb 2025 09:51:51 -0500 Subject: [PATCH 9/9] Exclude test for TruffleRuby as well --- api/test/opentelemetry/context_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/test/opentelemetry/context_test.rb b/api/test/opentelemetry/context_test.rb index 12918809af..a3f017adb7 100644 --- a/api/test/opentelemetry/context_test.rb +++ b/api/test/opentelemetry/context_test.rb @@ -312,7 +312,7 @@ end it 'is isolated with respect to Fiber-local storage manipulation' do - skip unless Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2.0') + skip unless Fiber.current.respond_to?(:storage) OpenTelemetry::TestHelpers.with_test_logger do |log_stream| ctx = new_context Context.with_current(ctx) do