flare 0.3.1 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1adc85107fcbe273da78049b6882bda0f234913b743b5c67988b8b90f53261ae
4
- data.tar.gz: c46bca41c454100e57ea005ad2a83af2bb7ada763ba48a9cb2ffb19e0811198f
3
+ metadata.gz: cee6e0910ebcd04f073efc41638823f7ed4d7abf511ab71b68caead994997af1
4
+ data.tar.gz: f28d669e444b2abb8b6043990b8f1cfc5cbd39abc14d25942cf5f41e08a80cb2
5
5
  SHA512:
6
- metadata.gz: 76e48757e0e4dedffd5363b6cbb671271f2e1447cc1b06f1f1d1518315ab775a9198f72c522cbb74fc3969c54e93b2baca82d65919855025247e28897f015f9f
7
- data.tar.gz: 89661ee1a5b0a1aa90cd21d410c087c359351b2714211f55d4a33bb35a739ef943348771fb7bf3a9cb7f4e8fd66446b0f213ac34f1c4da0e9f54b0babea74ec5
6
+ metadata.gz: 06647d35a64ae38dafb6e946b181fd87a8b8dc8e7065c5b0d4af3cb47419bd5be5dc9e930f3aa322740e824279169d9b2ae867c2b3c14dfda22c1d01be25507d
7
+ data.tar.gz: 837f70a2ef44f4d0f8c415cc15c6455ab5250b63e7dd06580ad640492385d8d1e6842cda3f56b036d918bb38c7b55438a2d60546a6c41fd7ce77a429b2d1793a
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flare
4
+ # A small monotonic deadline shared by lifecycle operations. A nil timeout
5
+ # represents an unbounded operation.
6
+ class Deadline
7
+ def initialize(timeout)
8
+ @expires_at = monotonic_now + [timeout.to_f, 0].max unless timeout.nil?
9
+ end
10
+
11
+ def remaining
12
+ return nil unless @expires_at
13
+
14
+ [@expires_at - monotonic_now, 0].max
15
+ end
16
+
17
+ def expired?
18
+ remaining == 0
19
+ end
20
+
21
+ private
22
+
23
+ def monotonic_now
24
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
25
+ end
26
+ end
27
+ end
@@ -4,6 +4,8 @@ require "concurrent/atomic/atomic_fixnum"
4
4
  require "logger"
5
5
  require "opentelemetry/sdk"
6
6
 
7
+ require_relative "deadline"
8
+
7
9
  module Flare
8
10
  # BSP-shaped span processor whose filter is `sampled OR marked` instead
9
11
  # of BSP's `sampled` (BSP early-returns on RECORD_ONLY spans -- our
@@ -17,6 +19,7 @@ module Flare
17
19
  class FilteringSpanProcessor
18
20
  SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
19
21
  FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE
22
+ TIMEOUT = OpenTelemetry::SDK::Trace::Export::TIMEOUT
20
23
 
21
24
  DEFAULT_MAX_QUEUE = 5_000
22
25
  DEFAULT_FLUSH_INTERVAL = 5 # seconds
@@ -47,6 +50,10 @@ module Flare
47
50
  @mutex = Mutex.new
48
51
  @cond = ConditionVariable.new
49
52
  @stopped = false
53
+ @active_exports = 0
54
+ @export_completion_sequence = 0
55
+ @last_export_result = SUCCESS
56
+ @flush_owner = nil
50
57
  @pid = $$
51
58
 
52
59
  @dropped_count = Concurrent::AtomicFixnum.new(0)
@@ -78,19 +85,43 @@ module Flare
78
85
  end
79
86
 
80
87
  def force_flush(timeout: nil)
81
- drain_and_export(include_pending: true)
82
- SUCCESS
88
+ detect_forking
89
+ deadline = Deadline.new(timeout)
90
+ return TIMEOUT unless begin_flush(deadline)
91
+ prior_result = @flush_prior_result
92
+
93
+ batch = snapshot_for_flush(deadline)
94
+ return TIMEOUT unless batch
95
+
96
+ operation = start_flush_export(batch, deadline)
97
+ result = wait_for_flush_export(operation, deadline)
98
+ [prior_result, result].max
99
+ ensure
100
+ finish_flush if @flush_owner == Thread.current
83
101
  end
84
102
 
85
103
  def shutdown(timeout: nil)
86
- @mutex.synchronize do
104
+ detect_forking
105
+ deadline = Deadline.new(timeout)
106
+ return TIMEOUT unless lock_before_deadline(deadline)
107
+
108
+ begin
87
109
  @stopped = true
88
110
  @cond.broadcast
111
+ ensure
112
+ @mutex.unlock
89
113
  end
90
- @worker.join(timeout || 5)
91
- drain_and_export(include_pending: true)
92
- @exporter.shutdown(timeout: timeout) if @exporter.respond_to?(:shutdown)
93
- SUCCESS
114
+ @worker.join(deadline.remaining || 5)
115
+ return TIMEOUT if @worker.alive? || deadline.expired?
116
+
117
+ result = force_flush(timeout: deadline.remaining)
118
+ return result unless result == SUCCESS
119
+ return TIMEOUT if deadline.expired?
120
+
121
+ exporter_result = @exporter.shutdown(timeout: deadline.remaining) if @exporter.respond_to?(:shutdown)
122
+ return TIMEOUT if deadline.expired?
123
+
124
+ exporter_result || SUCCESS
94
125
  end
95
126
 
96
127
  def buffer_size
@@ -124,7 +155,8 @@ module Flare
124
155
  until stopped?
125
156
  @mutex.synchronize do
126
157
  timeout = next_wait_timeout
127
- @cond.wait(@mutex, timeout) if @ready_queue.empty? && !@stopped
158
+ waiting_for_export = @flush_owner || @active_exports.positive?
159
+ @cond.wait(@mutex, timeout) if (@ready_queue.empty? || waiting_for_export) && !@stopped
128
160
  end
129
161
  drain_and_export
130
162
  end
@@ -134,30 +166,141 @@ module Flare
134
166
  @mutex.synchronize { @stopped }
135
167
  end
136
168
 
137
- def drain_and_export(include_pending: false)
169
+ def drain_and_export
138
170
  batch = nil
139
171
  @mutex.synchronize do
140
172
  promote_due_delayed_traces
173
+ return if @ready_queue.empty? || @flush_owner || @active_exports.positive?
141
174
 
142
- if include_pending
143
- @ready_queue.concat(@pending_by_trace.values.flatten)
144
- @pending_by_trace.clear
145
- @trace_order.clear
146
- @pending_count = 0
147
- unmark_delayed_traces
148
- @delayed_ready_by_trace.clear
175
+ batch = @ready_queue
176
+ @ready_queue = []
177
+ @active_exports += 1
178
+ end
179
+
180
+ result = export_batch(batch, timeout: @export_timeout)
181
+ ensure
182
+ export_finished(result || FAILURE) if batch
183
+ end
184
+
185
+ def begin_flush(deadline)
186
+ return false unless lock_before_deadline(deadline)
187
+
188
+ begin
189
+ initial_sequence = @export_completion_sequence
190
+ while @flush_owner && @flush_owner != Thread.current
191
+ return false if deadline.expired?
192
+
193
+ @cond.wait(@mutex, deadline.remaining)
149
194
  end
195
+ @flush_owner = Thread.current
196
+
197
+ while @active_exports.positive?
198
+ return false if deadline.expired?
150
199
 
151
- return if @ready_queue.empty?
200
+ @cond.wait(@mutex, deadline.remaining)
201
+ end
202
+ @flush_prior_result = if @export_completion_sequence > initial_sequence
203
+ @last_export_result
204
+ else
205
+ SUCCESS
206
+ end
207
+ ensure
208
+ @mutex.unlock
209
+ end
210
+ true
211
+ end
212
+
213
+ def finish_flush
214
+ @mutex.synchronize do
215
+ @flush_owner = nil
216
+ @cond.broadcast
217
+ end
218
+ end
219
+
220
+ def snapshot_for_flush(deadline)
221
+ return unless lock_before_deadline(deadline)
222
+
223
+ begin
224
+ @ready_queue.concat(@pending_by_trace.values.flatten)
225
+ @pending_by_trace.clear
226
+ @trace_order.clear
227
+ @pending_count = 0
228
+ unmark_delayed_traces
229
+ @delayed_ready_by_trace.clear
152
230
  batch = @ready_queue
153
231
  @ready_queue = []
232
+ batch
233
+ ensure
234
+ @mutex.unlock
235
+ end
236
+ end
237
+
238
+ def start_flush_export(batch, deadline)
239
+ operation = { done: false, result: nil }
240
+ @mutex.synchronize { @active_exports += 1 }
241
+ Thread.new do
242
+ result = batch.empty? ? SUCCESS : export_batch(batch, timeout: deadline.remaining)
243
+ if result == SUCCESS && !deadline.expired? && @exporter.respond_to?(:force_flush)
244
+ result = @exporter.force_flush(timeout: deadline.remaining)
245
+ end
246
+ result = TIMEOUT if deadline.expired?
247
+ operation[:result] = result
248
+ rescue StandardError => e
249
+ @exception_count.increment
250
+ @logger.warn("[Flare::FilteringSpanProcessor] force flush failed: #{e.class}: #{e.message}")
251
+ operation[:result] = FAILURE
252
+ ensure
253
+ @mutex.synchronize do
254
+ operation[:done] = true
255
+ complete_export(operation[:result])
256
+ end
154
257
  end
258
+ operation
259
+ end
260
+
261
+ def wait_for_flush_export(operation, deadline)
262
+ @mutex.synchronize do
263
+ until operation[:done]
264
+ return TIMEOUT if deadline.expired?
155
265
 
156
- result = @exporter.export(batch, timeout: @export_timeout)
266
+ @cond.wait(@mutex, deadline.remaining)
267
+ end
268
+ end
269
+ operation[:result]
270
+ end
271
+
272
+ def export_batch(batch, timeout:)
273
+ result = @exporter.export(batch, timeout: timeout)
157
274
  @failed_export_count.increment if result != SUCCESS
275
+ result
158
276
  rescue StandardError => e
159
277
  @exception_count.increment
160
278
  @logger.warn("[Flare::FilteringSpanProcessor] export failed: #{e.class}: #{e.message}")
279
+ FAILURE
280
+ end
281
+
282
+ def export_finished(result)
283
+ @mutex.synchronize do
284
+ complete_export(result)
285
+ end
286
+ end
287
+
288
+ def complete_export(result)
289
+ @active_exports -= 1
290
+ @export_completion_sequence += 1
291
+ @last_export_result = result || FAILURE
292
+ @cond.broadcast
293
+ end
294
+
295
+ def lock_before_deadline(deadline)
296
+ return @mutex.lock unless deadline.remaining
297
+
298
+ until @mutex.try_lock
299
+ return false if deadline.expired?
300
+
301
+ sleep([deadline.remaining, 0.001].min)
302
+ end
303
+ true
161
304
  end
162
305
 
163
306
  def mark_trace_ready(trace_id)
@@ -255,18 +398,25 @@ module Flare
255
398
  def detect_forking
256
399
  return if @pid == $$
257
400
 
258
- @mutex.synchronize do
259
- return if @pid == $$
260
-
261
- @pid = $$
262
- @pending_by_trace.clear
263
- @trace_order.clear
264
- @ready_queue.clear
265
- @delayed_ready_by_trace.clear
266
- @pending_count = 0
267
- @stopped = false
268
- start_worker
269
- end
401
+ # The child only retains the forking thread. Replace synchronization
402
+ # objects so it cannot inherit locks or active-export bookkeeping owned
403
+ # by vanished threads.
404
+ @pid = $$
405
+ @mutex = Mutex.new
406
+ @cond = ConditionVariable.new
407
+ @pending_by_trace = {}
408
+ @trace_order = []
409
+ @ready_queue = []
410
+ @delayed_ready_by_trace = {}
411
+ @pending_count = 0
412
+ @active_exports = 0
413
+ @export_completion_sequence = 0
414
+ @last_export_result = SUCCESS
415
+ @flush_prior_result = SUCCESS
416
+ @flush_owner = nil
417
+ @stopped = false
418
+ @worker = nil
419
+ start_worker
270
420
  end
271
421
 
272
422
  def start_worker
@@ -3,12 +3,15 @@
3
3
  require "net/http"
4
4
  require "uri"
5
5
 
6
+ require_relative "deadline"
7
+
6
8
  module Flare
7
9
  # Tiny HTTP wrapper used by TraceExporter (and anything else that wants
8
10
  # to PUT/POST without pulling in a heavy client). Designed for injection
9
11
  # at the boundary so tests can swap in a recording fake; no other moving
10
12
  # parts.
11
13
  class HttpTransport
14
+ DeadlineExceeded = Class.new(StandardError)
12
15
  DEFAULT_OPEN_TIMEOUT = 2
13
16
  DEFAULT_READ_TIMEOUT = 5
14
17
  DEFAULT_WRITE_TIMEOUT = 5
@@ -28,35 +31,46 @@ module Flare
28
31
  @write_timeout = write_timeout
29
32
  end
30
33
 
31
- def get(url, headers = {})
32
- request(url, nil, headers, Net::HTTP::Get)
34
+ def get(url, headers = {}, timeout: nil)
35
+ request(url, nil, headers, Net::HTTP::Get, timeout: timeout)
33
36
  end
34
37
 
35
- def put(url, body, headers = {})
36
- request(url, body, headers, Net::HTTP::Put)
38
+ def put(url, body, headers = {}, timeout: nil)
39
+ request(url, body, headers, Net::HTTP::Put, timeout: timeout)
37
40
  end
38
41
 
39
- def post(url, body, headers = {})
40
- request(url, body, headers, Net::HTTP::Post)
42
+ def post(url, body, headers = {}, timeout: nil)
43
+ request(url, body, headers, Net::HTTP::Post, timeout: timeout)
41
44
  end
42
45
 
43
46
  private
44
47
 
45
- def request(url, body, headers, klass)
48
+ def request(url, body, headers, klass, timeout: nil)
49
+ deadline = Deadline.new(timeout)
50
+ raise DeadlineExceeded if deadline.expired?
51
+
46
52
  uri = URI(url)
47
53
  http = Net::HTTP.new(uri.host, uri.port)
48
54
  http.use_ssl = uri.scheme == "https"
49
- http.open_timeout = @open_timeout
50
- http.read_timeout = @read_timeout
51
- http.write_timeout = @write_timeout if http.respond_to?(:write_timeout=)
55
+ http.open_timeout = effective_timeout(@open_timeout, deadline.remaining)
56
+ http.read_timeout = effective_timeout(@read_timeout, deadline.remaining)
57
+ http.write_timeout = effective_timeout(@write_timeout, deadline.remaining) if http.respond_to?(:write_timeout=)
52
58
 
53
59
  req = klass.new(uri.request_uri == "" ? "/" : uri.request_uri)
54
60
  headers.each { |k, v| req[k] = v }
55
61
  req.body = body if body
56
62
 
57
63
  response = http.request(req)
64
+ raise DeadlineExceeded if deadline.expired?
65
+
58
66
  hash = response.each_header.to_h
59
67
  Response.new(code: response.code.to_s, body: response.body, headers: hash)
60
68
  end
69
+
70
+ def effective_timeout(configured_timeout, remaining)
71
+ return configured_timeout unless remaining
72
+
73
+ [configured_timeout, remaining].min
74
+ end
61
75
  end
62
76
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "opentelemetry/sdk"
4
+
5
+ require_relative "deadline"
6
+
7
+ module Flare
8
+ module Lifecycle
9
+ # Flush all trace processors and the separate aggregated metric pipeline
10
+ # using one monotonic timeout budget. This is safe to call from lifecycle
11
+ # hooks for short-lived and fork-per-job workers.
12
+ def force_flush(timeout: nil)
13
+ deadline = Deadline.new(timeout)
14
+ results = []
15
+
16
+ results << tracer_provider_for_flush.force_flush(timeout: deadline.remaining)
17
+ return OpenTelemetry::SDK::Trace::Export::TIMEOUT if deadline.expired?
18
+
19
+ flusher = metric_flusher_for_flush
20
+ results << flusher.force_flush(timeout: deadline.remaining) if flusher
21
+ return OpenTelemetry::SDK::Trace::Export::TIMEOUT if deadline.expired?
22
+
23
+ results.max || OpenTelemetry::SDK::Trace::Export::SUCCESS
24
+ rescue => e
25
+ warn "[Flare] Telemetry flush error: #{e.message}"
26
+ OpenTelemetry::SDK::Trace::Export::FAILURE
27
+ end
28
+
29
+ def tracer_provider_for_flush
30
+ OpenTelemetry.tracer_provider
31
+ end
32
+
33
+ def metric_flusher_for_flush
34
+ @metric_flusher
35
+ end
36
+ end
37
+
38
+ extend Lifecycle
39
+ end
@@ -2,6 +2,9 @@
2
2
 
3
3
  require "concurrent/timer_task"
4
4
  require "concurrent/executor/fixed_thread_pool"
5
+ require "opentelemetry/sdk"
6
+
7
+ require_relative "deadline"
5
8
 
6
9
  module Flare
7
10
  # Background threads that periodically drain in-memory metrics and submit
@@ -10,6 +13,9 @@ module Flare
10
13
  #
11
14
  # Fork-safe: detects forked processes and restarts automatically.
12
15
  class MetricFlusher
16
+ SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
17
+ FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE
18
+ TIMEOUT = OpenTelemetry::SDK::Trace::Export::TIMEOUT
13
19
  DEFAULT_INTERVAL = 60 # seconds
14
20
  DEFAULT_SHUTDOWN_TIMEOUT = 5 # seconds
15
21
 
@@ -23,6 +29,7 @@ module Flare
23
29
  @health_reporters = Array(health_reporters)
24
30
  @pid = $$
25
31
  @stopped = false
32
+ initialize_synchronization
26
33
  end
27
34
 
28
35
  def start
@@ -40,23 +47,25 @@ module Flare
40
47
  }) { post_to_pool }
41
48
  end
42
49
 
43
- def stop
50
+ def stop(timeout: @shutdown_timeout)
44
51
  return if @stopped
45
52
 
53
+ deadline = Deadline.new(timeout)
46
54
  @stopped = true
47
55
 
48
56
  log "Shutting down metrics flusher, draining remaining metrics..."
49
57
 
50
58
  if @timer
51
59
  @timer.shutdown
52
- @timer.wait_for_termination(1)
60
+ @timer.wait_for_termination([deadline.remaining || 1, 1].min)
53
61
  @timer.kill unless @timer.shutdown?
54
62
  end
55
63
 
64
+ force_flush(timeout: deadline.remaining)
65
+
56
66
  if @pool
57
- post_to_pool # one last drain
58
67
  @pool.shutdown
59
- pool_terminated = @pool.wait_for_termination(@shutdown_timeout)
68
+ pool_terminated = @pool.wait_for_termination(deadline.remaining || @shutdown_timeout)
60
69
  @pool.kill unless pool_terminated
61
70
  end
62
71
 
@@ -70,14 +79,11 @@ module Flare
70
79
  end
71
80
 
72
81
  # Manually trigger a flush (useful for testing or forced flushes).
73
- def flush_now
82
+ def flush_now(timeout: nil)
74
83
  return 0 unless @storage && @submitter
75
84
 
76
- record_health_metrics
77
- drained = @storage.drain
78
- return 0 if drained.empty?
79
-
80
- count, error = @submitter.submit(drained)
85
+ detect_forking
86
+ count, error, = flush_synchronously(Deadline.new(timeout))
81
87
  if error
82
88
  warn "[Flare] Metric submission error: #{error.message}"
83
89
  end
@@ -87,6 +93,21 @@ module Flare
87
93
  0
88
94
  end
89
95
 
96
+ def force_flush(timeout: nil)
97
+ return SUCCESS unless @storage && @submitter
98
+
99
+ detect_forking
100
+ deadline = Deadline.new(timeout)
101
+ _count, error, timed_out = flush_synchronously(deadline)
102
+ return TIMEOUT if timed_out || deadline.expired?
103
+ return FAILURE if error
104
+
105
+ SUCCESS
106
+ rescue => e
107
+ warn "[Flare] Metric flush error: #{e.message}"
108
+ FAILURE
109
+ end
110
+
90
111
  def running?
91
112
  @timer&.running? || false
92
113
  end
@@ -96,22 +117,46 @@ module Flare
96
117
  # after_fork hooks.
97
118
  def after_fork
98
119
  @pid = $$
99
- restart
120
+ @storage.after_fork if @storage.respond_to?(:after_fork)
121
+ initialize_synchronization
122
+ @timer = nil
123
+ @pool = nil
124
+ start
100
125
  end
101
126
 
102
127
  private
103
128
 
129
+ def detect_forking
130
+ after_fork if @pid != $$
131
+ end
132
+
133
+ def initialize_synchronization
134
+ @submission_mutex = Mutex.new
135
+ @submission_condition = ConditionVariable.new
136
+ @pending_submissions = 0
137
+ @flush_owner = nil
138
+ end
139
+
104
140
  def post_to_pool
141
+ return unless reserve_background_submission
142
+
105
143
  record_health_metrics
106
144
  drained = @storage.drain
107
145
  if drained.empty?
108
146
  log "No metrics to flush"
147
+ background_submission_finished
109
148
  return
110
149
  end
111
150
 
112
151
  log "Drained #{drained.size} metric keys for submission"
113
- @pool.post { submit_to_cloud(drained) }
152
+ posted = @pool.post do
153
+ submit_to_cloud(drained)
154
+ ensure
155
+ background_submission_finished
156
+ end
157
+ background_submission_finished unless posted
114
158
  rescue => e
159
+ background_submission_finished
115
160
  warn "[Flare] Metric drain error: #{e.message}"
116
161
  end
117
162
 
@@ -124,6 +169,103 @@ module Flare
124
169
  warn "[Flare] Metric submission error: #{e.message}"
125
170
  end
126
171
 
172
+ def reserve_background_submission
173
+ @submission_mutex.synchronize do
174
+ return false if @flush_owner || @pending_submissions.positive?
175
+
176
+ @pending_submissions += 1
177
+ true
178
+ end
179
+ end
180
+
181
+ def background_submission_finished
182
+ @submission_mutex.synchronize do
183
+ @pending_submissions -= 1 if @pending_submissions.positive?
184
+ @submission_condition.broadcast
185
+ end
186
+ end
187
+
188
+ def flush_synchronously(deadline)
189
+ return [0, nil, true] unless begin_synchronous_flush(deadline)
190
+
191
+ record_health_metrics
192
+ drained = @storage.drain
193
+ return [0, nil, false] if drained.empty?
194
+
195
+ submit_with_deadline(drained, deadline)
196
+ ensure
197
+ finish_synchronous_flush if @flush_owner == Thread.current
198
+ end
199
+
200
+ def begin_synchronous_flush(deadline)
201
+ @submission_mutex.synchronize do
202
+ while @flush_owner && @flush_owner != Thread.current
203
+ return false if deadline.expired?
204
+
205
+ @submission_condition.wait(@submission_mutex, deadline.remaining)
206
+ end
207
+ @flush_owner = Thread.current
208
+
209
+ while @pending_submissions.positive?
210
+ return false if deadline.expired?
211
+
212
+ @submission_condition.wait(@submission_mutex, deadline.remaining)
213
+ end
214
+ end
215
+ true
216
+ end
217
+
218
+ def finish_synchronous_flush
219
+ @submission_mutex.synchronize do
220
+ @flush_owner = nil
221
+ @submission_condition.broadcast
222
+ end
223
+ end
224
+
225
+ def submit_metrics(drained, timeout:)
226
+ parameters = @submitter.method(:submit).parameters
227
+ accepts_timeout = parameters.any? do |type, name|
228
+ type == :keyrest || ([:key, :keyreq].include?(type) && name == :timeout)
229
+ end
230
+
231
+ if accepts_timeout
232
+ @submitter.submit(drained, timeout: timeout)
233
+ else
234
+ @submitter.submit(drained)
235
+ end
236
+ end
237
+
238
+ def submit_with_deadline(drained, deadline)
239
+ operation = { done: false, count: 0, error: nil }
240
+ @submission_mutex.synchronize { @pending_submissions += 1 }
241
+ Thread.new do
242
+ operation[:count], operation[:error] = submit_metrics(drained, timeout: deadline.remaining)
243
+ rescue => e
244
+ operation[:error] = e
245
+ ensure
246
+ @submission_mutex.synchronize do
247
+ operation[:done] = true
248
+ @pending_submissions -= 1
249
+ @submission_condition.broadcast
250
+ end
251
+ end
252
+
253
+ @submission_mutex.synchronize do
254
+ until operation[:done]
255
+ return [0, nil, true] if deadline.expired?
256
+
257
+ @submission_condition.wait(@submission_mutex, deadline.remaining)
258
+ end
259
+ end
260
+
261
+ timed_out = deadline.expired? || deadline_error?(operation[:error])
262
+ [operation[:count], operation[:error], timed_out]
263
+ end
264
+
265
+ def deadline_error?(error)
266
+ defined?(MetricSubmitter::DeadlineExceeded) && error.is_a?(MetricSubmitter::DeadlineExceeded)
267
+ end
268
+
127
269
  def record_health_metrics
128
270
  @health_reporters.each { |reporter| reporter.record(@storage) }
129
271
  rescue => e