dogstatsd-ruby 4.8.1 → 5.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
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).
12
+ class Sender
13
+ CLOSEABLE_QUEUES = Queue.instance_methods.include?(:close)
14
+
15
+ def initialize(message_buffer, telemetry: nil, queue_size: UDP_DEFAULT_BUFFER_SIZE, logger: nil, flush_interval: nil, queue_class: Queue, thread_class: Thread)
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
28
+ end
29
+
30
+ def flush(sync: false)
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
44
+
45
+ current_message_queue.push(:flush)
46
+ rendez_vous if sync
47
+ end
48
+
49
+ def rendez_vous
50
+ # could happen if #start hasn't be called
51
+ return unless message_queue
52
+
53
+ # Initialize and get the thread's sync queue
54
+ queue = (@thread_class.current[:statsd_sync_queue] ||= @queue_class.new)
55
+ # tell sender-thread to notify us in the current
56
+ # thread's queue
57
+ message_queue.push(queue)
58
+ # wait for the sender thread to send a message
59
+ # once the flush is done
60
+ queue.pop
61
+ end
62
+
63
+ def add(message)
64
+ raise ArgumentError, 'Start sender first' unless message_queue
65
+
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
92
+ end
93
+
94
+ def start
95
+ raise ArgumentError, 'Sender already started' if message_queue
96
+
97
+ # initialize a new message queue for the background thread
98
+ @message_queue = @queue_class.new
99
+ # start background thread
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
103
+ end
104
+
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.
109
+ def stop(join_worker: true)
110
+ @flush_timer.stop if @flush_timer
111
+
112
+ message_queue = @message_queue
113
+ message_queue.close if message_queue
114
+
115
+ sender_thread = @sender_thread
116
+ sender_thread.join if sender_thread && join_worker
117
+ end
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.
122
+ def stop(join_worker: true)
123
+ @flush_timer.stop if @flush_timer
124
+
125
+ message_queue = @message_queue
126
+ message_queue << :close if message_queue
127
+
128
+ sender_thread = @sender_thread
129
+ sender_thread.join if sender_thread && join_worker
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ attr_reader :message_buffer
136
+ attr_reader :message_queue
137
+ attr_reader :sender_thread
138
+
139
+ if CLOSEABLE_QUEUES
140
+ def send_loop
141
+ until (message = message_queue.pop).nil? && message_queue.closed?
142
+ # skip if message is nil, e.g. when message_queue
143
+ # is empty and closed
144
+ next unless message
145
+
146
+ case message
147
+ when :flush
148
+ message_buffer.flush
149
+ when @queue_class
150
+ message.push(:go_on)
151
+ else
152
+ message_buffer.add(message)
153
+ end
154
+ end
155
+
156
+ @message_queue = nil
157
+ @sender_thread = nil
158
+ end
159
+ else
160
+ def send_loop
161
+ loop do
162
+ message = message_queue.pop
163
+
164
+ next unless message
165
+
166
+ case message
167
+ when :close
168
+ break
169
+ when :flush
170
+ message_buffer.flush
171
+ when @queue_class
172
+ message.push(:go_on)
173
+ else
174
+ message_buffer.add(message)
175
+ end
176
+ end
177
+
178
+ @message_queue = nil
179
+ @sender_thread = nil
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -48,7 +48,11 @@ module Datadog
48
48
  end
49
49
 
50
50
  if event.bytesize > MAX_EVENT_SIZE
51
- raise "Event #{title} payload is too big (more that 8KB), event discarded"
51
+ if options[:truncate_if_too_long]
52
+ event.slice!(MAX_EVENT_SIZE..event.length)
53
+ else
54
+ raise "Event #{title} payload is too big (more that 8KB), event discarded"
55
+ end
52
56
  end
53
57
  end
54
58
  end
@@ -6,34 +6,24 @@ module Datadog
6
6
  class StatSerializer
7
7
  def initialize(prefix, global_tags: [])
8
8
  @prefix = prefix
9
+ @prefix_str = prefix.to_s
9
10
  @tag_serializer = TagSerializer.new(global_tags)
10
11
  end
11
12
 
12
13
  def format(name, delta, type, tags: [], sample_rate: 1)
13
- String.new.tap do |stat|
14
- stat << prefix if prefix
15
-
16
- # stat value
17
- stat << formated_name(name)
18
- stat << ':'
19
- stat << delta.to_s
20
-
21
- # stat type
22
- stat << '|'
23
- stat << type
24
-
25
- # sample_rate
26
- if sample_rate != 1
27
- stat << '|'
28
- stat << '@'
29
- stat << sample_rate.to_s
30
- end
14
+ name = formated_name(name)
31
15
 
32
- # tags
16
+ if sample_rate != 1
17
+ if tags_list = tag_serializer.format(tags)
18
+ "#{@prefix_str}#{name}:#{delta}|#{type}|@#{sample_rate}|##{tags_list}"
19
+ else
20
+ "#{@prefix_str}#{name}:#{delta}|#{type}|@#{sample_rate}"
21
+ end
22
+ else
33
23
  if tags_list = tag_serializer.format(tags)
34
- stat << '|'
35
- stat << '#'
36
- stat << tags_list
24
+ "#{@prefix_str}#{name}:#{delta}|#{type}|##{tags_list}"
25
+ else
26
+ "#{@prefix_str}#{name}:#{delta}|#{type}"
37
27
  end
38
28
  end
39
29
  end
@@ -43,18 +33,21 @@ module Datadog
43
33
  end
44
34
 
45
35
  private
36
+
46
37
  attr_reader :prefix
47
38
  attr_reader :tag_serializer
48
39
 
49
40
  def formated_name(name)
50
- formated = name.is_a?(String) ? name.dup : name.to_s
51
-
52
- formated.tap do |f|
53
- # replace Ruby module scoping with '.'
54
- f.gsub!('::', '.')
55
- # replace reserved chars (: | @) with underscores.
56
- f.tr!(':|@', '_')
41
+ if name.is_a?(String)
42
+ # DEV: gsub is faster than dup.gsub!
43
+ formated = name.gsub('::', '.')
44
+ else
45
+ formated = name.to_s
46
+ formated.gsub!('::', '.')
57
47
  end
48
+
49
+ formated.tr!(':|@', '_')
50
+ formated
58
51
  end
59
52
  end
60
53
  end
@@ -13,18 +13,25 @@ module Datadog
13
13
 
14
14
  # Convert to tag list and set
15
15
  @global_tags = to_tags_list(global_tags)
16
+ if @global_tags.any?
17
+ @global_tags_formatted = @global_tags.join(',')
18
+ else
19
+ @global_tags_formatted = nil
20
+ end
16
21
  end
17
22
 
18
23
  def format(message_tags)
19
- # fast return global tags if there's no message_tags
20
- # to avoid more allocations
21
- tag_list = if message_tags && message_tags.any?
22
- global_tags + to_tags_list(message_tags)
23
- else
24
- global_tags
25
- end
24
+ if !message_tags || message_tags.empty?
25
+ return @global_tags_formatted
26
+ end
26
27
 
27
- tag_list.join(',') if tag_list.any?
28
+ tags = if @global_tags_formatted
29
+ [@global_tags_formatted, to_tags_list(message_tags)]
30
+ else
31
+ to_tags_list(message_tags)
32
+ end
33
+
34
+ tags.join(',')
28
35
  end
29
36
 
30
37
  attr_reader :global_tags
@@ -51,19 +58,17 @@ module Datadog
51
58
  def to_tags_list(tags)
52
59
  case tags
53
60
  when Hash
54
- tags.each_with_object([]) do |tag_pair, formatted_tags|
55
- if tag_pair.last.nil?
56
- formatted_tags << "#{tag_pair.first}"
61
+ tags.map do |name, value|
62
+ if value
63
+ escape_tag_content("#{name}:#{value}")
57
64
  else
58
- formatted_tags << "#{tag_pair.first}:#{tag_pair.last}"
65
+ escape_tag_content(name)
59
66
  end
60
67
  end
61
68
  when Array
62
- tags.dup
69
+ tags.map { |tag| escape_tag_content(tag) }
63
70
  else
64
71
  []
65
- end.map! do |tag|
66
- escape_tag_content(tag)
67
72
  end
68
73
  end
69
74
 
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
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.
9
+ class SingleThreadSender
10
+ def initialize(message_buffer, logger: nil, flush_interval: nil, queue_size: 1)
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
23
+ end
24
+
25
+ def 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
+ }
41
+ end
42
+
43
+ def flush(*)
44
+ @mx.synchronize {
45
+ drain_message_queue
46
+ @message_buffer.flush()
47
+ }
48
+ end
49
+
50
+ def start()
51
+ @flush_timer.start if @flush_timer
52
+ end
53
+
54
+ def stop()
55
+ @flush_timer.stop if @flush_timer
56
+ end
57
+
58
+ # Compatibility with `Sender`
59
+ def rendez_vous()
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
80
+ end
81
+ end
82
+ end
@@ -9,12 +9,17 @@ 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
14
- attr_reader :estimate_max_size
16
+ attr_reader :packets_dropped_queue
17
+ attr_reader :packets_dropped_writer
15
18
 
16
- def initialize(disabled, flush_interval, global_tags: [], transport_type: :udp)
17
- @disabled = disabled
19
+ # Rough estimation of maximum telemetry message size without tags
20
+ MAX_TELEMETRY_MESSAGE_SIZE_WT_TAGS = 50 # bytes
21
+
22
+ def initialize(flush_interval, global_tags: [], transport_type: :udp)
18
23
  @flush_interval = flush_interval
19
24
  @global_tags = global_tags
20
25
  @transport_type = transport_type
@@ -27,15 +32,10 @@ module Datadog
27
32
  client_version: VERSION,
28
33
  client_transport: transport_type,
29
34
  ).format(global_tags)
35
+ end
30
36
 
31
- # estimate_max_size is an estimation or the maximum size of the
32
- # telemetry payload. Since we don't want our packet to go over
33
- # 'max_buffer_bytes', we have to adjust with the size of the telemetry
34
- # (and any tags used). The telemetry payload size will change depending
35
- # on the actual value of metrics: metrics received, packet dropped,
36
- # etc. This is why we add a 63bytes margin: 9 bytes for each of the 7
37
- # telemetry metrics.
38
- @estimate_max_size = disabled ? 0 : flush.length + 9 * 7
37
+ def would_fit_in?(max_buffer_payload_size)
38
+ MAX_TELEMETRY_MESSAGE_SIZE_WT_TAGS + serialized_tags.size < max_buffer_payload_size
39
39
  end
40
40
 
41
41
  def reset
@@ -44,8 +44,12 @@ module Datadog
44
44
  @service_checks = 0
45
45
  @bytes_sent = 0
46
46
  @bytes_dropped = 0
47
+ @bytes_dropped_queue = 0
48
+ @bytes_dropped_writer = 0
47
49
  @packets_sent = 0
48
50
  @packets_dropped = 0
51
+ @packets_dropped_queue = 0
52
+ @packets_dropped_writer = 0
49
53
  @next_flush_time = now_in_s + @flush_interval
50
54
  end
51
55
 
@@ -58,32 +62,47 @@ module Datadog
58
62
  @packets_sent += packets
59
63
  end
60
64
 
61
- def dropped(bytes: 0, packets: 0)
65
+ def dropped_queue(bytes: 0, packets: 0)
62
66
  @bytes_dropped += bytes
67
+ @bytes_dropped_queue += bytes
63
68
  @packets_dropped += packets
69
+ @packets_dropped_queue += packets
64
70
  end
65
71
 
66
- def flush?
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
77
+ end
78
+
79
+ def should_flush?
67
80
  @next_flush_time < now_in_s
68
81
  end
69
82
 
70
83
  def flush
71
- return '' if @disabled
72
-
73
- # using shorthand syntax to reduce the garbage collection
74
- %Q(
75
- datadog.dogstatsd.client.metrics:#{@metrics}|#{COUNTER_TYPE}|##{serialized_tags}
76
- datadog.dogstatsd.client.events:#{@events}|#{COUNTER_TYPE}|##{serialized_tags}
77
- datadog.dogstatsd.client.service_checks:#{@service_checks}|#{COUNTER_TYPE}|##{serialized_tags}
78
- datadog.dogstatsd.client.bytes_sent:#{@bytes_sent}|#{COUNTER_TYPE}|##{serialized_tags}
79
- datadog.dogstatsd.client.bytes_dropped:#{@bytes_dropped}|#{COUNTER_TYPE}|##{serialized_tags}
80
- datadog.dogstatsd.client.packets_sent:#{@packets_sent}|#{COUNTER_TYPE}|##{serialized_tags}
81
- datadog.dogstatsd.client.packets_dropped:#{@packets_dropped}|#{COUNTER_TYPE}|##{serialized_tags})
84
+ [
85
+ sprintf(pattern, 'metrics', @metrics),
86
+ sprintf(pattern, 'events', @events),
87
+ sprintf(pattern, 'service_checks', @service_checks),
88
+ sprintf(pattern, 'bytes_sent', @bytes_sent),
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),
92
+ sprintf(pattern, 'packets_sent', @packets_sent),
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),
96
+ ]
82
97
  end
83
98
 
84
99
  private
85
100
  attr_reader :serialized_tags
86
101
 
102
+ def pattern
103
+ @pattern ||= "datadog.dogstatsd.client.%s:%d|#{COUNTER_TYPE}|##{serialized_tags}"
104
+ end
105
+
87
106
  if Kernel.const_defined?('Process') && Process.respond_to?(:clock_gettime)
88
107
  def now_in_s
89
108
  Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
@@ -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
- def initialize(host, port, logger, telemetry)
18
- super(telemetry)
19
- @host = host || ENV.fetch('DD_AGENT_HOST', DEFAULT_HOST)
20
- @port = port || ENV.fetch('DD_DOGSTATSD_PORT', DEFAULT_PORT).to_i
21
- @logger = logger
14
+ def initialize(host, port, **kwargs)
15
+ super(**kwargs)
16
+
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
@@ -10,24 +10,35 @@ module Datadog
10
10
  # DogStatsd unix socket path
11
11
  attr_reader :socket_path
12
12
 
13
- def initialize(socket_path, logger, telemetry)
14
- super(telemetry)
13
+ def initialize(socket_path, **kwargs)
14
+ super(**kwargs)
15
+
15
16
  @socket_path = socket_path
16
- @logger = logger
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.