dogstatsd-ruby 5.2.0 → 5.6.1

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.
@@ -2,25 +2,56 @@
2
2
 
3
3
  module Datadog
4
4
  class Statsd
5
+ # Sender is using a companion thread to flush and pack messages
6
+ # in a `MessageBuffer`.
7
+ # The communication with this thread is done using a `Queue`.
8
+ # If the thread is dead, it is starting a new one to avoid having a blocked
9
+ # Sender with no companion thread to communicate with (most of the time, having
10
+ # a dead companion thread means that a fork just happened and that we are
11
+ # running in the child process).
5
12
  class Sender
6
13
  CLOSEABLE_QUEUES = Queue.instance_methods.include?(:close)
7
14
 
8
- def initialize(message_buffer)
15
+ def initialize(message_buffer, telemetry: nil, queue_size: UDP_DEFAULT_BUFFER_SIZE, logger: nil, flush_interval: nil, queue_class: Queue, thread_class: Thread)
9
16
  @message_buffer = message_buffer
17
+ @telemetry = telemetry
18
+ @queue_size = queue_size
19
+ @logger = logger
20
+ @mx = Mutex.new
21
+ @queue_class = queue_class
22
+ @thread_class = thread_class
23
+ @flush_timer = if flush_interval
24
+ Datadog::Statsd::Timer.new(flush_interval) { flush(sync: true) }
25
+ else
26
+ nil
27
+ end
10
28
  end
11
29
 
12
30
  def flush(sync: false)
13
- # don't try to flush if there is no message_queue instantiated
14
- return unless message_queue
15
-
16
- message_queue.push(:flush)
31
+ # keep a copy around in case another thread is calling #stop while this method is running
32
+ current_message_queue = message_queue
33
+
34
+ # don't try to flush if there is no message_queue instantiated or
35
+ # no companion thread running
36
+ if !current_message_queue
37
+ @logger.debug { "Statsd: can't flush: no message queue ready" } if @logger
38
+ return
39
+ end
40
+ if !sender_thread.alive?
41
+ @logger.debug { "Statsd: can't flush: no sender_thread alive" } if @logger
42
+ return
43
+ end
17
44
 
45
+ current_message_queue.push(:flush)
18
46
  rendez_vous if sync
19
47
  end
20
48
 
21
49
  def rendez_vous
50
+ # could happen if #start hasn't be called
51
+ return unless message_queue
52
+
22
53
  # Initialize and get the thread's sync queue
23
- queue = (Thread.current[:statsd_sync_queue] ||= Queue.new)
54
+ queue = (@thread_class.current[:statsd_sync_queue] ||= @queue_class.new)
24
55
  # tell sender-thread to notify us in the current
25
56
  # thread's queue
26
57
  message_queue.push(queue)
@@ -32,20 +63,52 @@ module Datadog
32
63
  def add(message)
33
64
  raise ArgumentError, 'Start sender first' unless message_queue
34
65
 
35
- message_queue << message
66
+ # if the thread does not exist, we assume we are running in a forked process,
67
+ # empty the message queue and message buffers (these messages belong to
68
+ # the parent process) and spawn a new companion thread.
69
+ if !sender_thread.alive?
70
+ @mx.synchronize {
71
+ # a call from another thread has already re-created
72
+ # the companion thread before this one acquired the lock
73
+ break if sender_thread.alive?
74
+ @logger.debug { "Statsd: companion thread is dead, re-creating one" } if @logger
75
+
76
+ message_queue.close if CLOSEABLE_QUEUES
77
+ @message_queue = nil
78
+ message_buffer.reset
79
+ start
80
+ @flush_timer.start if @flush_timer && @flush_timer.stop?
81
+ }
82
+ end
83
+
84
+ if message_queue.length <= @queue_size
85
+ message_queue << message
86
+ else
87
+ if @telemetry
88
+ bytesize = message.respond_to?(:bytesize) ? message.bytesize : 0
89
+ @telemetry.dropped_queue(packets: 1, bytes: bytesize)
90
+ end
91
+ end
36
92
  end
37
93
 
38
94
  def start
39
95
  raise ArgumentError, 'Sender already started' if message_queue
40
96
 
41
- # initialize message queue for background thread
42
- @message_queue = Queue.new
97
+ # initialize a new message queue for the background thread
98
+ @message_queue = @queue_class.new
43
99
  # start background thread
44
- @sender_thread = Thread.new(&method(:send_loop))
100
+ @sender_thread = @thread_class.new(&method(:send_loop))
101
+ @sender_thread.name = "Statsd Sender" unless Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3')
102
+ @flush_timer.start if @flush_timer
45
103
  end
46
104
 
47
105
  if CLOSEABLE_QUEUES
106
+ # when calling stop, make sure that no other threads is trying
107
+ # to close the sender nor trying to continue to `#add` more message
108
+ # into the sender.
48
109
  def stop(join_worker: true)
110
+ @flush_timer.stop if @flush_timer
111
+
49
112
  message_queue = @message_queue
50
113
  message_queue.close if message_queue
51
114
 
@@ -53,7 +116,12 @@ module Datadog
53
116
  sender_thread.join if sender_thread && join_worker
54
117
  end
55
118
  else
119
+ # when calling stop, make sure that no other threads is trying
120
+ # to close the sender nor trying to continue to `#add` more message
121
+ # into the sender.
56
122
  def stop(join_worker: true)
123
+ @flush_timer.stop if @flush_timer
124
+
57
125
  message_queue = @message_queue
58
126
  message_queue << :close if message_queue
59
127
 
@@ -65,7 +133,6 @@ module Datadog
65
133
  private
66
134
 
67
135
  attr_reader :message_buffer
68
-
69
136
  attr_reader :message_queue
70
137
  attr_reader :sender_thread
71
138
 
@@ -79,7 +146,7 @@ module Datadog
79
146
  case message
80
147
  when :flush
81
148
  message_buffer.flush
82
- when Queue
149
+ when @queue_class
83
150
  message.push(:go_on)
84
151
  else
85
152
  message_buffer.add(message)
@@ -101,7 +168,7 @@ module Datadog
101
168
  break
102
169
  when :flush
103
170
  message_buffer.flush
104
- when Queue
171
+ when @queue_class
105
172
  message.push(:go_on)
106
173
  else
107
174
  message_buffer.add(message)
@@ -2,30 +2,81 @@
2
2
 
3
3
  module Datadog
4
4
  class Statsd
5
+ # The SingleThreadSender is a sender synchronously buffering messages
6
+ # in a `MessageBuffer`.
7
+ # It is using current Process.PID to check it is the result of a recent fork
8
+ # and it is reseting the MessageBuffer if that's the case.
5
9
  class SingleThreadSender
6
- def initialize(message_buffer)
10
+ def initialize(message_buffer, logger: nil, flush_interval: nil, queue_size: 1)
7
11
  @message_buffer = message_buffer
12
+ @logger = logger
13
+ @mx = Mutex.new
14
+ @message_queue_size = queue_size
15
+ @message_queue = []
16
+ @flush_timer = if flush_interval
17
+ Datadog::Statsd::Timer.new(flush_interval) { flush }
18
+ else
19
+ nil
20
+ end
21
+ # store the pid for which this sender has been created
22
+ update_fork_pid
8
23
  end
9
24
 
10
25
  def add(message)
11
- @message_buffer.add(message)
26
+ @mx.synchronize {
27
+ # we have just forked, meaning we have messages in the buffer that we should
28
+ # not send, they belong to the parent process, let's clear the buffer.
29
+ if forked?
30
+ @message_buffer.reset
31
+ @message_queue.clear
32
+ @flush_timer.start if @flush_timer && @flush_timer.stop?
33
+ update_fork_pid
34
+ end
35
+
36
+ @message_queue << message
37
+ if @message_queue.size >= @message_queue_size
38
+ drain_message_queue
39
+ end
40
+ }
12
41
  end
13
42
 
14
43
  def flush(*)
15
- @message_buffer.flush()
44
+ @mx.synchronize {
45
+ drain_message_queue
46
+ @message_buffer.flush()
47
+ }
16
48
  end
17
49
 
18
- # Compatibility with `Sender`
19
50
  def start()
51
+ @flush_timer.start if @flush_timer
20
52
  end
21
53
 
22
- # Compatibility with `Sender`
23
54
  def stop()
55
+ @flush_timer.stop if @flush_timer
24
56
  end
25
57
 
26
58
  # Compatibility with `Sender`
27
59
  def rendez_vous()
28
60
  end
61
+
62
+ private
63
+
64
+ def drain_message_queue
65
+ while msg = @message_queue.shift
66
+ @message_buffer.add(msg)
67
+ end
68
+ end
69
+
70
+ # below are "fork management" methods to be able to clean the MessageBuffer
71
+ # if it detects that it is running in a unknown PID.
72
+
73
+ def forked?
74
+ Process.pid != @fork_pid
75
+ end
76
+
77
+ def update_fork_pid
78
+ @fork_pid = Process.pid
79
+ end
29
80
  end
30
81
  end
31
82
  end
@@ -9,8 +9,12 @@ module Datadog
9
9
  attr_reader :service_checks
10
10
  attr_reader :bytes_sent
11
11
  attr_reader :bytes_dropped
12
+ attr_reader :bytes_dropped_queue
13
+ attr_reader :bytes_dropped_writer
12
14
  attr_reader :packets_sent
13
15
  attr_reader :packets_dropped
16
+ attr_reader :packets_dropped_queue
17
+ attr_reader :packets_dropped_writer
14
18
 
15
19
  # Rough estimation of maximum telemetry message size without tags
16
20
  MAX_TELEMETRY_MESSAGE_SIZE_WT_TAGS = 50 # bytes
@@ -40,8 +44,12 @@ module Datadog
40
44
  @service_checks = 0
41
45
  @bytes_sent = 0
42
46
  @bytes_dropped = 0
47
+ @bytes_dropped_queue = 0
48
+ @bytes_dropped_writer = 0
43
49
  @packets_sent = 0
44
50
  @packets_dropped = 0
51
+ @packets_dropped_queue = 0
52
+ @packets_dropped_writer = 0
45
53
  @next_flush_time = now_in_s + @flush_interval
46
54
  end
47
55
 
@@ -54,9 +62,18 @@ module Datadog
54
62
  @packets_sent += packets
55
63
  end
56
64
 
57
- def dropped(bytes: 0, packets: 0)
65
+ def dropped_queue(bytes: 0, packets: 0)
58
66
  @bytes_dropped += bytes
67
+ @bytes_dropped_queue += bytes
59
68
  @packets_dropped += packets
69
+ @packets_dropped_queue += packets
70
+ end
71
+
72
+ def dropped_writer(bytes: 0, packets: 0)
73
+ @bytes_dropped += bytes
74
+ @bytes_dropped_writer += bytes
75
+ @packets_dropped += packets
76
+ @packets_dropped_writer += packets
60
77
  end
61
78
 
62
79
  def should_flush?
@@ -70,8 +87,12 @@ module Datadog
70
87
  sprintf(pattern, 'service_checks', @service_checks),
71
88
  sprintf(pattern, 'bytes_sent', @bytes_sent),
72
89
  sprintf(pattern, 'bytes_dropped', @bytes_dropped),
90
+ sprintf(pattern, 'bytes_dropped_queue', @bytes_dropped_queue),
91
+ sprintf(pattern, 'bytes_dropped_writer', @bytes_dropped_writer),
73
92
  sprintf(pattern, 'packets_sent', @packets_sent),
74
93
  sprintf(pattern, 'packets_dropped', @packets_dropped),
94
+ sprintf(pattern, 'packets_dropped_queue', @packets_dropped_queue),
95
+ sprintf(pattern, 'packets_dropped_writer', @packets_dropped_writer),
75
96
  ]
76
97
  end
77
98
 
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ class Statsd
5
+ class Timer
6
+ def initialize(interval, &callback)
7
+ @mx = Mutex.new
8
+ @cv = ConditionVariable.new
9
+ @interval = interval
10
+ @callback = callback
11
+ @stop = true
12
+ @thread = nil
13
+ end
14
+
15
+ def start
16
+ return unless stop?
17
+
18
+ @stop = false
19
+ @thread = Thread.new do
20
+ last_execution_time = current_time
21
+ @mx.synchronize do
22
+ until @stop
23
+ timeout = @interval - (current_time - last_execution_time)
24
+ @cv.wait(@mx, timeout > 0 ? timeout : 0)
25
+ last_execution_time = current_time
26
+ @callback.call
27
+ end
28
+ end
29
+ end
30
+ @thread.name = 'Statsd Timer' unless Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3')
31
+ end
32
+
33
+ def stop
34
+ return if @thread.nil?
35
+
36
+ @stop = true
37
+ @mx.synchronize do
38
+ @cv.signal
39
+ end
40
+ @thread.join
41
+ @thread = nil
42
+ end
43
+
44
+ def stop?
45
+ @thread.nil? || @thread.stop?
46
+ end
47
+
48
+ private
49
+
50
+ if Process.const_defined?(:CLOCK_MONOTONIC)
51
+ def current_time
52
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
53
+ end
54
+ else
55
+ def current_time
56
+ Time.now
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -5,32 +5,43 @@ require_relative 'connection'
5
5
  module Datadog
6
6
  class Statsd
7
7
  class UDPConnection < Connection
8
- DEFAULT_HOST = '127.0.0.1'
9
- DEFAULT_PORT = 8125
10
-
11
- # StatsD host. Defaults to 127.0.0.1.
8
+ # StatsD host.
12
9
  attr_reader :host
13
10
 
14
- # StatsD port. Defaults to 8125.
11
+ # StatsD port.
15
12
  attr_reader :port
16
13
 
17
14
  def initialize(host, port, **kwargs)
18
15
  super(**kwargs)
19
16
 
20
- @host = host || ENV.fetch('DD_AGENT_HOST', DEFAULT_HOST)
21
- @port = port || ENV.fetch('DD_DOGSTATSD_PORT', DEFAULT_PORT).to_i
17
+ @host = host
18
+ @port = port
19
+ @socket = nil
20
+ end
21
+
22
+ def close
23
+ @socket.close if @socket
24
+ @socket = nil
22
25
  end
23
26
 
24
27
  private
25
28
 
26
29
  def connect
27
- UDPSocket.new.tap do |socket|
28
- socket.connect(host, port)
29
- end
30
+ close if @socket
31
+
32
+ family = Addrinfo.udp(host, port).afamily
33
+
34
+ @socket = UDPSocket.new(family)
35
+ @socket.connect(host, port)
30
36
  end
31
37
 
38
+ # send_message is writing the message in the socket, it may create the socket if nil
39
+ # It is not thread-safe but since it is called by either the Sender bg thread or the
40
+ # SingleThreadSender (which is using a mutex while Flushing), only one thread must call
41
+ # it at a time.
32
42
  def send_message(message)
33
- socket.send(message, 0)
43
+ connect unless @socket
44
+ @socket.send(message, 0)
34
45
  end
35
46
  end
36
47
  end
@@ -14,20 +14,31 @@ module Datadog
14
14
  super(**kwargs)
15
15
 
16
16
  @socket_path = socket_path
17
+ @socket = nil
18
+ end
19
+
20
+ def close
21
+ @socket.close if @socket
22
+ @socket = nil
17
23
  end
18
24
 
19
25
  private
20
26
 
21
27
  def connect
22
- socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM)
23
- socket.connect(Socket.pack_sockaddr_un(@socket_path))
24
- socket
28
+ close if @socket
29
+
30
+ @socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM)
31
+ @socket.connect(Socket.pack_sockaddr_un(@socket_path))
25
32
  end
26
33
 
34
+ # send_message is writing the message in the socket, it may create the socket if nil
35
+ # It is not thread-safe but since it is called by either the Sender bg thread or the
36
+ # SingleThreadSender (which is using a mutex while Flushing), only one thread must call
37
+ # it at a time.
27
38
  def send_message(message)
28
- socket.sendmsg_nonblock(message)
39
+ connect unless @socket
40
+ @socket.sendmsg_nonblock(message)
29
41
  rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ENOENT => e
30
- @socket = nil
31
42
  # TODO: FIXME: This error should be considered as a retryable error in the
32
43
  # Connection class. An even better solution would be to make BadSocketError inherit
33
44
  # from a specific retryable error class in the Connection class.
@@ -4,6 +4,6 @@ require_relative 'connection'
4
4
 
5
5
  module Datadog
6
6
  class Statsd
7
- VERSION = '5.2.0'
7
+ VERSION = '5.6.1'
8
8
  end
9
9
  end