airbrake-ruby 4.6.0-java → 4.7.0-java

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
  SHA1:
3
- metadata.gz: d9bfac1b8aca0a9932a5c197842b335b1fbfe0c2
4
- data.tar.gz: 46ab44d8c2a29ee444f7d1fc332e9a9e3e8cefa4
3
+ metadata.gz: e49bb6ddb05f50704b2fb2bf52b680e1310b9234
4
+ data.tar.gz: b2fafa4ebceec499b04d23c9a8094bbfb5f59c66
5
5
  SHA512:
6
- metadata.gz: 3efe36caa116da7d9f2f77a8655dfa4b7a61ce152f89190c82160cd86258a8353e97d382e78a4c52e4a624f19c712f456143bafe972e3c44cb15feb2114deb21
7
- data.tar.gz: 410b1dc3de20748fa1ae020636b5051e1b96fe5f1a35e814152738df8d4a7dfe1425573418a73b8151ebc2fe16947375b281e8e74969deeaa7ba105d43643863
6
+ metadata.gz: 40adfc247b2a12a0bddbcf87ec654f28125954c64380bd31f662f496219496154ef017e61a369918dfff20055667e23453e6b0c6a7ae2fa049792854ca53091a
7
+ data.tar.gz: 02d13ebd1cc32aab0b9b60b273d26b37f627b13dac701ad81ecaebfd0a03b984d4bc7b17435264d78a90c32d5c5eb973348294aa98de0f4a3664fef16f855506
@@ -12,6 +12,7 @@ require 'airbrake-ruby/stashable'
12
12
  require 'airbrake-ruby/config'
13
13
  require 'airbrake-ruby/config/validator'
14
14
  require 'airbrake-ruby/promise'
15
+ require 'airbrake-ruby/thread_pool'
15
16
  require 'airbrake-ruby/sync_sender'
16
17
  require 'airbrake-ruby/async_sender'
17
18
  require 'airbrake-ruby/response'
@@ -252,6 +253,7 @@ module Airbrake
252
253
  # @return [void]
253
254
  def close
254
255
  notice_notifier.close
256
+ performance_notifier.close
255
257
  end
256
258
 
257
259
  # Pings the Airbrake Deploy API endpoint about the occurred deploy.
@@ -1,7 +1,5 @@
1
1
  module Airbrake
2
- # Responsible for sending notices to Airbrake asynchronously. The class
3
- # supports an unlimited number of worker threads and an unlimited queue size
4
- # (both values are configurable).
2
+ # Responsible for sending notices to Airbrake asynchronously.
5
3
  #
6
4
  # @see SyncSender
7
5
  # @api private
@@ -15,25 +13,9 @@ module Airbrake
15
13
  "and the following notice will not be delivered " \
16
14
  "Error: %<type>s - %<message>s\nBacktrace: %<backtrace>s\n".freeze
17
15
 
18
- # @return [ThreadGroup] the list of workers
19
- # @note This is exposed for eaiser unit testing
20
- # @since v4.0.0
21
- attr_reader :workers
22
-
23
- # @return [Array<[Airbrake::Notice,Airbrake::Promise]>] the list of unsent
24
- # payload
25
- # @note This is exposed for eaiser unit testing
26
- # @since v4.0.0
27
- attr_reader :unsent
28
-
29
- def initialize
16
+ def initialize(method = :post)
30
17
  @config = Airbrake::Config.instance
31
- @unsent = SizedQueue.new(Airbrake::Config.instance.queue_size)
32
- @sender = SyncSender.new
33
- @closed = false
34
- @workers = ThreadGroup.new
35
- @mutex = Mutex.new
36
- @pid = nil
18
+ @method = method
37
19
  end
38
20
 
39
21
  # Asynchronously sends a notice to Airbrake.
@@ -41,83 +23,39 @@ module Airbrake
41
23
  # @param [Airbrake::Notice] notice A notice that was generated by the
42
24
  # library
43
25
  # @return [Airbrake::Promise]
44
- def send(notice, promise)
45
- return will_not_deliver(notice, promise) if @unsent.size >= @unsent.max
26
+ def send(notice, promise, endpoint = @config.endpoint)
27
+ unless thread_pool << [notice, promise, endpoint]
28
+ return will_not_deliver(notice, promise)
29
+ end
46
30
 
47
- @unsent << [notice, promise]
48
31
  promise
49
32
  end
50
33
 
51
- # Closes the instance making it a no-op (it shut downs all worker
52
- # threads). Before closing, waits on all unsent notices to be sent.
53
- #
54
34
  # @return [void]
55
- # @raise [Airbrake::Error] when invoked more than one time
56
35
  def close
57
- threads = @mutex.synchronize do
58
- raise Airbrake::Error, 'attempted to close already closed sender' if closed?
59
-
60
- unless @unsent.empty?
61
- msg = "#{LOG_LABEL} waiting to send #{@unsent.size} unsent notice(s)..."
62
- logger.debug(msg + ' (Ctrl-C to abort)')
63
- end
64
-
65
- @config.workers.times { @unsent << [:stop, Airbrake::Promise.new] }
66
- @closed = true
67
- @workers.list.dup
68
- end
69
-
70
- threads.each(&:join)
71
- logger.debug("#{LOG_LABEL} closed")
36
+ thread_pool.close
72
37
  end
73
38
 
74
- # Checks whether the sender is closed and thus usable.
75
39
  # @return [Boolean]
76
40
  def closed?
77
- @closed
41
+ thread_pool.closed?
78
42
  end
79
43
 
80
- # Checks if an active sender has any workers. A sender doesn't have any
81
- # workers only in two cases: when it was closed or when all workers
82
- # crashed. An *active* sender doesn't have any workers only when something
83
- # went wrong.
84
- #
85
- # Workers are expected to crash when you +fork+ the process the workers are
86
- # living in. In this case we detect a +fork+ and try to revive them here.
87
- #
88
- # Another possible scenario that crashes workers is when you close the
89
- # instance on +at_exit+, but some other +at_exit+ hook prevents the process
90
- # from exiting.
91
- #
92
- # @return [Boolean] true if an instance wasn't closed, but has no workers
93
- # @see https://goo.gl/oydz8h Example of at_exit that prevents exit
44
+ # @return [Boolean]
94
45
  def has_workers?
95
- @mutex.synchronize do
96
- return false if @closed
97
-
98
- if @pid != Process.pid && @workers.list.empty?
99
- @pid = Process.pid
100
- spawn_workers
101
- end
102
-
103
- !@closed && @workers.list.any?
104
- end
46
+ thread_pool.has_workers?
105
47
  end
106
48
 
107
49
  private
108
50
 
109
- def spawn_workers
110
- @workers = ThreadGroup.new
111
- @config.workers.times { @workers.add(spawn_worker) }
112
- @workers.enclose
113
- end
114
-
115
- def spawn_worker
116
- Thread.new do
117
- while (message = @unsent.pop)
118
- break if message.first == :stop
119
- @sender.send(*message)
120
- end
51
+ def thread_pool
52
+ @thread_pool ||= begin
53
+ sender = SyncSender.new(@method)
54
+ ThreadPool.new(
55
+ worker_size: @config.workers,
56
+ queue_size: @config.queue_size,
57
+ block: proc { |args| sender.send(*args) }
58
+ )
121
59
  end
122
60
  end
123
61
 
@@ -128,7 +66,7 @@ module Airbrake
128
66
  format(
129
67
  WILL_NOT_DELIVER_MSG,
130
68
  log_label: LOG_LABEL,
131
- capacity: @unsent.max,
69
+ capacity: @config.queue_size,
132
70
  type: error[:type],
133
71
  message: error[:message],
134
72
  backtrace: error[:backtrace].map do |line|
@@ -136,7 +74,7 @@ module Airbrake
136
74
  end.join("\n")
137
75
  )
138
76
  )
139
- promise.reject("AsyncSender has reached its capacity of #{@unsent.max}")
77
+ promise.reject("AsyncSender has reached its capacity of #{@config.queue_size}")
140
78
  end
141
79
  end
142
80
  end
@@ -40,6 +40,12 @@ module Airbrake
40
40
  data.empty?
41
41
  end
42
42
 
43
+ # @since ?.?.?
44
+ # @return [void]
45
+ def self.reset
46
+ @data = {}
47
+ end
48
+
43
49
  def self.data
44
50
  @data ||= {}
45
51
  end
@@ -37,6 +37,10 @@ module Airbrake
37
37
  # rubocop:enable Metrics/LineLength
38
38
  }.freeze
39
39
 
40
+ # @return [Regexp] the regexp that is applied after the feature regexps
41
+ # were used
42
+ POST_FILTER = /(?<=[values|in ]\().+(?=\))/i
43
+
40
44
  # @return [Hash{Symbol=>Array<Symbol>}] a set of features that corresponds
41
45
  # to a certain dialect
42
46
  DIALECT_FEATURES = {
@@ -75,6 +79,16 @@ module Airbrake
75
79
  oracle_enhanced: %r{'|/\*|\*/}
76
80
  }.freeze
77
81
 
82
+ # @return [Array<Regexp>] the list of queries to be ignored
83
+ IGNORED_QUERIES = [
84
+ /\ACOMMIT/i,
85
+ /\ABEGIN/i,
86
+ /\ASET/i,
87
+ /\ASHOW/i,
88
+ /\AWITH/i,
89
+ /FROM pg_attribute/i
90
+ ].freeze
91
+
78
92
  def initialize(dialect)
79
93
  @dialect =
80
94
  case dialect
@@ -95,7 +109,14 @@ module Airbrake
95
109
  def call(resource)
96
110
  return unless resource.respond_to?(:query)
97
111
 
98
- q = resource.query.gsub(@regexp, FILTERED)
112
+ query = resource.query
113
+ if IGNORED_QUERIES.any? { |q| q =~ query }
114
+ resource.ignore!
115
+ return
116
+ end
117
+
118
+ q = query.gsub(@regexp, FILTERED)
119
+ q.gsub!(POST_FILTER, FILTERED) if q =~ POST_FILTER
99
120
  q = ERROR_MSG if UNMATCHED_PAIR[@dialect] =~ q
100
121
  resource.query = q
101
122
  end
@@ -21,6 +21,7 @@ module Airbrake
21
21
  start_time:,
22
22
  end_time: Time.now
23
23
  )
24
+ @start_time_utc = TimeTruncate.utc_truncate_minutes(start_time)
24
25
  super(method, route, response_type, groups, start_time, end_time)
25
26
  end
26
27
 
@@ -37,7 +38,7 @@ module Airbrake
37
38
  'method' => method,
38
39
  'route' => route,
39
40
  'responseType' => response_type,
40
- 'time' => TimeTruncate.utc_truncate_minutes(start_time)
41
+ 'time' => @start_time_utc
41
42
  }.delete_if { |_key, val| val.nil? }
42
43
  end
43
44
  end
@@ -11,11 +11,12 @@ module Airbrake
11
11
  def initialize
12
12
  @config = Airbrake::Config.instance
13
13
  @flush_period = Airbrake::Config.instance.performance_stats_flush_period
14
- @sender = SyncSender.new(:put)
14
+ @sender = AsyncSender.new(:put)
15
15
  @payload = {}
16
16
  @schedule_flush = nil
17
17
  @mutex = Mutex.new
18
18
  @filter_chain = FilterChain.new
19
+ @waiting = false
19
20
  end
20
21
 
21
22
  # @param [Hash] resource
@@ -33,10 +34,10 @@ module Airbrake
33
34
 
34
35
  @mutex.synchronize do
35
36
  update_payload(resource)
36
- @flush_period > 0 ? schedule_flush(promise) : send(@payload, promise)
37
+ @flush_period > 0 ? schedule_flush : send(@payload, promise)
37
38
  end
38
39
 
39
- promise
40
+ promise.resolve(:success)
40
41
  end
41
42
 
42
43
  # @see Airbrake.add_performance_filter
@@ -49,6 +50,14 @@ module Airbrake
49
50
  @filter_chain.delete_filter(filter_class)
50
51
  end
51
52
 
53
+ def close
54
+ @mutex.synchronize do
55
+ @schedule_flush.kill if @schedule_flush
56
+ @sender.close
57
+ logger.debug("#{LOG_LABEL} performance notifier closed")
58
+ end
59
+ end
60
+
52
61
  private
53
62
 
54
63
  def update_payload(resource)
@@ -61,18 +70,39 @@ module Airbrake
61
70
  end
62
71
  end
63
72
 
64
- def schedule_flush(promise)
65
- @schedule_flush ||= Thread.new do
66
- sleep(@flush_period)
73
+ def schedule_flush
74
+ return if @payload.empty?
67
75
 
68
- payload = nil
69
- @mutex.synchronize do
70
- payload = @payload
71
- @payload = {}
72
- @schedule_flush = nil
76
+ if @schedule_flush && @schedule_flush.status == 'sleep' && @waiting
77
+ begin
78
+ @schedule_flush.run
79
+ rescue ThreadError => exception
80
+ logger.error("#{LOG_LABEL}: error occurred while flushing: #{exception}")
73
81
  end
82
+ end
83
+
84
+ @schedule_flush ||= spawn_timer
85
+ end
86
+
87
+ def spawn_timer
88
+ Thread.new do
89
+ loop do
90
+ if @payload.none?
91
+ @waiting = true
92
+ Thread.stop
93
+ @waiting = false
94
+ end
95
+
96
+ sleep(@flush_period)
74
97
 
75
- send(payload, promise)
98
+ payload = nil
99
+ @mutex.synchronize do
100
+ payload = @payload
101
+ @payload = {}
102
+ end
103
+
104
+ send(payload, Airbrake::Promise.new)
105
+ end
76
106
  end
77
107
  end
78
108
 
@@ -22,6 +22,7 @@ module Airbrake
22
22
  start_time:,
23
23
  end_time: Time.now
24
24
  )
25
+ @start_time_utc = TimeTruncate.utc_truncate_minutes(start_time)
25
26
  super(method, route, query, func, file, line, start_time, end_time)
26
27
  end
27
28
 
@@ -42,7 +43,7 @@ module Airbrake
42
43
  'method' => method,
43
44
  'route' => route,
44
45
  'query' => query,
45
- 'time' => TimeTruncate.utc_truncate_minutes(start_time),
46
+ 'time' => @start_time_utc,
46
47
  'function' => func,
47
48
  'file' => file,
48
49
  'line' => line
@@ -17,6 +17,7 @@ module Airbrake
17
17
  start_time:,
18
18
  end_time: Time.now
19
19
  )
20
+ @start_time_utc = TimeTruncate.utc_truncate_minutes(start_time)
20
21
  super(method, route, status_code, start_time, end_time)
21
22
  end
22
23
 
@@ -37,7 +38,7 @@ module Airbrake
37
38
  'method' => method,
38
39
  'route' => route,
39
40
  'statusCode' => status_code,
40
- 'time' => TimeTruncate.utc_truncate_minutes(start_time)
41
+ 'time' => @start_time_utc
41
42
  }.delete_if { |_key, val| val.nil? }
42
43
  end
43
44
  end
@@ -0,0 +1,128 @@
1
+ module Airbrake
2
+ # ThreadPool implements a simple thread pool that can configure the number of
3
+ # worker threads and the size of the queue to process.
4
+ #
5
+ # @example
6
+ # # Initialize a new thread pool with 5 workers and a queue size of 100. Set
7
+ # # the block to be run concurrently.
8
+ # thread_pool = ThreadPool.new(
9
+ # worker_size: 5,
10
+ # queue_size: 100,
11
+ # block: proc { |message| print "ECHO: #{message}..."}
12
+ # )
13
+ #
14
+ # # Send work.
15
+ # 10.times { |i| thread_pool << i }
16
+ # #=> ECHO: 0...ECHO: 1...ECHO: 2...
17
+ #
18
+ # @api private
19
+ # @since v4.6.1
20
+ class ThreadPool
21
+ include Loggable
22
+
23
+ # @return [ThreadGroup] the list of workers
24
+ # @note This is exposed for eaiser unit testing
25
+ attr_reader :workers
26
+
27
+ def initialize(worker_size:, queue_size:, block:)
28
+ @worker_size = worker_size
29
+ @queue_size = queue_size
30
+ @block = block
31
+
32
+ @queue = SizedQueue.new(queue_size)
33
+ @workers = ThreadGroup.new
34
+ @mutex = Mutex.new
35
+ @pid = nil
36
+ @closed = false
37
+
38
+ has_workers?
39
+ end
40
+
41
+ # Adds a new message to the thread pool. Rejects messages if the queue is at
42
+ # its capacity.
43
+ #
44
+ # @param [Object] message The message that gets passed to the block
45
+ # @return [Boolean] true if the message was successfully sent to the pool,
46
+ # false if the queue is full
47
+ def <<(message)
48
+ return false if backlog >= @queue_size
49
+ @queue << message
50
+ true
51
+ end
52
+
53
+ # @return [Integer] how big the queue is at the moment
54
+ def backlog
55
+ @queue.size
56
+ end
57
+
58
+ # Checks if a thread pool has any workers. A thread pool doesn't have any
59
+ # workers only in two cases: when it was closed or when all workers
60
+ # crashed. An *active* thread pool doesn't have any workers only when
61
+ # something went wrong.
62
+ #
63
+ # Workers are expected to crash when you +fork+ the process the workers are
64
+ # living in. In this case we detect a +fork+ and try to revive them here.
65
+ #
66
+ # Another possible scenario that crashes workers is when you close the
67
+ # instance on +at_exit+, but some other +at_exit+ hook prevents the process
68
+ # from exiting.
69
+ #
70
+ # @return [Boolean] true if an instance wasn't closed, but has no workers
71
+ # @see https://goo.gl/oydz8h Example of at_exit that prevents exit
72
+ def has_workers?
73
+ @mutex.synchronize do
74
+ return false if @closed
75
+
76
+ if @pid != Process.pid && @workers.list.empty?
77
+ @pid = Process.pid
78
+ spawn_workers
79
+ end
80
+
81
+ !@closed && @workers.list.any?
82
+ end
83
+ end
84
+
85
+ # Closes the thread pool making it a no-op (it shut downs all worker
86
+ # threads). Before closing, waits on all unprocessed tasks to be processed.
87
+ #
88
+ # @return [void]
89
+ # @raise [Airbrake::Error] when invoked more than one time
90
+ def close
91
+ threads = @mutex.synchronize do
92
+ raise Airbrake::Error, 'this thread pool is closed already' if @closed
93
+
94
+ unless @queue.empty?
95
+ msg = "#{LOG_LABEL} waiting to process #{@queue.size} task(s)..."
96
+ logger.debug(msg + ' (Ctrl-C to abort)')
97
+ end
98
+
99
+ @worker_size.times { @queue << :stop }
100
+ @closed = true
101
+ @workers.list.dup
102
+ end
103
+
104
+ threads.each(&:join)
105
+ logger.debug("#{LOG_LABEL} thread pool closed")
106
+ end
107
+
108
+ def closed?
109
+ @closed
110
+ end
111
+
112
+ def spawn_workers
113
+ @worker_size.times { @workers.add(spawn_worker) }
114
+ @workers.enclose
115
+ end
116
+
117
+ private
118
+
119
+ def spawn_worker
120
+ Thread.new do
121
+ while (message = @queue.pop)
122
+ break if message == :stop
123
+ @block.call(message)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -2,5 +2,5 @@
2
2
  # More information: http://semver.org/
3
3
  module Airbrake
4
4
  # @return [String] the library version
5
- AIRBRAKE_RUBY_VERSION = '4.6.0'.freeze
5
+ AIRBRAKE_RUBY_VERSION = '4.7.0'.freeze
6
6
  end
@@ -8,148 +8,65 @@ RSpec.describe Airbrake::AsyncSender do
8
8
  Airbrake::Config.instance = Airbrake::Config.new(
9
9
  project_id: '1',
10
10
  workers: 3,
11
- queue_size: queue_size
11
+ queue_size: 10
12
12
  )
13
-
14
- allow(Airbrake::Loggable.instance).to receive(:debug)
15
- expect(subject).to have_workers
16
13
  end
17
14
 
18
15
  describe "#send" do
19
- it "sends payload to Airbrake" do
20
- 2.times do
21
- subject.send(notice, Airbrake::Promise.new)
22
- end
23
- subject.close
24
-
25
- expect(a_request(:post, endpoint)).to have_been_made.twice
26
- end
27
-
28
- context "when the queue is full" do
29
- before do
30
- allow(subject.unsent).to receive(:size).and_return(queue_size)
31
- end
32
-
33
- it "discards payload" do
34
- 200.times do
35
- subject.send(notice, Airbrake::Promise.new)
36
- end
16
+ context "when sender has the capacity to send" do
17
+ it "sends notices to Airbrake" do
18
+ 2.times { subject.send(notice, Airbrake::Promise.new) }
37
19
  subject.close
38
20
 
39
- expect(a_request(:post, endpoint)).not_to have_been_made
21
+ expect(a_request(:post, endpoint)).to have_been_made.twice
40
22
  end
41
23
 
42
- it "logs discarded payload" do
43
- expect(Airbrake::Loggable.instance).to receive(:error).with(
44
- /reached its capacity/
45
- ).exactly(15).times
46
-
47
- 15.times do
48
- subject.send(notice, Airbrake::Promise.new)
49
- end
50
- subject.close
51
- end
52
-
53
- it "returns a rejected promise" do
24
+ it "returns a resolved promise" do
54
25
  promise = Airbrake::Promise.new
55
- 200.times { subject.send(notice, promise) }
56
- expect(promise.value).to eq(
57
- 'error' => "AsyncSender has reached its capacity of #{queue_size}"
58
- )
59
- end
60
- end
61
- end
62
-
63
- describe "#close" do
64
- context "when there are no unsent notices" do
65
- it "joins the spawned thread" do
66
- workers = subject.workers.list
67
- expect(workers).to all(be_alive)
68
-
26
+ subject.send(notice, promise)
69
27
  subject.close
70
- expect(workers).to all(be_stop)
28
+
29
+ expect(promise).to be_resolved
71
30
  end
72
31
  end
73
32
 
74
- context "when there are some unsent notices" do
75
- it "logs how many notices are left to send" do
76
- expect(Airbrake::Loggable.instance).to receive(:debug).with(
77
- /waiting to send \d+ unsent notice\(s\)/
33
+ context "when sender has exceeded the capacity to send" do
34
+ before do
35
+ Airbrake::Config.instance = Airbrake::Config.new(
36
+ project_id: '1',
37
+ workers: 0,
38
+ queue_size: 1
78
39
  )
79
- expect(Airbrake::Loggable.instance).to receive(:debug).with(/closed/)
80
-
81
- 300.times { subject.send(notice, Airbrake::Promise.new) }
82
- subject.close
83
40
  end
84
41
 
85
- it "waits until the unsent notices queue is empty" do
42
+ it "doesn't send the exceeded notices to Airbrake" do
43
+ 15.times { subject.send(notice, Airbrake::Promise.new) }
86
44
  subject.close
87
- expect(subject.unsent.size).to be_zero
88
- end
89
- end
90
45
 
91
- context "when it was already closed" do
92
- it "doesn't increase the unsent queue size" do
93
- begin
94
- subject.close
95
- rescue Airbrake::Error
96
- nil
97
- end
98
-
99
- expect(subject.unsent.size).to be_zero
46
+ expect(a_request(:post, endpoint)).not_to have_been_made
100
47
  end
101
48
 
102
- it "raises error" do
49
+ it "returns a rejected promise" do
50
+ promise = nil
51
+ 15.times do
52
+ promise = subject.send(notice, Airbrake::Promise.new)
53
+ end
103
54
  subject.close
104
55
 
105
- expect(subject).to be_closed
106
- expect { subject.close }.to raise_error(
107
- Airbrake::Error, 'attempted to close already closed sender'
56
+ expect(promise).to be_rejected
57
+ expect(promise.value).to eq(
58
+ 'error' => "AsyncSender has reached its capacity of 1"
108
59
  )
109
60
  end
110
- end
111
61
 
112
- context "when workers were not spawned" do
113
- it "correctly closes the notifier nevertheless" do
114
- subject.close
115
- expect(subject).to be_closed
116
- end
117
- end
118
- end
62
+ it "logs discarded notice" do
63
+ expect(Airbrake::Loggable.instance).to receive(:error).with(
64
+ /reached its capacity/
65
+ ).at_least(:once)
119
66
 
120
- describe "#has_workers?" do
121
- it "returns false when the sender is not closed, but has 0 workers" do
122
- subject.workers.list.each do |worker|
123
- worker.kill.join
67
+ 15.times { subject.send(notice, Airbrake::Promise.new) }
68
+ subject.close
124
69
  end
125
- expect(subject).not_to have_workers
126
- end
127
-
128
- it "returns false when the sender is closed" do
129
- subject.close
130
- expect(subject).not_to have_workers
131
- end
132
-
133
- it "respawns workers on fork()", skip: %w[jruby rbx].include?(RUBY_ENGINE) do
134
- pid = fork { expect(subject).to have_workers }
135
- Process.wait(pid)
136
- subject.close
137
- expect(subject).not_to have_workers
138
- end
139
- end
140
-
141
- describe "#spawn_workers" do
142
- it "spawns alive threads in an enclosed ThreadGroup" do
143
- expect(subject.workers).to be_a(ThreadGroup)
144
- expect(subject.workers.list).to all(be_alive)
145
- expect(subject.workers).to be_enclosed
146
-
147
- subject.close
148
- end
149
-
150
- it "spawns exactly config.workers workers" do
151
- expect(subject.workers.list.size).to eq(Airbrake::Config.instance.workers)
152
- subject.close
153
70
  end
154
71
  end
155
72
  end
@@ -1,8 +1,6 @@
1
1
  RSpec.describe Airbrake::FileCache do
2
- after do
3
- %i[banana mango].each { |k| described_class.delete(k) }
4
- expect(described_class).to be_empty
5
- end
2
+ before { described_class.reset }
3
+ after { described_class.reset }
6
4
 
7
5
  describe ".[]=" do
8
6
  context "when cache limit isn't reached" do
@@ -10,6 +10,18 @@ RSpec.describe Airbrake::Filters::SqlFilter do
10
10
  end
11
11
  end
12
12
 
13
+ shared_examples "query blacklisting" do |query, opts|
14
+ it "ignores '#{query}'" do
15
+ filter = described_class.new('postgres')
16
+ q = Airbrake::Query.new(
17
+ query: query, method: 'GET', route: '/', start_time: Time.now
18
+ )
19
+ filter.call(q)
20
+
21
+ expect(q.ignored?).to eq(opts[:should_ignore])
22
+ end
23
+ end
24
+
13
25
  ALL_DIALECTS = %i[mysql postgres sqlite cassandra oracle].freeze
14
26
 
15
27
  # rubocop:disable Metrics/LineLength
@@ -60,7 +72,11 @@ RSpec.describe Airbrake::Filters::SqlFilter do
60
72
  dialects: %i[postgres]
61
73
  }, {
62
74
  input: "INSERT INTO `X` values(\"test\",0, 1 , 2, 'test')",
63
- output: "INSERT INTO `X` values(?,?, ? , ?, ?)",
75
+ output: "INSERT INTO `X` values(?)",
76
+ dialects: %i[mysql]
77
+ }, {
78
+ input: "INSERT INTO `X` values(\"test\",0, 1 , 2, 'test')",
79
+ output: "INSERT INTO `X` values(?)",
64
80
  dialects: %i[mysql]
65
81
  }, {
66
82
  input: "SELECT c11.col1, c22.col2 FROM table c11, table c22 WHERE value='nothing'",
@@ -68,7 +84,7 @@ RSpec.describe Airbrake::Filters::SqlFilter do
68
84
  dialects: ALL_DIALECTS
69
85
  }, {
70
86
  input: "INSERT INTO X VALUES(1, 23456, 123.456, 99+100)",
71
- output: "INSERT INTO X VALUES(?, ?, ?, ?+?)",
87
+ output: "INSERT INTO X VALUES(?)",
72
88
  dialects: ALL_DIALECTS
73
89
  }, {
74
90
  input: "SELECT * FROM table WHERE name=\"foo\" AND value=\"don't\"",
@@ -117,7 +133,7 @@ RSpec.describe Airbrake::Filters::SqlFilter do
117
133
  dialects: ALL_DIALECTS
118
134
  }, {
119
135
  input: "INSERT INTO X values('', 'a''b c',0, 1 , 'd''e f''s h')",
120
- output: "INSERT INTO X values(?, ?,?, ? , ?)",
136
+ output: "INSERT INTO X values(?)",
121
137
  dialects: ALL_DIALECTS
122
138
  }, {
123
139
  input: "SELECT * FROM t WHERE -- '\n bar='baz' -- '",
@@ -153,7 +169,7 @@ RSpec.describe Airbrake::Filters::SqlFilter do
153
169
  dialects: %i[postgres]
154
170
  }, {
155
171
  input: "INSERT INTO \"foo\" (\"bar\", \"baz\", \"qux\") VALUES ($1, $2, $3) RETURNING \"id\"",
156
- output: "INSERT INTO \"foo\" (\"bar\", \"baz\", \"qux\") VALUES ($?, $?, $?) RETURNING \"id\"",
172
+ output: "INSERT INTO \"foo\" (?) RETURNING \"id\"",
157
173
  dialects: %i[postgres]
158
174
  }, {
159
175
  input: "select * from foo where bar = 'some\\tthing' and baz = 10",
@@ -216,4 +232,31 @@ RSpec.describe Airbrake::Filters::SqlFilter do
216
232
  include_examples 'query filtering', test
217
233
  end
218
234
  # rubocop:enable Metrics/LineLength
235
+
236
+ [
237
+ 'COMMIT',
238
+ 'commit',
239
+ 'BEGIN',
240
+ 'begin',
241
+ 'SET time zone ?',
242
+ 'set time zone ?',
243
+ 'SHOW max_identifier_length',
244
+ 'show max_identifier_length',
245
+
246
+ 'WITH pk_constraint AS ( SELECT conrelid, unnest(conkey) AS connum ' \
247
+ 'FROM pg_constraint WHERE contype = ? AND conrelid = ?::regclass ), ' \
248
+ 'cons AS ( SELECT conrelid, connum, row_number() OVER() AS rownum FROM ' \
249
+ 'pk_constraint ) SELECT attr.attname FROM pg_attribute attr INNER JOIN ' \
250
+ 'cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.connum ' \
251
+ 'ORDER BY cons.rownum'
252
+
253
+ ].each do |query|
254
+ include_examples 'query blacklisting', query, should_ignore: true
255
+ end
256
+
257
+ [
258
+ 'UPDATE "users" SET "last_sign_in_at" = ? WHERE "users"."id" = ?'
259
+ ].each do |query|
260
+ include_examples 'query blacklisting', query, should_ignore: false
261
+ end
219
262
  end
@@ -245,12 +245,12 @@ RSpec.describe Airbrake::Filters::ThreadFilter do
245
245
 
246
246
  it "appends thread inspect (self)" do
247
247
  subject.call(notice)
248
- expect(notice[:params][:thread][:self]).to match(/\A#<Thread:.+ run>\z/)
248
+ expect(notice[:params][:thread][:self]).to match(/\A#<Thread:.+>\z/)
249
249
  end
250
250
 
251
251
  it "appends thread group" do
252
252
  subject.call(notice)
253
- expect(notice[:params][:thread][:group][0]).to match(/\A#<Thread:.+ run>\z/)
253
+ expect(notice[:params][:thread][:group][0]).to match(/\A#<Thread:.+>\z/)
254
254
  end
255
255
 
256
256
  it "appends priority" do
@@ -31,6 +31,7 @@ RSpec.describe Airbrake::PerformanceNotifier do
31
31
  end_time: Time.new(2018, 1, 1, 0, 50, 0, 0)
32
32
  )
33
33
  )
34
+ subject.close
34
35
 
35
36
  expect(
36
37
  a_request(:put, queries).with(body: %r|
@@ -60,6 +61,7 @@ RSpec.describe Airbrake::PerformanceNotifier do
60
61
  end_time: Time.new(2018, 1, 1, 0, 50, 0, 0)
61
62
  )
62
63
  )
64
+ subject.close
63
65
 
64
66
  expect(
65
67
  a_request(:put, routes).with(body: %r|
@@ -87,6 +89,7 @@ RSpec.describe Airbrake::PerformanceNotifier do
87
89
  groups: { db: 131, view: 421 }
88
90
  )
89
91
  )
92
+ subject.close
90
93
 
91
94
  expect(
92
95
  a_request(:put, breakdowns).with(body: %r|
@@ -126,6 +129,8 @@ RSpec.describe Airbrake::PerformanceNotifier do
126
129
  start_time: Time.new(2018, 1, 1, 0, 0, 20, 0)
127
130
  )
128
131
  )
132
+ subject.close
133
+
129
134
  expect(
130
135
  a_request(:put, routes).with(body: /"time":"2018-01-01T00:00:00\+00:00"/)
131
136
  ).to have_been_made
@@ -148,6 +153,8 @@ RSpec.describe Airbrake::PerformanceNotifier do
148
153
  start_time: Time.new(2018, 1, 1, 0, 0, 50, 0)
149
154
  )
150
155
  )
156
+ subject.close
157
+
151
158
  expect(
152
159
  a_request(:put, routes).with(body: /"count":2/)
153
160
  ).to have_been_made
@@ -172,6 +179,8 @@ RSpec.describe Airbrake::PerformanceNotifier do
172
179
  end_time: Time.new(2018, 1, 1, 0, 1, 55, 0)
173
180
  )
174
181
  )
182
+ subject.close
183
+
175
184
  expect(
176
185
  a_request(:put, routes).with(
177
186
  body: %r|\A
@@ -206,6 +215,8 @@ RSpec.describe Airbrake::PerformanceNotifier do
206
215
  end_time: Time.new(2018, 1, 1, 0, 50, 0, 0)
207
216
  )
208
217
  )
218
+ subject.close
219
+
209
220
  expect(
210
221
  a_request(:put, routes).with(
211
222
  body: %r|\A
@@ -242,6 +253,7 @@ RSpec.describe Airbrake::PerformanceNotifier do
242
253
  groups: { db: 55, view: 11 }
243
254
  )
244
255
  )
256
+ subject.close
245
257
 
246
258
  expect(
247
259
  a_request(:put, breakdowns).with(body: %r|
@@ -281,6 +293,8 @@ RSpec.describe Airbrake::PerformanceNotifier do
281
293
  start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
282
294
  )
283
295
  )
296
+ subject.close
297
+
284
298
  expect(promise).to be_an(Airbrake::Promise)
285
299
  expect(promise.value).to eq('' => nil)
286
300
  end
@@ -292,6 +306,7 @@ RSpec.describe Airbrake::PerformanceNotifier do
292
306
  expect(Airbrake::Config.instance).to receive(:check_performance_options)
293
307
  .with(request).and_return(Airbrake::Promise.new)
294
308
  subject.notify(request)
309
+ subject.close
295
310
  end
296
311
 
297
312
  it "sends environment when it's specified" do
@@ -305,6 +320,8 @@ RSpec.describe Airbrake::PerformanceNotifier do
305
320
  start_time: Time.new
306
321
  )
307
322
  )
323
+ subject.close
324
+
308
325
  expect(
309
326
  a_request(:put, routes).with(
310
327
  body: /\A{"routes":\[.+\],"environment":"test"}\z/x
@@ -369,6 +386,8 @@ RSpec.describe Airbrake::PerformanceNotifier do
369
386
  start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
370
387
  )
371
388
  )
389
+ subject.close
390
+
372
391
  expect(a_request(:put, routes)).not_to have_been_made
373
392
  end
374
393
 
@@ -381,6 +400,8 @@ RSpec.describe Airbrake::PerformanceNotifier do
381
400
  start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
382
401
  )
383
402
  )
403
+ subject.close
404
+
384
405
  expect(a_request(:put, queries)).not_to have_been_made
385
406
  end
386
407
  end
@@ -401,6 +422,8 @@ RSpec.describe Airbrake::PerformanceNotifier do
401
422
  start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
402
423
  )
403
424
  )
425
+ subject.close
426
+
404
427
  expect(
405
428
  a_request(:put, queries).with(
406
429
  body: /\A{"queries":\[{"method":"POST","route":"\[Filtered\]"/
@@ -410,6 +433,37 @@ RSpec.describe Airbrake::PerformanceNotifier do
410
433
  end
411
434
  end
412
435
 
436
+ describe "#close" do
437
+ before do
438
+ Airbrake::Config.instance.merge(performance_stats_flush_period: 0.1)
439
+ end
440
+
441
+ after do
442
+ Airbrake::Config.instance.merge(performance_stats_flush_period: 0)
443
+ end
444
+
445
+ it "kills the background thread" do
446
+ expect_any_instance_of(Thread).to receive(:kill).and_call_original
447
+ subject.notify(
448
+ Airbrake::Query.new(
449
+ method: 'POST',
450
+ route: '/foo',
451
+ query: 'SELECT * FROM things',
452
+ start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
453
+ )
454
+ )
455
+ subject.close
456
+ end
457
+
458
+ it "logs the exit message" do
459
+ allow(Airbrake::Loggable.instance).to receive(:debug)
460
+ expect(Airbrake::Loggable.instance).to receive(:debug).with(
461
+ /performance notifier closed/
462
+ )
463
+ subject.close
464
+ end
465
+ end
466
+
413
467
  describe "#delete_filter" do
414
468
  let(:filter) do
415
469
  Class.new do
@@ -429,6 +483,8 @@ RSpec.describe Airbrake::PerformanceNotifier do
429
483
  start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
430
484
  )
431
485
  )
486
+ subject.close
487
+
432
488
  expect(a_request(:put, routes)).to have_been_made
433
489
  end
434
490
  end
@@ -0,0 +1,158 @@
1
+ RSpec.describe Airbrake::ThreadPool do
2
+ let(:tasks) { [] }
3
+ let(:worker_size) { 1 }
4
+ let(:queue_size) { 2 }
5
+
6
+ subject do
7
+ described_class.new(
8
+ worker_size: worker_size,
9
+ queue_size: queue_size,
10
+ block: proc { |message| tasks << message }
11
+ )
12
+ end
13
+
14
+ describe "#<<" do
15
+ it "returns true" do
16
+ retval = subject << 1
17
+ subject.close
18
+ expect(retval).to eq(true)
19
+ end
20
+
21
+ it "performs work in background" do
22
+ subject << 2
23
+ subject << 1
24
+ subject.close
25
+
26
+ expect(tasks).to eq([2, 1])
27
+ end
28
+
29
+ context "when the queue is full" do
30
+ before do
31
+ allow(subject).to receive(:backlog).and_return(queue_size)
32
+ end
33
+
34
+ subject do
35
+ described_class.new(
36
+ worker_size: 1,
37
+ queue_size: 1,
38
+ block: proc { |message| tasks << message }
39
+ )
40
+ end
41
+
42
+ it "returns false" do
43
+ retval = subject << 1
44
+ subject.close
45
+ expect(retval).to eq(false)
46
+ end
47
+
48
+ it "discards tasks" do
49
+ 200.times { subject << 1 }
50
+ subject.close
51
+
52
+ expect(tasks.size).to be_zero
53
+ end
54
+ end
55
+ end
56
+
57
+ describe "#backlog" do
58
+ let(:worker_size) { 0 }
59
+
60
+ it "returns the size of the queue" do
61
+ subject << 1
62
+ expect(subject.backlog).to eq(1)
63
+ end
64
+ end
65
+
66
+ describe "#has_workers?" do
67
+ it "returns false when the thread pool is not closed, but has 0 workers" do
68
+ subject.workers.list.each do |worker|
69
+ worker.kill.join
70
+ end
71
+ expect(subject).not_to have_workers
72
+ end
73
+
74
+ it "returns false when the thread pool is closed" do
75
+ subject.close
76
+ expect(subject).not_to have_workers
77
+ end
78
+
79
+ it "respawns workers on fork()", skip: %w[jruby].include?(RUBY_ENGINE) do
80
+ pid = fork { expect(subject).to have_workers }
81
+ Process.wait(pid)
82
+ subject.close
83
+ expect(subject).not_to have_workers
84
+ end
85
+ end
86
+
87
+ describe "#close" do
88
+ context "when there's no work to do" do
89
+ it "joins the spawned thread" do
90
+ workers = subject.workers.list
91
+ expect(workers).to all(be_alive)
92
+
93
+ subject.close
94
+ expect(workers).to all(be_stop)
95
+ end
96
+ end
97
+
98
+ context "when there's some work to do" do
99
+ it "logs how many tasks are left to process" do
100
+ thread_pool = described_class.new(
101
+ worker_size: 0, queue_size: 2, block: proc {}
102
+ )
103
+
104
+ expect(Airbrake::Loggable.instance).to receive(:debug).with(
105
+ /waiting to process \d+ task\(s\)/
106
+ )
107
+ expect(Airbrake::Loggable.instance).to receive(:debug).with(/closed/)
108
+
109
+ 2.times { thread_pool << 1 }
110
+ thread_pool.close
111
+ end
112
+
113
+ it "waits until the queue gets empty" do
114
+ thread_pool = described_class.new(
115
+ worker_size: 1, queue_size: 2, block: proc {}
116
+ )
117
+
118
+ 10.times { subject << 1 }
119
+ thread_pool.close
120
+ expect(thread_pool.backlog).to be_zero
121
+ end
122
+ end
123
+
124
+ context "when it was already closed" do
125
+ it "doesn't increase the queue size" do
126
+ begin
127
+ subject.close
128
+ rescue Airbrake::Error
129
+ nil
130
+ end
131
+
132
+ expect(subject.backlog).to be_zero
133
+ end
134
+
135
+ it "raises error" do
136
+ subject.close
137
+ expect { subject.close }.to raise_error(
138
+ Airbrake::Error, 'this thread pool is closed already'
139
+ )
140
+ end
141
+ end
142
+ end
143
+
144
+ describe "#spawn_workers" do
145
+ it "spawns alive threads in an enclosed ThreadGroup" do
146
+ expect(subject.workers).to be_a(ThreadGroup)
147
+ expect(subject.workers.list).to all(be_alive)
148
+ expect(subject.workers).to be_enclosed
149
+
150
+ subject.close
151
+ end
152
+
153
+ it "spawns exactly `workers_size` workers" do
154
+ expect(subject.workers.list.size).to eq(worker_size)
155
+ subject.close
156
+ end
157
+ end
158
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: airbrake-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.6.0
4
+ version: 4.7.0
5
5
  platform: java
6
6
  authors:
7
7
  - Airbrake Technologies, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-05 00:00:00.000000000 Z
11
+ date: 2019-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rbtree-jruby
@@ -80,6 +80,7 @@ files:
80
80
  - lib/airbrake-ruby/stat.rb
81
81
  - lib/airbrake-ruby/sync_sender.rb
82
82
  - lib/airbrake-ruby/tdigest.rb
83
+ - lib/airbrake-ruby/thread_pool.rb
83
84
  - lib/airbrake-ruby/time_truncate.rb
84
85
  - lib/airbrake-ruby/timed_trace.rb
85
86
  - lib/airbrake-ruby/truncator.rb
@@ -92,7 +93,7 @@ files:
92
93
  - spec/config/validator_spec.rb
93
94
  - spec/config_spec.rb
94
95
  - spec/deploy_notifier_spec.rb
95
- - spec/file_cache.rb
96
+ - spec/file_cache_spec.rb
96
97
  - spec/filter_chain_spec.rb
97
98
  - spec/filters/context_filter_spec.rb
98
99
  - spec/filters/dependency_filter_spec.rb
@@ -118,8 +119,8 @@ files:
118
119
  - spec/inspectable_spec.rb
119
120
  - spec/monotonic_time_spec.rb
120
121
  - spec/nested_exception_spec.rb
122
+ - spec/notice_notifier/options_spec.rb
121
123
  - spec/notice_notifier_spec.rb
122
- - spec/notice_notifier_spec/options_spec.rb
123
124
  - spec/notice_spec.rb
124
125
  - spec/performance_breakdown_spec.rb
125
126
  - spec/performance_notifier_spec.rb
@@ -132,6 +133,7 @@ files:
132
133
  - spec/stat_spec.rb
133
134
  - spec/sync_sender_spec.rb
134
135
  - spec/tdigest_spec.rb
136
+ - spec/thread_pool_spec.rb
135
137
  - spec/time_truncate_spec.rb
136
138
  - spec/timed_trace_spec.rb
137
139
  - spec/truncator_spec.rb
@@ -187,6 +189,7 @@ test_files:
187
189
  - spec/notice_notifier_spec.rb
188
190
  - spec/time_truncate_spec.rb
189
191
  - spec/promise_spec.rb
192
+ - spec/thread_pool_spec.rb
190
193
  - spec/config/validator_spec.rb
191
194
  - spec/sync_sender_spec.rb
192
195
  - spec/ignorable_spec.rb
@@ -195,10 +198,11 @@ test_files:
195
198
  - spec/airbrake_spec.rb
196
199
  - spec/nested_exception_spec.rb
197
200
  - spec/timed_trace_spec.rb
201
+ - spec/file_cache_spec.rb
198
202
  - spec/request_spec.rb
203
+ - spec/notice_notifier/options_spec.rb
199
204
  - spec/filter_chain_spec.rb
200
205
  - spec/response_spec.rb
201
- - spec/file_cache.rb
202
206
  - spec/code_hunk_spec.rb
203
207
  - spec/fixtures/notroot.txt
204
208
  - spec/fixtures/project_root/long_line.txt
@@ -210,4 +214,3 @@ test_files:
210
214
  - spec/inspectable_spec.rb
211
215
  - spec/stashable_spec.rb
212
216
  - spec/query_spec.rb
213
- - spec/notice_notifier_spec/options_spec.rb