scout_apm 6.2.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.markdown +11 -0
- data/gems/instruments-ruby4.gemfile +1 -0
- data/gems/instruments.gemfile +1 -0
- data/lib/scout_apm/agent_context.rb +1 -1
- data/lib/scout_apm/git_revision.rb +3 -1
- data/lib/scout_apm/instrument_manager.rb +1 -0
- data/lib/scout_apm/instruments/grape.rb +61 -6
- data/lib/scout_apm/instruments/graphql.rb +58 -0
- data/lib/scout_apm/request_manager.rb +14 -2
- data/lib/scout_apm/tracked_request.rb +26 -4
- data/lib/scout_apm/version.rb +1 -1
- data/lib/scout_apm.rb +1 -0
- data/test/unit/agent_context_test.rb +10 -1
- data/test/unit/git_revision_test.rb +13 -0
- data/test/unit/instruments/grape_test.rb +83 -0
- data/test/unit/request_manager_test.rb +70 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fe18e4045997fdf6b731025ee98c13e2702096a0cfd13ac5f0f651d7a7f4a4db
|
|
4
|
+
data.tar.gz: d9b4593ef2b8d5df4983d7b93cb9eaf5cc3f7e6916d8288ae9814e59dd913007
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5b838d8e12286496038899c7dd006b6c3d6507061bcba1a963fc10664fa96e36c603f71e315430eda9eeaa84dabadb21f91dee9e9aa9baf7009f6e03d8f3d9d
|
|
7
|
+
data.tar.gz: bee9ed547611139d56eb6f88cf6c1e730dc77dbdf575e89a8b1cd95f86756687174c78b64cf00acd389a74e90198f9750aec334e2dfba195340a0a9753c280bd
|
data/CHANGELOG.markdown
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Pending
|
|
2
2
|
|
|
3
|
+
# 6.3.0
|
|
4
|
+
|
|
5
|
+
- Fix span leak on `ActionController::Live` streaming requests (#631)
|
|
6
|
+
|
|
7
|
+
# 6.2.1
|
|
8
|
+
|
|
9
|
+
- Fix `SystemStackError` with `grape >= 3.3.0` by falling back to `prepend` instrumentation when Grape owns `Endpoint#run` itself (#624)
|
|
10
|
+
- Fix a GraphQL layer leak where an unhandled exception skipping `end_execute_field` could leave stale layers on the request (#626)
|
|
11
|
+
- Honor `SCOUT_LOG_LEVEL` during agent bootstrap instead of always logging at the default level (#628)
|
|
12
|
+
- Use `HEROKU_BUILD_COMMIT` for Heroku git revision detection, since `HEROKU_SLUG_COMMIT` is deprecated (#625)
|
|
13
|
+
|
|
3
14
|
# 6.2.0
|
|
4
15
|
|
|
5
16
|
- Fix compatibility with `http >= 6.0.0` (#613)
|
data/gems/instruments.gemfile
CHANGED
|
@@ -26,7 +26,9 @@ module ScoutApm
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def detect_from_heroku
|
|
29
|
-
|
|
29
|
+
# HEROKU_SLUG_COMMIT is deprecated in favor of HEROKU_BUILD_COMMIT.
|
|
30
|
+
# https://devcenter.heroku.com/articles/dyno-metadata#usage
|
|
31
|
+
ENV['HEROKU_BUILD_COMMIT'] || ENV['HEROKU_SLUG_COMMIT']
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
# Config will locate the value from:
|
|
@@ -41,6 +41,7 @@ module ScoutApm
|
|
|
41
41
|
install_instrument(ScoutApm::Instruments::Elasticsearch)
|
|
42
42
|
install_instrument(ScoutApm::Instruments::OpenSearch)
|
|
43
43
|
install_instrument(ScoutApm::Instruments::Grape)
|
|
44
|
+
install_instrument(ScoutApm::Instruments::GraphQL)
|
|
44
45
|
rescue
|
|
45
46
|
logger.warn "Exception loading instruments:"
|
|
46
47
|
logger.warn $!.message
|
|
@@ -20,16 +20,35 @@ module ScoutApm
|
|
|
20
20
|
if defined?(::Grape) && defined?(::Grape::Endpoint)
|
|
21
21
|
@installed = true
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
# Grape >= 3.3.0 prepends a module of its own (Grape::Testing::RunBeforeEach)
|
|
24
|
+
# in front of Grape::Endpoint#run. An alias-method chain built on a method
|
|
25
|
+
# owned by a prepended module recurses infinitely (SystemStackError), because
|
|
26
|
+
# the aliased copy's `super` resolves back into our wrapper. Whenever #run is
|
|
27
|
+
# not owned by Grape::Endpoint itself, prepend is the only safe option.
|
|
28
|
+
prepend = true unless endpoint_owns_run?
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
include ScoutApm::Instruments::GrapeEndpointInstruments
|
|
30
|
+
logger.info "Instrumenting Grape::Endpoint. Prepend: #{prepend}"
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
if prepend
|
|
33
|
+
::Grape::Endpoint.send(:prepend, GrapeEndpointInstrumentsPrepend)
|
|
34
|
+
else
|
|
35
|
+
::Grape::Endpoint.class_eval do
|
|
36
|
+
include ScoutApm::Instruments::GrapeEndpointInstruments
|
|
37
|
+
|
|
38
|
+
alias run_without_scout_instruments run
|
|
39
|
+
alias run run_with_scout_instruments
|
|
40
|
+
end
|
|
30
41
|
end
|
|
31
42
|
end
|
|
32
43
|
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def endpoint_owns_run?
|
|
48
|
+
::Grape::Endpoint.instance_method(:run).owner == ::Grape::Endpoint
|
|
49
|
+
rescue NameError
|
|
50
|
+
true
|
|
51
|
+
end
|
|
33
52
|
end
|
|
34
53
|
|
|
35
54
|
module GrapeEndpointInstruments
|
|
@@ -68,6 +87,42 @@ module ScoutApm
|
|
|
68
87
|
end
|
|
69
88
|
end
|
|
70
89
|
end
|
|
90
|
+
|
|
91
|
+
module GrapeEndpointInstrumentsPrepend
|
|
92
|
+
def run(*args)
|
|
93
|
+
request = ::Grape::Request.new(env || args.first)
|
|
94
|
+
req = ScoutApm::RequestManager.lookup
|
|
95
|
+
|
|
96
|
+
path = ScoutApm::Agent.instance.context.config.value("uri_reporting") == 'path' ? request.path : request.fullpath
|
|
97
|
+
req.annotate_request(:uri => path)
|
|
98
|
+
|
|
99
|
+
# IP Spoofing Protection can throw an exception, just move on w/o remote ip
|
|
100
|
+
req.context.add_user(:ip => request.ip) rescue nil
|
|
101
|
+
|
|
102
|
+
req.set_headers(request.headers)
|
|
103
|
+
|
|
104
|
+
begin
|
|
105
|
+
name = ["Grape",
|
|
106
|
+
self.options[:method].first,
|
|
107
|
+
self.options[:for].to_s,
|
|
108
|
+
self.namespace.sub(%r{\A/}, ''), # removing leading slashes
|
|
109
|
+
self.options[:path].first,
|
|
110
|
+
].compact.map{ |n| n.to_s }.join("/")
|
|
111
|
+
rescue => e
|
|
112
|
+
ScoutApm::Agent.instance.context.logger.info("Error getting Grape Endpoint Name. Error: #{e.message}. Options: #{self.options.inspect}")
|
|
113
|
+
name = "Grape/Unknown"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
req.start_layer( ScoutApm::Layer.new("Controller", name) )
|
|
117
|
+
begin
|
|
118
|
+
super(*args)
|
|
119
|
+
rescue
|
|
120
|
+
req.error!
|
|
121
|
+
raise
|
|
122
|
+
ensure
|
|
123
|
+
req.stop_layer
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
71
127
|
end
|
|
72
128
|
end
|
|
73
|
-
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScoutApm
|
|
4
|
+
module Instruments
|
|
5
|
+
class GraphQL
|
|
6
|
+
attr_reader :context
|
|
7
|
+
|
|
8
|
+
def initialize(context)
|
|
9
|
+
@context = context
|
|
10
|
+
@installed = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def logger
|
|
14
|
+
context.logger
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def installed?
|
|
18
|
+
@installed
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def install(prepend: false)
|
|
22
|
+
return unless defined?(::GraphQL::Tracing::ScoutTrace)
|
|
23
|
+
|
|
24
|
+
@installed = true
|
|
25
|
+
logger.info "Instrumenting GraphQL::Tracing::ScoutTrace"
|
|
26
|
+
::GraphQL::Tracing::ScoutTrace.prepend(ScoutApm::Instruments::GraphQLExecuteFieldGuard)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Prepended onto GraphQL::Tracing::ScoutTrace to guarantee that every
|
|
31
|
+
# Scout layer opened during a GraphQL multiplex is closed before control
|
|
32
|
+
# returns to the Scout Rack middleware.
|
|
33
|
+
#
|
|
34
|
+
# graphql-ruby's interpreter calls begin_execute_field / end_execute_field
|
|
35
|
+
# around each field resolver. When an unhandled exception causes
|
|
36
|
+
# end_execute_field to be skipped (runtime.rb lines 465-479), the
|
|
37
|
+
# corresponding ScoutApm::Layer is never popped from TrackedRequest#@layers.
|
|
38
|
+
# finalized? never returns true, record! is never called, and
|
|
39
|
+
# Thread.current[:scout_request] accumulates every subsequent request's
|
|
40
|
+
# layers without bound.
|
|
41
|
+
#
|
|
42
|
+
# This guard snapshots the open layer count before executing the multiplex
|
|
43
|
+
# and pops any excess layers in an ensure block, independent of whichever
|
|
44
|
+
# internal trace hooks graphql-ruby happens to use. It has no dependency on
|
|
45
|
+
# MonitorTrace's field-tracing condition or event slot architecture, so it
|
|
46
|
+
# remains correct even if those internals change.
|
|
47
|
+
module GraphQLExecuteFieldGuard
|
|
48
|
+
def execute_multiplex(multiplex:)
|
|
49
|
+
req = ScoutApm::RequestManager.lookup
|
|
50
|
+
layers_before = req.layer_count
|
|
51
|
+
super
|
|
52
|
+
ensure
|
|
53
|
+
extra = req.layer_count - layers_before
|
|
54
|
+
extra.times { req.stop_layer } if extra > 0
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -7,11 +7,23 @@ module ScoutApm
|
|
|
7
7
|
find || create
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
# Get the current Thread local,
|
|
10
|
+
# Get the current Thread local, detecting and not returning a stale request,
|
|
11
|
+
# nor a request that belongs to a different thread.
|
|
11
12
|
def self.find
|
|
12
13
|
req = Thread.current[:scout_request]
|
|
14
|
+
return nil unless req
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
# ActionController::Live runs the controller action in a child thread and
|
|
17
|
+
# copies the parent thread's raw Thread.current locals (including
|
|
18
|
+
# :scout_request) into it. Without this check the parent (Rack) thread and
|
|
19
|
+
# the child (streaming) thread would share and concurrently mutate a single
|
|
20
|
+
# TrackedRequest -- racing on @layers/@stopping -- which corrupts the layer
|
|
21
|
+
# stack, drops the transaction, and leaks layers across requests. If the
|
|
22
|
+
# resident request was created on a different thread, treat it as absent so
|
|
23
|
+
# lookup creates a fresh one owned by this thread.
|
|
24
|
+
return nil if req.creating_thread_id != Thread.current.object_id
|
|
25
|
+
|
|
26
|
+
if req.stopping? || req.recorded?
|
|
15
27
|
nil
|
|
16
28
|
else
|
|
17
29
|
req
|
|
@@ -74,9 +74,24 @@ module ScoutApm
|
|
|
74
74
|
@recorder = agent_context.recorder
|
|
75
75
|
@real_request = false
|
|
76
76
|
@transaction_id = ScoutApm::Utils::TransactionId.new.to_s
|
|
77
|
+
# The thread this request was created on. ActionController::Live copies the
|
|
78
|
+
# raw Thread.current locals (including :scout_request) into its streaming
|
|
79
|
+
# child thread, which would otherwise cause two threads to share and race
|
|
80
|
+
# on one TrackedRequest. RequestManager uses this to detect an inherited
|
|
81
|
+
# request and give the child its own instead. See RequestManager.find.
|
|
82
|
+
@creating_thread_id = Thread.current.object_id
|
|
77
83
|
ignore_request! if @recorder.nil?
|
|
78
84
|
end
|
|
79
85
|
|
|
86
|
+
# The object_id of the Thread that created this request. Used to detect a
|
|
87
|
+
# TrackedRequest that was inherited by a different thread (e.g. an
|
|
88
|
+
# ActionController::Live streaming thread) rather than created for it.
|
|
89
|
+
attr_reader :creating_thread_id
|
|
90
|
+
|
|
91
|
+
def layer_count
|
|
92
|
+
@layers.size
|
|
93
|
+
end
|
|
94
|
+
|
|
80
95
|
def start_layer(layer)
|
|
81
96
|
# If we're already stopping, don't do additional layers
|
|
82
97
|
return if stopping?
|
|
@@ -364,14 +379,21 @@ module ScoutApm
|
|
|
364
379
|
end
|
|
365
380
|
end
|
|
366
381
|
|
|
367
|
-
#
|
|
368
|
-
#
|
|
382
|
+
# True iff this request has a 'Job' layer. Only reliable during recording --
|
|
383
|
+
# see #web? for why.
|
|
369
384
|
def job?
|
|
370
385
|
layer_finder.job != nil
|
|
371
386
|
end
|
|
372
387
|
|
|
373
|
-
#
|
|
374
|
-
#
|
|
388
|
+
# True iff this request has a 'Controller' layer.
|
|
389
|
+
#
|
|
390
|
+
# Only reliable during recording; returns false mid-request even for a real
|
|
391
|
+
# web request. #layer_finder walks root_layer's child *tree*, but layers are
|
|
392
|
+
# attached to that tree only in #stop_layer (via add_child), not in
|
|
393
|
+
# #start_layer. Controller/Job are top-level spans, so they stop last --
|
|
394
|
+
# meaning web?/job? only resolve as the request unwinds into recording.
|
|
395
|
+
# (Negative results aren't memoized, so an early false doesn't poison a
|
|
396
|
+
# later check.)
|
|
375
397
|
def web?
|
|
376
398
|
layer_finder.controller != nil
|
|
377
399
|
end
|
data/lib/scout_apm/version.rb
CHANGED
data/lib/scout_apm.rb
CHANGED
|
@@ -102,6 +102,7 @@ require 'scout_apm/instruments/middleware_summary'
|
|
|
102
102
|
require 'scout_apm/instruments/middleware_detailed' # Disabled by default, see the file for more details
|
|
103
103
|
require 'scout_apm/instruments/rails_router'
|
|
104
104
|
require 'scout_apm/instruments/grape'
|
|
105
|
+
require 'scout_apm/instruments/graphql'
|
|
105
106
|
require 'scout_apm/instruments/sinatra'
|
|
106
107
|
require 'allocations'
|
|
107
108
|
|
|
@@ -3,6 +3,15 @@ require "test_helper"
|
|
|
3
3
|
require "scout_apm/agent_context"
|
|
4
4
|
|
|
5
5
|
class AgentContextTest < Minitest::Test
|
|
6
|
+
def test_bootstrap_logger_uses_scout_log_level
|
|
7
|
+
original_log_level = ENV["SCOUT_LOG_LEVEL"]
|
|
8
|
+
ENV["SCOUT_LOG_LEVEL"] = "error"
|
|
9
|
+
|
|
10
|
+
assert_equal ::Logger::ERROR, ScoutApm::AgentContext.new.logger.log_level
|
|
11
|
+
ensure
|
|
12
|
+
ENV["SCOUT_LOG_LEVEL"] = original_log_level
|
|
13
|
+
end
|
|
14
|
+
|
|
6
15
|
def test_has_error_service_ignored_exceptions
|
|
7
16
|
context = ScoutApm::AgentContext.new
|
|
8
17
|
assert ScoutApm::ErrorService::IgnoredExceptions, context.ignored_exceptions.class
|
|
@@ -26,4 +35,4 @@ class AgentContextTest < Minitest::Test
|
|
|
26
35
|
context.slow_request_policy.add(TestPolicy.new)
|
|
27
36
|
assert 5, context.slow_request_policy.policies
|
|
28
37
|
end
|
|
29
|
-
end
|
|
38
|
+
end
|
|
@@ -34,6 +34,19 @@ class GitRevisionTest < Minitest::Test
|
|
|
34
34
|
assert_equal 'heroku_slug', revision.sha
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def test_sha_from_heroku_build_commit
|
|
38
|
+
ENV['HEROKU_BUILD_COMMIT'] = 'heroku_build'
|
|
39
|
+
revision = ScoutApm::GitRevision.new(ScoutApm::AgentContext.new)
|
|
40
|
+
assert_equal 'heroku_build', revision.sha
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_sha_from_heroku_prefers_build_commit_over_deprecated_slug_commit
|
|
44
|
+
ENV['HEROKU_BUILD_COMMIT'] = 'heroku_build'
|
|
45
|
+
ENV['HEROKU_SLUG_COMMIT'] = 'heroku_slug'
|
|
46
|
+
revision = ScoutApm::GitRevision.new(ScoutApm::AgentContext.new)
|
|
47
|
+
assert_equal 'heroku_build', revision.sha
|
|
48
|
+
end
|
|
49
|
+
|
|
37
50
|
def test_sha_from_capistrano
|
|
38
51
|
Dir.mktmpdir do |dir|
|
|
39
52
|
context = context_with_file_in_root(File.join(dir, 'REVISION'), 'capistrano_sha')
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments")
|
|
2
|
+
|
|
3
|
+
require 'test_helper'
|
|
4
|
+
|
|
5
|
+
require 'grape'
|
|
6
|
+
require 'scout_apm/instruments/grape'
|
|
7
|
+
|
|
8
|
+
class GrapeTest < Minitest::Test
|
|
9
|
+
# Captured before the instrument is installed. On grape >= 3.3.0 #run is
|
|
10
|
+
# owned by the prepended Grape::Testing::RunBeforeEach module, not the
|
|
11
|
+
# Grape::Endpoint class itself.
|
|
12
|
+
GRAPE_ENDPOINT_OWNS_RUN = ::Grape::Endpoint.instance_method(:run).owner == ::Grape::Endpoint
|
|
13
|
+
|
|
14
|
+
class HelloAPI < ::Grape::API
|
|
15
|
+
format :json
|
|
16
|
+
|
|
17
|
+
get :hello do
|
|
18
|
+
{ message: 'hello' }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Install exactly once for the entire test run. Re-running an
|
|
23
|
+
# alias-method install would alias the wrapper onto itself.
|
|
24
|
+
def self.install_instruments
|
|
25
|
+
@install_instruments ||= begin
|
|
26
|
+
context = ScoutApm::AgentContext.new
|
|
27
|
+
instrument_manager = ScoutApm::InstrumentManager.new(context)
|
|
28
|
+
instance = ScoutApm::Instruments::Grape.new(context)
|
|
29
|
+
instance.install(prepend: instrument_manager.prepend_for_instrument?(instance.class))
|
|
30
|
+
instance
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def setup
|
|
35
|
+
@instance = self.class.install_instruments
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_installed
|
|
39
|
+
assert @instance.installed?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Grape >= 3.3.0 prepends Grape::Testing::RunBeforeEach in front of
|
|
43
|
+
# Grape::Endpoint#run. Building an alias-method chain on top of the
|
|
44
|
+
# prepended method caused infinite recursion (SystemStackError) as soon
|
|
45
|
+
# as any endpoint was called.
|
|
46
|
+
def test_endpoint_runs_without_infinite_recursion
|
|
47
|
+
status, _headers, body = HelloAPI.call(Rack::MockRequest.env_for('/hello'))
|
|
48
|
+
|
|
49
|
+
assert_equal 200, status
|
|
50
|
+
|
|
51
|
+
body_content = "".dup
|
|
52
|
+
body.each { |chunk| body_content << chunk }
|
|
53
|
+
assert_includes body_content, 'hello'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Either instrumentation method is valid: alias_method is the default,
|
|
57
|
+
# prepend is used when configured (`use_prepend`/`prepend_instruments`)
|
|
58
|
+
# or when the instrument falls back to it for safety.
|
|
59
|
+
def test_installs_using_prepend_or_alias_method
|
|
60
|
+
assert prepended? || aliased?, "expected Grape::Endpoint to be instrumented via prepend or alias_method"
|
|
61
|
+
|
|
62
|
+
unless GRAPE_ENDPOINT_OWNS_RUN
|
|
63
|
+
# On grape >= 3.3.0 #run is owned by a module prepended onto
|
|
64
|
+
# Grape::Endpoint, where an alias-method chain would recurse, so the
|
|
65
|
+
# instrument must have used prepend even though alias_method is the
|
|
66
|
+
# default.
|
|
67
|
+
assert prepended?, "grape >= 3.3.0 requires the prepend instrumentation method"
|
|
68
|
+
refute aliased?, "the alias-method chain must not be installed over a prepended #run"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def prepended?
|
|
75
|
+
::Grape::Endpoint.ancestors.include?(ScoutApm::Instruments::GrapeEndpointInstrumentsPrepend)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def aliased?
|
|
79
|
+
::Grape::Endpoint.method_defined?(:run_without_scout_instruments) ||
|
|
80
|
+
::Grape::Endpoint.private_method_defined?(:run_without_scout_instruments)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
# Tests for RequestManager's thread-local handling, in particular that a
|
|
4
|
+
# TrackedRequest created on one thread is NOT shared with another thread.
|
|
5
|
+
#
|
|
6
|
+
# ActionController::Live runs the controller action in a child thread and copies
|
|
7
|
+
# the parent (Rack) thread's raw Thread.current locals -- including
|
|
8
|
+
# :scout_request -- into it. Without protection both threads would share and
|
|
9
|
+
# concurrently mutate a single TrackedRequest, racing on @layers/@stopping,
|
|
10
|
+
# which drops the transaction and leaks layers. RequestManager must give the
|
|
11
|
+
# inheriting thread its own request instead.
|
|
12
|
+
class RequestManagerTest < Minitest::Test
|
|
13
|
+
def teardown
|
|
14
|
+
# Never let a test leak the thread-local into sibling tests.
|
|
15
|
+
Thread.current[:scout_request] = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_lookup_reuses_the_request_on_the_same_thread
|
|
19
|
+
first = ScoutApm::RequestManager.lookup
|
|
20
|
+
second = ScoutApm::RequestManager.lookup
|
|
21
|
+
|
|
22
|
+
assert_same first, second,
|
|
23
|
+
"lookup should return the same TrackedRequest on repeated calls within one thread"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_lookup_in_a_child_thread_that_inherited_the_local_gets_its_own_request
|
|
27
|
+
parent_request = ScoutApm::RequestManager.lookup
|
|
28
|
+
inherited = Thread.current[:scout_request]
|
|
29
|
+
|
|
30
|
+
child_request = nil
|
|
31
|
+
child_local_before = nil
|
|
32
|
+
|
|
33
|
+
t = Thread.new do
|
|
34
|
+
# Simulate ActionController::Live copying the parent's raw thread local
|
|
35
|
+
# into the streaming child thread.
|
|
36
|
+
Thread.current[:scout_request] = inherited
|
|
37
|
+
child_local_before = Thread.current[:scout_request]
|
|
38
|
+
|
|
39
|
+
child_request = ScoutApm::RequestManager.lookup
|
|
40
|
+
end
|
|
41
|
+
t.join
|
|
42
|
+
|
|
43
|
+
# The child observed the inherited (parent's) request as its raw local...
|
|
44
|
+
assert_same parent_request, child_local_before
|
|
45
|
+
|
|
46
|
+
# ...but lookup must NOT hand it the parent's request to mutate.
|
|
47
|
+
refute_nil child_request
|
|
48
|
+
refute_same parent_request, child_request,
|
|
49
|
+
"a thread that inherited another thread's :scout_request must get its own TrackedRequest"
|
|
50
|
+
|
|
51
|
+
# And the parent's request must be left untouched (still the parent's).
|
|
52
|
+
assert_same parent_request, Thread.current[:scout_request]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_find_treats_a_foreign_thread_request_as_absent
|
|
56
|
+
# A request that reports it was created on a different thread should not be
|
|
57
|
+
# returned by find (so lookup falls through to create).
|
|
58
|
+
foreign = ScoutApm::TrackedRequest.new(ScoutApm::Agent.instance.context, ScoutApm::FakeStore.new)
|
|
59
|
+
foreign.stubs(:creating_thread_id).returns(Thread.current.object_id + 1)
|
|
60
|
+
Thread.current[:scout_request] = foreign
|
|
61
|
+
|
|
62
|
+
assert_nil ScoutApm::RequestManager.find,
|
|
63
|
+
"find should treat a request created on another thread as absent"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_find_returns_a_same_thread_request
|
|
67
|
+
own = ScoutApm::RequestManager.lookup
|
|
68
|
+
assert_same own, ScoutApm::RequestManager.find
|
|
69
|
+
end
|
|
70
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: scout_apm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.
|
|
4
|
+
version: 6.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Derek Haynes
|
|
@@ -314,6 +314,7 @@ files:
|
|
|
314
314
|
- lib/scout_apm/instruments/active_record.rb
|
|
315
315
|
- lib/scout_apm/instruments/elasticsearch.rb
|
|
316
316
|
- lib/scout_apm/instruments/grape.rb
|
|
317
|
+
- lib/scout_apm/instruments/graphql.rb
|
|
317
318
|
- lib/scout_apm/instruments/http.rb
|
|
318
319
|
- lib/scout_apm/instruments/http_client.rb
|
|
319
320
|
- lib/scout_apm/instruments/httpx.rb
|
|
@@ -464,6 +465,7 @@ files:
|
|
|
464
465
|
- test/unit/instruments/fixtures/test/_test_partial.html.erb
|
|
465
466
|
- test/unit/instruments/fixtures/test/_test_partial_collection.html.erb
|
|
466
467
|
- test/unit/instruments/fixtures/test_view.html.erb
|
|
468
|
+
- test/unit/instruments/grape_test.rb
|
|
467
469
|
- test/unit/instruments/http_client_test.rb
|
|
468
470
|
- test/unit/instruments/http_test.rb
|
|
469
471
|
- test/unit/instruments/httpx_test.rb
|
|
@@ -484,6 +486,7 @@ files:
|
|
|
484
486
|
- test/unit/remote/route_test.rb
|
|
485
487
|
- test/unit/remote/server_test.rb
|
|
486
488
|
- test/unit/request_histograms_test.rb
|
|
489
|
+
- test/unit/request_manager_test.rb
|
|
487
490
|
- test/unit/sampling_test.rb
|
|
488
491
|
- test/unit/scored_item_set_test.rb
|
|
489
492
|
- test/unit/serializers/payload_serializer_test.rb
|
|
@@ -520,7 +523,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
520
523
|
- !ruby/object:Gem::Version
|
|
521
524
|
version: '0'
|
|
522
525
|
requirements: []
|
|
523
|
-
rubygems_version: 4.0.
|
|
526
|
+
rubygems_version: 4.0.16
|
|
524
527
|
specification_version: 4
|
|
525
528
|
summary: Ruby application performance monitoring
|
|
526
529
|
test_files: []
|