scout_apm 6.2.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c667c4b7279ecdc5107a30bb50ac2a42eda6403a257f6bf2a05a6452c1143f18
4
- data.tar.gz: d5183ae2f3fb7fbdfabae56616543f99503f59bb65b154e6709ea278f42166d3
3
+ metadata.gz: fe18e4045997fdf6b731025ee98c13e2702096a0cfd13ac5f0f651d7a7f4a4db
4
+ data.tar.gz: d9b4593ef2b8d5df4983d7b93cb9eaf5cc3f7e6916d8288ae9814e59dd913007
5
5
  SHA512:
6
- metadata.gz: bdc045802aa3d9cc37c147f6fd5a866e4bc3a3a49de480711595b3ca5b6dd04c314c2085bd229ccaebc0dca3125a7463f2bf0a53552adbb2caef8b0a09102de2
7
- data.tar.gz: 07a522e00f9d86d4d03a57c5e8965a9fd724104881429c8acddf603c0a0ad8829d098f998b3e551a6a0751ab1ba60af0f2c077bab1c3528d988138d2f4c2268e
6
+ metadata.gz: a5b838d8e12286496038899c7dd006b6c3d6507061bcba1a963fc10664fa96e36c603f71e315430eda9eeaa84dabadb21f91dee9e9aa9baf7009f6e03d8f3d9d
7
+ data.tar.gz: bee9ed547611139d56eb6f88cf6c1e730dc77dbdf575e89a8b1cd95f86756687174c78b64cf00acd389a74e90198f9750aec334e2dfba195340a0a9753c280bd
data/CHANGELOG.markdown CHANGED
@@ -1,5 +1,9 @@
1
1
  # Pending
2
2
 
3
+ # 6.3.0
4
+
5
+ - Fix span leak on `ActionController::Live` streaming requests (#631)
6
+
3
7
  # 6.2.1
4
8
 
5
9
  - Fix `SystemStackError` with `grape >= 3.3.0` by falling back to `prepend` instrumentation when Grape owns `Endpoint#run` itself (#624)
@@ -7,11 +7,23 @@ module ScoutApm
7
7
  find || create
8
8
  end
9
9
 
10
- # Get the current Thread local, and detecting, and not returning a stale request
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
- if req && (req.stopping? || req.recorded?)
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,20 @@ 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
+
80
91
  def layer_count
81
92
  @layers.size
82
93
  end
@@ -368,14 +379,21 @@ module ScoutApm
368
379
  end
369
380
  end
370
381
 
371
- # This request is a job transaction iff it has a 'Job' layer
372
- # Use this only during recording
382
+ # True iff this request has a 'Job' layer. Only reliable during recording --
383
+ # see #web? for why.
373
384
  def job?
374
385
  layer_finder.job != nil
375
386
  end
376
387
 
377
- # This request is a web transaction iff it has a 'Controller' layer
378
- # Use this only during recording
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.)
379
397
  def web?
380
398
  layer_finder.controller != nil
381
399
  end
@@ -1,3 +1,3 @@
1
1
  module ScoutApm
2
- VERSION = "6.2.1"
2
+ VERSION = "6.3.0"
3
3
  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.2.1
4
+ version: 6.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derek Haynes
@@ -486,6 +486,7 @@ files:
486
486
  - test/unit/remote/route_test.rb
487
487
  - test/unit/remote/server_test.rb
488
488
  - test/unit/request_histograms_test.rb
489
+ - test/unit/request_manager_test.rb
489
490
  - test/unit/sampling_test.rb
490
491
  - test/unit/scored_item_set_test.rb
491
492
  - test/unit/serializers/payload_serializer_test.rb