airbrake-ruby 4.5.1 → 4.7.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.
@@ -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.5.1'.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
@@ -21,6 +21,7 @@ RSpec.describe Airbrake::Config do
21
21
  its(:whitelist_keys) { is_expected.to be_empty }
22
22
  its(:performance_stats) { is_expected.to eq(true) }
23
23
  its(:performance_stats_flush_period) { is_expected.to eq(15) }
24
+ its(:query_stats) { is_expected.to eq(false) }
24
25
 
25
26
  describe "#new" do
26
27
  context "when user config is passed" do
@@ -106,4 +107,48 @@ RSpec.describe Airbrake::Config do
106
107
  its(:check_configuration) { is_expected.not_to be_rejected }
107
108
  end
108
109
  end
110
+
111
+ describe "#check_performance_options" do
112
+ it "returns a promise" do
113
+ resource = Airbrake::Query.new(
114
+ method: '', route: '', query: '', start_time: Time.now
115
+ )
116
+ expect(subject.check_performance_options(resource))
117
+ .to be_an(Airbrake::Promise)
118
+ end
119
+
120
+ context "when performance stats are disabled" do
121
+ before { subject.performance_stats = false }
122
+
123
+ let(:resource) do
124
+ Airbrake::Request.new(
125
+ method: 'GET', route: '/foo', status_code: 200, start_time: Time.new
126
+ )
127
+ end
128
+
129
+ it "returns a rejected promise" do
130
+ promise = subject.check_performance_options(resource)
131
+ expect(promise.value).to eq(
132
+ 'error' => "The Performance Stats feature is disabled"
133
+ )
134
+ end
135
+ end
136
+
137
+ context "when query stats are disabled" do
138
+ before { subject.query_stats = false }
139
+
140
+ let(:resource) do
141
+ Airbrake::Query.new(
142
+ method: 'GET', route: '/foo', query: '', start_time: Time.new
143
+ )
144
+ end
145
+
146
+ it "returns a rejected promise" do
147
+ promise = subject.check_performance_options(resource)
148
+ expect(promise.value).to eq(
149
+ 'error' => "The Query Stats feature is disabled"
150
+ )
151
+ end
152
+ end
153
+ end
109
154
  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