statsd-instrument 3.3.0 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 62f90038a90bccc54c0fe5ac2fe1a2449e8662183e9b9a0cac209e5ae22a07c7
4
- data.tar.gz: e620363a10bff05710ce52f6869364e378a0979e2ef53cc86c56f8c7fe491d91
3
+ metadata.gz: bcbaac2cd4178c61bfcb484c45931bb387d81dfa632fbd114bba126c393beb75
4
+ data.tar.gz: d1101fbc534b6612ff76a282ab45d37aad3a0c185b3ccedf6dcf2ef78ce85bb1
5
5
  SHA512:
6
- metadata.gz: 18836250885562c7862db1e515c8f8433e43cf795b886800e8a40fae7e7ebead2a120656e6f7654d5c3b87c9f364d3861441593f42dfda7fa69479146b800842
7
- data.tar.gz: 6915ee31b5bab72a8d52ef588f6fa90e1df4e821e1aaa96cf7523b1392d49bdc56003de4e3f09e818aee21e2d094e9671b2b2297e7b3f597ca193dafbf03824a
6
+ metadata.gz: 4cd62e31fe1dc59ae49345e598de791ba1295a18d527d88f840de7349d31e8d325cf00543e59eddf02c3fed56a58f9020ace88ca7554de2340d193e8e62cb1d4
7
+ data.tar.gz: 98fcae4a9b924ab432d745ad2c02f4ac5ed6d0e94ad1f002e80afc0f4f741f01412dbf605151813432dc828ad57f9c1747e26a7127c32950df9ffddeb90e9523
@@ -13,12 +13,15 @@ jobs:
13
13
  - name: Set up Ruby
14
14
  uses: ruby/setup-ruby@v1
15
15
  with:
16
- ruby-version: 2.6
16
+ ruby-version: 3.1
17
17
  bundler-cache: true
18
18
 
19
19
  - name: Run benchmark on branch
20
20
  run: benchmark/send-metrics-to-local-udp-receiver
21
21
 
22
+ - name: Run throughput benchmark on branch
23
+ run: benchmark/local-udp-throughput
24
+
22
25
  - uses: actions/checkout@v1
23
26
  with:
24
27
  ref: 'master'
@@ -28,3 +31,6 @@ jobs:
28
31
 
29
32
  - name: Run benchmark on master
30
33
  run: benchmark/send-metrics-to-local-udp-receiver
34
+
35
+ - name: Run throughput benchmark on master
36
+ run: benchmark/local-udp-throughput
@@ -9,9 +9,8 @@ jobs:
9
9
  strategy:
10
10
  fail-fast: false
11
11
  matrix:
12
- ruby: ['2.6', '2.7', '3.0', '3.1']
13
-
14
- # Windows on macOS builds started failing, so they are disabled for noew
12
+ ruby: ['2.6', '2.7', '3.0', '3.1', 'ruby-head', 'jruby-9.3.7.0', 'truffleruby-22.2.0']
13
+ # Windows on macOS builds started failing, so they are disabled for now
15
14
  # platform: [windows-2019, macOS-10.14, ubuntu-18.04]
16
15
  # exclude:
17
16
  # ...
data/.rubocop.yml CHANGED
@@ -20,9 +20,15 @@ Naming/FileName:
20
20
  Metrics/ParameterLists:
21
21
  Enabled: false
22
22
 
23
+ Metrics/BlockNesting:
24
+ Enabled: false
25
+
23
26
  Style/WhileUntilModifier:
24
27
  Enabled: false
25
28
 
29
+ Style/IdenticalConditionalBranches:
30
+ Enabled: false
31
+
26
32
  # Enable our own cops on our own repo
27
33
 
28
34
  StatsD/MetricReturnValue:
data/CHANGELOG.md CHANGED
@@ -6,6 +6,12 @@ section below.
6
6
 
7
7
  ### Unreleased changes
8
8
 
9
+ - UDP Batching has been largely refactored again. The `STATSD_FLUSH_INTERVAL` environment variable
10
+ is deprecated. It still disable batching if set to `0`, but other than that is has no effect.
11
+ Setting `STATSD_BUFFER_CAPACITY` to `0` is now the recommended way to disable batching.
12
+ - The synchronous UDP sink now use one socket per thread, instead of a single socket
13
+ protected by a mutex.
14
+
9
15
  ## Version 3.3.0
10
16
 
11
17
  - UDP Batching now has a max queue size and emitter threads will block if the queue
data/README.md CHANGED
@@ -42,13 +42,14 @@ The following environment variables are supported:
42
42
  overridden in a metric method call.
43
43
  - `STATSD_DEFAULT_TAGS`: A comma-separated list of tags to apply to all metrics.
44
44
  (Note: tags are not supported by all implementations.)
45
- - `STATSD_FLUSH_INTERVAL`: (default: `1.0`) The interval in seconds at which
46
- events are sent in batch. Only applicable to the UDP configuration. If set
47
- to `0.0`, metrics are sent immediately.
48
45
  - `STATSD_BUFFER_CAPACITY`: (default: `5000`) The maximum amount of events that
49
46
  may be buffered before emitting threads will start to block. Increasing this
50
47
  value may help for application generating spikes of events. However if the
51
48
  application emit events faster than they can be sent, increasing it won't help.
49
+ If set to `0`, batching will be disabled, and events will be sent in individual
50
+ UDP packets, which is much slower.
51
+ - `STATSD_FLUSH_INTERVAL`: (default: `1`) Deprecated. Setting this to `0` is
52
+ equivalent to setting `STATSD_BUFFER_CAPACITY` to `0`.
52
53
  - `STATSD_MAX_PACKET_SIZE`: (default: `1472`) The maximum size of UDP packets.
53
54
  If your network is properly configured to handle larger packets you may try
54
55
  to increase this value for better performance, but most network can't handle
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "benchmark/ips"
6
+ require "tmpdir"
7
+ require "socket"
8
+ require "statsd-instrument"
9
+
10
+ def send_metrics(client)
11
+ client.increment("StatsD.increment", 10)
12
+ client.measure("StatsD.measure") { 1 + 1 }
13
+ client.gauge("StatsD.gauge", 12.0, tags: ["foo:bar", "quc"])
14
+ client.set("StatsD.set", "value", tags: { foo: "bar", baz: "quc" })
15
+ client.event("StasD.event", "12345")
16
+ client.service_check("StatsD.service_check", "ok")
17
+ end
18
+
19
+ THREAD_COUNT = Integer(ENV.fetch("THREAD_COUNT", 5))
20
+ EVENTS_PER_ITERATION = 6
21
+ ITERATIONS = 50_000
22
+ def benchmark_implementation(name, env = {})
23
+ intermediate_results_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/"
24
+ log_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/#{File.basename($PROGRAM_NAME)}-#{name}.log"
25
+ FileUtils.mkdir_p(File.dirname(intermediate_results_filename))
26
+
27
+ # Set up an UDP listener to which we can send StatsD packets
28
+ receiver = UDPSocket.new
29
+ receiver.bind("localhost", 0)
30
+
31
+ log_file = File.open(log_filename, "w+", level: Logger::WARN)
32
+ StatsD.logger = Logger.new(log_file)
33
+
34
+ udp_client = StatsD::Instrument::Environment.new(ENV.to_h.merge(
35
+ "STATSD_ADDR" => "#{receiver.addr[2]}:#{receiver.addr[1]}",
36
+ "STATSD_IMPLEMENTATION" => "dogstatsd",
37
+ "STATSD_ENV" => "production",
38
+ ).merge(env)).client
39
+
40
+ puts "===== #{name} throughtput (#{THREAD_COUNT} threads) ====="
41
+ threads = THREAD_COUNT.times.map do
42
+ Thread.new do
43
+ count = ITERATIONS
44
+ while (count -= 1) > 0
45
+ send_metrics(udp_client)
46
+ end
47
+ end
48
+ end
49
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
50
+ threads.each(&:join)
51
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
52
+ events_sent = THREAD_COUNT * EVENTS_PER_ITERATION * ITERATIONS
53
+ puts "events: #{(events_sent / duration).round(1)}/s"
54
+ receiver.close
55
+ udp_client.shutdown if udp_client.respond_to?(:shutdown)
56
+ end
57
+
58
+ benchmark_implementation("UDP sync", "STATSD_BUFFER_CAPACITY" => "0")
59
+ benchmark_implementation("UDP batched")
@@ -7,38 +7,6 @@ require "tmpdir"
7
7
  require "socket"
8
8
  require "statsd-instrument"
9
9
 
10
- revision = %x(git rev-parse HEAD).rstrip
11
- base_revision = %x(git rev-parse origin/master).rstrip
12
- branch = if revision == base_revision
13
- "master"
14
- else
15
- %x(git rev-parse --abbrev-ref HEAD).rstrip
16
- end
17
-
18
- intermediate_results_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/#{File.basename($PROGRAM_NAME)}"
19
- log_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/#{File.basename($PROGRAM_NAME)}.log"
20
- FileUtils.mkdir_p(File.dirname(intermediate_results_filename))
21
-
22
- # Set up an UDP listener to which we can send StatsD packets
23
- receiver = UDPSocket.new
24
- receiver.bind("localhost", 0)
25
-
26
- log_file = File.open(log_filename, "w+", level: Logger::WARN)
27
- StatsD.logger = Logger.new(log_file)
28
-
29
- udp_client = StatsD::Instrument::Environment.new(ENV.to_h.merge(
30
- "STATSD_ADDR" => "#{receiver.addr[2]}:#{receiver.addr[1]}",
31
- "STATSD_IMPLEMENTATION" => "dogstatsd",
32
- "STATSD_ENV" => "production",
33
- "STATSD_FLUSH_INTERVAL" => "0",
34
- )).client
35
-
36
- batched_udp_client = StatsD::Instrument::Environment.new(ENV.to_h.merge(
37
- "STATSD_ADDR" => "#{receiver.addr[2]}:#{receiver.addr[1]}",
38
- "STATSD_IMPLEMENTATION" => "dogstatsd",
39
- "STATSD_ENV" => "production",
40
- )).client
41
-
42
10
  def send_metrics(client)
43
11
  client.increment("StatsD.increment", 10)
44
12
  client.measure("StatsD.measure") { 1 + 1 }
@@ -50,37 +18,66 @@ def send_metrics(client)
50
18
  end
51
19
  end
52
20
 
53
- report = Benchmark.ips do |bench|
54
- bench.report("local UDP sync (branch: #{branch}, sha: #{revision[0, 7]})") do
55
- send_metrics(udp_client)
21
+ def benchmark_implementation(name, env = {})
22
+ revision = %x(git rev-parse HEAD).rstrip
23
+ base_revision = %x(git rev-parse origin/master).rstrip
24
+ branch = if revision == base_revision
25
+ "master"
26
+ else
27
+ %x(git rev-parse --abbrev-ref HEAD).rstrip
56
28
  end
57
29
 
58
- bench.report("local UDP batched (branch: #{branch}, sha: #{revision[0, 7]})") do
59
- send_metrics(batched_udp_client)
30
+ intermediate_results_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/#{File.basename($PROGRAM_NAME)}-#{name}"
31
+ log_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/#{File.basename($PROGRAM_NAME)}-#{name}.log"
32
+ FileUtils.mkdir_p(File.dirname(intermediate_results_filename))
33
+
34
+ # Set up an UDP listener to which we can send StatsD packets
35
+ receiver = UDPSocket.new
36
+ receiver.bind("localhost", 0)
37
+
38
+ log_file = File.open(log_filename, "w+", level: Logger::WARN)
39
+ StatsD.logger = Logger.new(log_file)
40
+
41
+ udp_client = StatsD::Instrument::Environment.new(ENV.to_h.merge(
42
+ "STATSD_ADDR" => "#{receiver.addr[2]}:#{receiver.addr[1]}",
43
+ "STATSD_IMPLEMENTATION" => "dogstatsd",
44
+ "STATSD_ENV" => "production",
45
+ ).merge(env)).client
46
+
47
+ puts "===== #{name} ====="
48
+ report = Benchmark.ips do |bench|
49
+ bench.report("#{name} (branch: #{branch}, sha: #{revision[0, 7]})") do
50
+ send_metrics(udp_client)
51
+ end
52
+
53
+ # Store the results in between runs
54
+ bench.save!(intermediate_results_filename)
55
+ bench.compare!
60
56
  end
61
57
 
62
- # Store the results in between runs
63
- bench.save!(intermediate_results_filename)
64
- bench.compare!
65
- end
58
+ receiver.close
59
+ udp_client.shutdown if udp_client.respond_to?(:shutdown)
66
60
 
67
- receiver.close
61
+ if report.entries.length == 1
62
+ puts
63
+ puts "To compare the performance of this revision against another revision (e.g. master),"
64
+ puts "check out a different branch and run this benchmark script again."
65
+ elsif ENV["KEEP_RESULTS"]
66
+ puts
67
+ puts "The intermediate results have been stored in #{intermediate_results_filename}"
68
+ else
69
+ File.unlink(intermediate_results_filename)
70
+ end
68
71
 
69
- if report.entries.length == 1
70
- puts
71
- puts "To compare the performance of this revision against another revision (e.g. master),"
72
- puts "check out a different branch and run this benchmark script again."
73
- elsif ENV["KEEP_RESULTS"]
74
- puts
75
- puts "The intermediate results have been stored in #{intermediate_results_filename}"
76
- else
77
- File.unlink(intermediate_results_filename)
72
+ log_file.close
73
+ logs = File.read(log_filename)
74
+ unless logs.empty?
75
+ puts
76
+ puts "==== logs ===="
77
+ puts logs
78
+ end
79
+ puts "================"
78
80
  end
79
81
 
80
- log_file.close
81
- logs = File.read(log_filename)
82
- unless logs.empty?
83
- puts
84
- puts "==== logs ===="
85
- puts logs
86
- end
82
+ benchmark_implementation("UDP sync", "STATSD_BUFFER_CAPACITY" => "0")
83
+ benchmark_implementation("UDP batched")
@@ -5,9 +5,7 @@ module StatsD
5
5
  # @note This class is part of the new Client implementation that is intended
6
6
  # to become the new default in the next major release of this library.
7
7
  class BatchedUDPSink
8
- DEFAULT_FLUSH_INTERVAL = 1.0
9
8
  DEFAULT_THREAD_PRIORITY = 100
10
- DEFAULT_FLUSH_THRESHOLD = 50
11
9
  DEFAULT_BUFFER_CAPACITY = 5_000
12
10
  # https://docs.datadoghq.com/developers/dogstatsd/high_throughput/?code-lang=ruby#ensure-proper-packet-sizes
13
11
  DEFAULT_MAX_PACKET_SIZE = 1472
@@ -28,9 +26,7 @@ module StatsD
28
26
  def initialize(
29
27
  host,
30
28
  port,
31
- flush_interval: DEFAULT_FLUSH_INTERVAL,
32
29
  thread_priority: DEFAULT_THREAD_PRIORITY,
33
- flush_threshold: DEFAULT_FLUSH_THRESHOLD,
34
30
  buffer_capacity: DEFAULT_BUFFER_CAPACITY,
35
31
  max_packet_size: DEFAULT_MAX_PACKET_SIZE
36
32
  )
@@ -39,8 +35,6 @@ module StatsD
39
35
  @dispatcher = Dispatcher.new(
40
36
  host,
41
37
  port,
42
- flush_interval,
43
- flush_threshold,
44
38
  buffer_capacity,
45
39
  thread_priority,
46
40
  max_packet_size,
@@ -61,112 +55,85 @@ module StatsD
61
55
  @dispatcher.shutdown(*args)
62
56
  end
63
57
 
64
- class Dispatcher
65
- BUFFER_CLASS = if !::Object.const_defined?(:RUBY_ENGINE) || RUBY_ENGINE == "ruby"
66
- ::Array
67
- else
68
- begin
69
- gem("concurrent-ruby")
70
- rescue Gem::MissingSpecError
71
- raise Gem::MissingSpecError, "statsd-instrument depends on `concurrent-ruby` on #{RUBY_ENGINE}"
72
- end
73
- require "concurrent/array"
74
- Concurrent::Array
58
+ class Buffer < SizedQueue
59
+ def push_nonblock(item)
60
+ push(item, true)
61
+ rescue ThreadError, ClosedQueueError
62
+ nil
63
+ end
64
+
65
+ def inspect
66
+ "<#{self.class.name}:#{object_id} capacity=#{max} size=#{size}>"
67
+ end
68
+
69
+ def pop_nonblock
70
+ pop(true)
71
+ rescue ThreadError
72
+ nil
75
73
  end
74
+ end
76
75
 
77
- def initialize(host, port, flush_interval, flush_threshold, buffer_capacity, thread_priority, max_packet_size)
78
- @host = host
79
- @port = port
76
+ class Dispatcher
77
+ def initialize(host, port, buffer_capacity, thread_priority, max_packet_size)
78
+ @udp_sink = UDPSink.new(host, port)
80
79
  @interrupted = false
81
- @flush_interval = flush_interval
82
- @flush_threshold = flush_threshold
83
- @buffer_capacity = buffer_capacity
84
80
  @thread_priority = thread_priority
85
81
  @max_packet_size = max_packet_size
86
- @buffer = BUFFER_CLASS.new
82
+ @buffer_capacity = buffer_capacity
83
+ @buffer = Buffer.new(buffer_capacity)
87
84
  @dispatcher_thread = Thread.new { dispatch }
88
85
  @pid = Process.pid
89
- @monitor = Monitor.new
90
- @condition = @monitor.new_cond
91
86
  end
92
87
 
93
88
  def <<(datagram)
94
- if thread_healthcheck
95
- @buffer << datagram
96
-
97
- # To avoid sending too many signals when the thread is already flushing
98
- # We only signal when the queue size is a multiple of `flush_threshold`
99
- if @buffer.size % @flush_threshold == 0
100
- wakeup_thread
101
- end
102
-
103
- # A SizedQueue would be perfect, except that it doesn't have a timeout
104
- # Ref: https://bugs.ruby-lang.org/issues/18774
105
- if @buffer.size >= @buffer_capacity
106
- StatsD.logger.warn do
107
- "[#{self.class.name}] Max buffer size reached (#{@buffer_capacity}), pausing " \
108
- "thread##{Thread.current.object_id}"
109
- end
110
- before = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
111
- @monitor.synchronize do
112
- while @buffer.size >= @buffer_capacity && @dispatcher_thread.alive?
113
- @condition.wait(0.01)
114
- end
115
- end
116
- duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - before
117
- StatsD.logger.warn do
118
- "[#{self.class.name}] thread##{Thread.current.object_id} resumed after #{duration.round(2)}ms"
119
- end
120
- end
121
- else
122
- flush
89
+ if !thread_healthcheck || !@buffer.push_nonblock(datagram)
90
+ # The buffer is full or the thread can't be respaned,
91
+ # we'll send the datagram synchronously
92
+ @udp_sink << datagram
123
93
  end
124
94
 
125
95
  self
126
96
  end
127
97
 
128
- def shutdown(wait = @flush_interval * 2)
98
+ def shutdown(wait = 2)
129
99
  @interrupted = true
100
+ @buffer.close
130
101
  if @dispatcher_thread&.alive?
131
102
  @dispatcher_thread.join(wait)
132
- else
133
- flush
134
103
  end
104
+ flush(blocking: false)
135
105
  end
136
106
 
137
107
  private
138
108
 
139
- def wakeup_thread
140
- begin
141
- @monitor.synchronize do
142
- @condition.signal
143
- end
144
- rescue ThreadError
145
- # Can't synchronize from trap context
146
- Thread.new { wakeup_thread }.join
147
- return
148
- end
149
-
150
- begin
151
- @dispatcher_thread&.run
152
- rescue ThreadError # Somehow the thread just died
153
- thread_healthcheck
154
- end
155
- end
156
-
157
109
  NEWLINE = "\n".b.freeze
158
- def flush
159
- return if @buffer.empty?
160
110
 
161
- datagrams = @buffer.shift(@buffer.size)
162
-
163
- until datagrams.empty?
164
- packet = String.new(datagrams.shift, encoding: Encoding::BINARY, capacity: @max_packet_size)
111
+ def flush(blocking:)
112
+ packet = "".b
113
+ next_datagram = nil
114
+ until @buffer.closed? && @buffer.empty? && next_datagram.nil?
115
+ if blocking
116
+ next_datagram ||= @buffer.pop
117
+ break if next_datagram.nil? # queue was closed
118
+ else
119
+ next_datagram ||= @buffer.pop_nonblock
120
+ break if next_datagram.nil? # no datagram in buffer
121
+ end
165
122
 
166
- until datagrams.empty? || packet.bytesize + datagrams.first.bytesize + 1 > @max_packet_size
167
- packet << NEWLINE << datagrams.shift
123
+ packet << next_datagram
124
+ next_datagram = nil
125
+ unless packet.bytesize > @max_packet_size
126
+ while (next_datagram = @buffer.pop_nonblock)
127
+ if @max_packet_size - packet.bytesize - 1 > next_datagram.bytesize
128
+ packet << NEWLINE << next_datagram
129
+ else
130
+ break
131
+ end
132
+ end
168
133
  end
169
- send_packet(packet)
134
+
135
+ @udp_sink << packet
136
+ packet.clear
170
137
  end
171
138
  end
172
139
 
@@ -196,26 +163,13 @@ module StatsD
196
163
  def dispatch
197
164
  until @interrupted
198
165
  begin
199
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
200
- flush
201
-
202
- # Other threads may have queued more events while we were doing IO
203
- flush while @buffer.size > @flush_threshold
204
-
205
- next_sleep_duration = @flush_interval - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start)
206
-
207
- if next_sleep_duration > 0
208
- @monitor.synchronize do
209
- @condition.wait(next_sleep_duration)
210
- end
211
- end
166
+ flush(blocking: true)
212
167
  rescue => error
213
168
  report_error(error)
214
169
  end
215
170
  end
216
171
 
217
- flush
218
- invalidate_socket
172
+ flush(blocking: false)
219
173
  end
220
174
 
221
175
  def report_error(error)
@@ -223,40 +177,6 @@ module StatsD
223
177
  "[#{self.class.name}] The dispatcher thread encountered an error #{error.class}: #{error.message}"
224
178
  end
225
179
  end
226
-
227
- def send_packet(packet)
228
- retried = false
229
- begin
230
- socket.send(packet, 0)
231
- rescue SocketError, IOError, SystemCallError => error
232
- StatsD.logger.debug do
233
- "[#{self.class.name}] Resetting connection because of #{error.class}: #{error.message}"
234
- end
235
- invalidate_socket
236
- if retried
237
- StatsD.logger.warn do
238
- "[#{self.class.name}] Events were dropped because of #{error.class}: #{error.message}"
239
- end
240
- else
241
- retried = true
242
- retry
243
- end
244
- end
245
- end
246
-
247
- def socket
248
- @socket ||= begin
249
- socket = UDPSocket.new
250
- socket.connect(@host, @port)
251
- socket
252
- end
253
- end
254
-
255
- def invalidate_socket
256
- @socket&.close
257
- ensure
258
- @socket = nil
259
- end
260
180
  end
261
181
  end
262
182
  end
@@ -35,6 +35,14 @@ module StatsD
35
35
 
36
36
  def initialize(env)
37
37
  @env = env
38
+ if env.key?("STATSD_FLUSH_INTERVAL")
39
+ value = env["STATSD_FLUSH_INTERVAL"]
40
+ if Float(value) == 0.0
41
+ warn("STATSD_FLUSH_INTERVAL=#{value} is deprecated, please set STATSD_BUFFER_CAPACITY=0 instead.")
42
+ else
43
+ warn("STATSD_FLUSH_INTERVAL=#{value} is deprecated and has no effect, please remove it.")
44
+ end
45
+ end
38
46
  end
39
47
 
40
48
  # Detects the current environment, either by asking Rails, or by inspecting environment variables.
@@ -78,12 +86,12 @@ module StatsD
78
86
  env.key?("STATSD_DEFAULT_TAGS") ? env.fetch("STATSD_DEFAULT_TAGS").split(",") : nil
79
87
  end
80
88
 
81
- def statsd_flush_interval
82
- Float(env.fetch("STATSD_FLUSH_INTERVAL", StatsD::Instrument::BatchedUDPSink::DEFAULT_FLUSH_INTERVAL))
89
+ def statsd_buffer_capacity
90
+ Integer(env.fetch("STATSD_BUFFER_CAPACITY", StatsD::Instrument::BatchedUDPSink::DEFAULT_BUFFER_CAPACITY))
83
91
  end
84
92
 
85
- def statsd_buffer_capacity
86
- Float(env.fetch("STATSD_BUFFER_CAPACITY", StatsD::Instrument::BatchedUDPSink::DEFAULT_BUFFER_CAPACITY))
93
+ def statsd_batching?
94
+ statsd_buffer_capacity > 0 && Float(env.fetch("STATSD_FLUSH_INTERVAL", 1.0)) > 0.0
87
95
  end
88
96
 
89
97
  def statsd_max_packet_size
@@ -97,10 +105,9 @@ module StatsD
97
105
  def default_sink_for_environment
98
106
  case environment
99
107
  when "production", "staging"
100
- if statsd_flush_interval > 0.0
108
+ if statsd_batching?
101
109
  StatsD::Instrument::BatchedUDPSink.for_addr(
102
110
  statsd_addr,
103
- flush_interval: statsd_flush_interval,
104
111
  buffer_capacity: statsd_buffer_capacity,
105
112
  max_packet_size: statsd_max_packet_size,
106
113
  )
@@ -12,11 +12,18 @@ module StatsD
12
12
 
13
13
  attr_reader :host, :port
14
14
 
15
+ FINALIZER = ->(object_id) do
16
+ Thread.list.each do |thread|
17
+ if (store = thread["StatsD::UDPSink"])
18
+ store.delete(object_id)&.close
19
+ end
20
+ end
21
+ end
22
+
15
23
  def initialize(host, port)
24
+ ObjectSpace.define_finalizer(self, FINALIZER)
16
25
  @host = host
17
26
  @port = port
18
- @mutex = Mutex.new
19
- @socket = nil
20
27
  end
21
28
 
22
29
  def sample?(sample_rate)
@@ -24,43 +31,43 @@ module StatsD
24
31
  end
25
32
 
26
33
  def <<(datagram)
27
- with_socket { |socket| socket.send(datagram, 0) }
28
- self
29
- rescue SocketError, IOError, SystemCallError => error
30
- StatsD.logger.debug do
31
- "[StatsD::Instrument::UDPSink] Resetting connection because of #{error.class}: #{error.message}"
34
+ retried = false
35
+ begin
36
+ socket.send(datagram, 0)
37
+ rescue SocketError, IOError, SystemCallError => error
38
+ StatsD.logger.debug do
39
+ "[StatsD::Instrument::UDPSink] Resetting connection because of #{error.class}: #{error.message}"
40
+ end
41
+ invalidate_socket
42
+ if retried
43
+ StatsD.logger.warn do
44
+ "[#{self.class.name}] Events were dropped because of #{error.class}: #{error.message}"
45
+ end
46
+ else
47
+ retried = true
48
+ retry
49
+ end
32
50
  end
33
- invalidate_socket
34
51
  self
35
52
  end
36
53
 
37
54
  private
38
55
 
39
- def synchronize(&block)
40
- @mutex.synchronize(&block)
41
- rescue ThreadError
42
- # In cases where a TERM or KILL signal has been sent, and we send stats as
43
- # part of a signal handler, locks cannot be acquired, so we do our best
44
- # to try and send the datagram without a lock.
45
- yield
46
- end
47
-
48
- def with_socket
49
- synchronize { yield(socket) }
56
+ def invalidate_socket
57
+ socket = thread_store.delete(object_id)
58
+ socket&.close
50
59
  end
51
60
 
52
61
  def socket
53
- @socket ||= begin
62
+ thread_store[object_id] ||= begin
54
63
  socket = UDPSocket.new
55
64
  socket.connect(@host, @port)
56
65
  socket
57
66
  end
58
67
  end
59
68
 
60
- def invalidate_socket
61
- synchronize do
62
- @socket = nil
63
- end
69
+ def thread_store
70
+ Thread.current["StatsD::UDPSink"] ||= {}
64
71
  end
65
72
  end
66
73
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module StatsD
4
4
  module Instrument
5
- VERSION = "3.3.0"
5
+ VERSION = "3.4.0"
6
6
  end
7
7
  end
@@ -21,6 +21,4 @@ Gem::Specification.new do |spec|
21
21
  spec.require_paths = ["lib"]
22
22
 
23
23
  spec.metadata['allowed_push_host'] = "https://rubygems.org"
24
-
25
- spec.add_development_dependency 'concurrent-ruby'
26
24
  end
@@ -64,4 +64,13 @@ class EnvironmentTest < Minitest::Test
64
64
  )
65
65
  assert_kind_of(StatsD::Instrument::UDPSink, env.client.sink)
66
66
  end
67
+
68
+ def test_client_from_env_uses_regular_udp_sink_when_buffer_capacity_is_0
69
+ env = StatsD::Instrument::Environment.new(
70
+ "STATSD_USE_NEW_CLIENT" => "1",
71
+ "STATSD_ENV" => "staging",
72
+ "STATSD_BUFFER_CAPACITY" => "0",
73
+ )
74
+ assert_kind_of(StatsD::Instrument::UDPSink, env.client.sink)
75
+ end
67
76
  end
@@ -34,15 +34,14 @@ module UDPSinkTests
34
34
 
35
35
  def test_concurrency
36
36
  udp_sink = build_sink(@host, @port)
37
- threads = 50.times.map { |i| Thread.new { udp_sink << "foo:#{i}|c" << "bar:#{i}|c" } }
38
- datagrams = []
39
-
40
- while @receiver.wait_readable(2)
41
- datagram, _source = @receiver.recvfrom(4096)
42
- datagrams += datagram.split("\n")
37
+ threads = 10.times.map do |i|
38
+ Thread.new do
39
+ udp_sink << "foo:#{i}|c" << "bar:#{i}|c" << "baz:#{i}|c" << "plop:#{i}|c"
40
+ end
43
41
  end
44
-
45
- assert_equal(100, datagrams.size)
42
+ threads.each(&:join)
43
+ udp_sink.shutdown if udp_sink.respond_to?(:shutdown)
44
+ assert_equal(40, read_datagrams(40).size)
46
45
  ensure
47
46
  threads&.each(&:kill)
48
47
  end
@@ -111,11 +110,12 @@ module UDPSinkTests
111
110
  @sink_class.new(host, port)
112
111
  end
113
112
 
114
- def read_datagrams(count, timeout: 2)
113
+ def read_datagrams(count, timeout: ENV["CI"] ? 5 : 1)
115
114
  datagrams = []
116
115
  count.times do
117
116
  if @receiver.wait_readable(timeout)
118
- datagrams += @receiver.recvfrom_nonblock(1000).first.lines(chomp: true)
117
+ datagrams += @receiver.recvfrom(2000).first.lines(chomp: true)
118
+ break if datagrams.size >= count
119
119
  else
120
120
  break
121
121
  end
@@ -149,8 +149,9 @@ module UDPSinkTests
149
149
  seq = sequence("connect_fail_connect_succeed")
150
150
  socket.expects(:connect).with("localhost", 8125).in_sequence(seq)
151
151
  socket.expects(:send).raises(Errno::EDESTADDRREQ).in_sequence(seq)
152
+ socket.expects(:close).in_sequence(seq)
152
153
  socket.expects(:connect).with("localhost", 8125).in_sequence(seq)
153
- socket.expects(:send).returns(1).in_sequence(seq)
154
+ socket.expects(:send).twice.returns(1).in_sequence(seq)
154
155
 
155
156
  udp_sink = build_sink("localhost", 8125)
156
157
  udp_sink << "foo:1|c"
@@ -187,52 +188,13 @@ module UDPSinkTests
187
188
  private
188
189
 
189
190
  def build_sink(host = @host, port = @port)
190
- sink = @sink_class.new(host, port, flush_threshold: default_flush_threshold, buffer_capacity: 50)
191
+ sink = @sink_class.new(host, port, buffer_capacity: 50)
191
192
  @sinks << sink
192
193
  sink
193
194
  end
194
-
195
- def default_flush_threshold
196
- StatsD::Instrument::BatchedUDPSink::DEFAULT_FLUSH_THRESHOLD
197
- end
198
195
  end
199
196
 
200
197
  class BatchedUDPSinkTest < Minitest::Test
201
198
  include BatchedUDPSinkTests
202
-
203
- def test_concurrency_buffering
204
- udp_sink = build_sink(@host, @port)
205
- threads = 50.times.map do |i|
206
- Thread.new do
207
- udp_sink << "foo:#{i}|c" << "bar:#{i}|c" << "baz:#{i}|c" << "plop:#{i}|c"
208
- end
209
- end
210
- threads.each(&:join)
211
- assert_equal(200, read_datagrams(10, timeout: 2).size)
212
- ensure
213
- threads&.each(&:kill)
214
- end
215
- end
216
-
217
- class LowThresholdBatchedUDPSinkTest < Minitest::Test
218
- include BatchedUDPSinkTests
219
-
220
- def test_sends_datagram_when_termed
221
- # When the main thread exit, the dispatcher thread is aborted
222
- # and there's no exceptions or anything like that to rescue.
223
- # So if the dispatcher thread poped some events from the buffer
224
- # but didn't sent them yet, then they may be lost.
225
- skip("Unfortunately this can't be guaranteed")
226
- end
227
- alias_method :test_sends_datagram_in_at_exit_callback, :test_sends_datagram_when_termed
228
- alias_method :test_sends_datagram_before_exit, :test_sends_datagram_when_termed
229
-
230
- private
231
-
232
- # We run the same tests again, but this time we wake up the dispatcher
233
- # thread on every call to make sure trap context is properly handled
234
- def default_flush_threshold
235
- 1
236
- end
237
199
  end
238
200
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statsd-instrument
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.0
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesse Storimer
@@ -10,22 +10,8 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2022-07-26 00:00:00.000000000 Z
14
- dependencies:
15
- - !ruby/object:Gem::Dependency
16
- name: concurrent-ruby
17
- requirement: !ruby/object:Gem::Requirement
18
- requirements:
19
- - - ">="
20
- - !ruby/object:Gem::Version
21
- version: '0'
22
- type: :development
23
- prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
25
- requirements:
26
- - - ">="
27
- - !ruby/object:Gem::Version
28
- version: '0'
13
+ date: 2022-08-29 00:00:00.000000000 Z
14
+ dependencies: []
29
15
  description: A StatsD client for Ruby apps. Provides metaprogramming methods to inject
30
16
  StatsD instrumentation into your code.
31
17
  email:
@@ -49,6 +35,7 @@ files:
49
35
  - README.md
50
36
  - Rakefile
51
37
  - benchmark/README.md
38
+ - benchmark/local-udp-throughput
52
39
  - benchmark/send-metrics-to-dev-null-log
53
40
  - benchmark/send-metrics-to-local-udp-receiver
54
41
  - bin/rake