airbrake-ruby 4.5.1 → 4.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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