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 +4 -4
- data/lib/flare/deadline.rb +27 -0
- data/lib/flare/filtering_span_processor.rb +180 -30
- data/lib/flare/http_transport.rb +24 -10
- data/lib/flare/lifecycle.rb +39 -0
- data/lib/flare/metric_flusher.rb +154 -12
- data/lib/flare/metric_storage.rb +20 -0
- data/lib/flare/metric_submitter.rb +34 -11
- data/lib/flare/recording_batch_span_processor.rb +238 -0
- data/lib/flare/sqlite_exporter.rb +80 -25
- data/lib/flare/trace_exporter.rb +44 -9
- data/lib/flare/version.rb +1 -1
- data/lib/flare.rb +3 -1
- metadata +5 -2
data/lib/flare/metric_storage.rb
CHANGED
|
@@ -9,14 +9,17 @@ module Flare
|
|
|
9
9
|
class MetricStorage
|
|
10
10
|
def initialize
|
|
11
11
|
@storage = Concurrent::Map.new
|
|
12
|
+
@pid = $$
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def increment(key, duration_ms:, error: false)
|
|
16
|
+
detect_forking
|
|
15
17
|
counter = @storage.compute_if_absent(key) { MetricCounter.new }
|
|
16
18
|
counter.increment(duration_ms: duration_ms, error: error)
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
def add(key, count:, sum_ms:, error_count: 0)
|
|
22
|
+
detect_forking
|
|
20
23
|
counter = @storage.compute_if_absent(key) { MetricCounter.new }
|
|
21
24
|
counter.add(count: count, sum_ms: sum_ms, error_count: error_count)
|
|
22
25
|
end
|
|
@@ -24,6 +27,7 @@ module Flare
|
|
|
24
27
|
# Atomically retrieves and clears all metrics.
|
|
25
28
|
# Returns a frozen hash of MetricKey => counter data.
|
|
26
29
|
def drain
|
|
30
|
+
detect_forking
|
|
27
31
|
result = {}
|
|
28
32
|
@storage.keys.each do |key|
|
|
29
33
|
counter = @storage.delete(key)
|
|
@@ -33,15 +37,31 @@ module Flare
|
|
|
33
37
|
end
|
|
34
38
|
|
|
35
39
|
def size
|
|
40
|
+
detect_forking
|
|
36
41
|
@storage.size
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
def empty?
|
|
45
|
+
detect_forking
|
|
40
46
|
@storage.empty?
|
|
41
47
|
end
|
|
42
48
|
|
|
43
49
|
def [](key)
|
|
50
|
+
detect_forking
|
|
44
51
|
@storage[key]
|
|
45
52
|
end
|
|
53
|
+
|
|
54
|
+
def after_fork
|
|
55
|
+
return if @pid == $$
|
|
56
|
+
|
|
57
|
+
@pid = $$
|
|
58
|
+
@storage = Concurrent::Map.new
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def detect_forking
|
|
64
|
+
after_fork
|
|
65
|
+
end
|
|
46
66
|
end
|
|
47
67
|
end
|
|
@@ -7,6 +7,7 @@ require "stringio"
|
|
|
7
7
|
require "securerandom"
|
|
8
8
|
|
|
9
9
|
require_relative "client_headers"
|
|
10
|
+
require_relative "deadline"
|
|
10
11
|
|
|
11
12
|
module Flare
|
|
12
13
|
# Submits metrics to the Flare metrics service via HTTP.
|
|
@@ -45,6 +46,8 @@ module Flare
|
|
|
45
46
|
end
|
|
46
47
|
end
|
|
47
48
|
|
|
49
|
+
class DeadlineExceeded < StandardError; end
|
|
50
|
+
|
|
48
51
|
attr_reader :endpoint, :api_key, :backoff_policy
|
|
49
52
|
|
|
50
53
|
def initialize(endpoint:, api_key:, project: nil, environment: nil, backoff_policy: nil, open_timeout: nil, read_timeout: nil, write_timeout: nil)
|
|
@@ -60,23 +63,28 @@ module Flare
|
|
|
60
63
|
|
|
61
64
|
# Submit drained metrics to the server.
|
|
62
65
|
# Returns [success_count, error] where error may be nil on success.
|
|
63
|
-
def submit(drained)
|
|
66
|
+
def submit(drained, timeout: nil)
|
|
64
67
|
return [0, nil] if drained.empty?
|
|
65
68
|
|
|
69
|
+
deadline = Deadline.new(timeout)
|
|
70
|
+
|
|
66
71
|
request_id = SecureRandom.uuid
|
|
67
|
-
Flare.log "Submitting #{drained.size} metrics to #{@endpoint} (request_id=#{request_id})"
|
|
72
|
+
Flare.log "Submitting #{drained.size} metrics to #{@endpoint} (request_id=#{request_id})" if Flare.respond_to?(:log)
|
|
68
73
|
|
|
69
74
|
body = build_body(drained, request_id)
|
|
70
75
|
return [0, nil] if body.nil?
|
|
76
|
+
return [0, DeadlineExceeded.new("metric submission deadline exceeded")] if deadline.expired?
|
|
71
77
|
|
|
72
78
|
@backoff_policy.reset
|
|
73
|
-
response, error = retry_with_backoff(MAX_RETRIES
|
|
79
|
+
response, error = retry_with_backoff(MAX_RETRIES, deadline: deadline) do
|
|
80
|
+
post(body, request_id, timeout: deadline.remaining)
|
|
81
|
+
end
|
|
74
82
|
|
|
75
83
|
if error
|
|
76
|
-
Flare.log "Submission failed: #{error.message} (request_id=#{request_id})"
|
|
84
|
+
Flare.log "Submission failed: #{error.message} (request_id=#{request_id})" if Flare.respond_to?(:log)
|
|
77
85
|
[0, error]
|
|
78
86
|
else
|
|
79
|
-
Flare.log "Submission succeeded: #{response.code} (request_id=#{request_id})"
|
|
87
|
+
Flare.log "Submission succeeded: #{response.code} (request_id=#{request_id})" if Flare.respond_to?(:log)
|
|
80
88
|
[drained.size, nil]
|
|
81
89
|
end
|
|
82
90
|
end
|
|
@@ -111,12 +119,14 @@ module Flare
|
|
|
111
119
|
nil
|
|
112
120
|
end
|
|
113
121
|
|
|
114
|
-
def post(body, request_id)
|
|
122
|
+
def post(body, request_id, timeout: nil)
|
|
123
|
+
raise DeadlineExceeded, "metric submission deadline exceeded" if timeout == 0
|
|
124
|
+
|
|
115
125
|
http = Net::HTTP.new(@endpoint.host, @endpoint.port)
|
|
116
126
|
http.use_ssl = @endpoint.scheme == "https"
|
|
117
|
-
http.open_timeout = @open_timeout
|
|
118
|
-
http.read_timeout = @read_timeout
|
|
119
|
-
http.write_timeout = @write_timeout if http.respond_to?(:write_timeout=)
|
|
127
|
+
http.open_timeout = effective_timeout(@open_timeout, timeout)
|
|
128
|
+
http.read_timeout = effective_timeout(@read_timeout, timeout)
|
|
129
|
+
http.write_timeout = effective_timeout(@write_timeout, timeout) if http.respond_to?(:write_timeout=)
|
|
120
130
|
|
|
121
131
|
request_uri = @endpoint.request_uri
|
|
122
132
|
request = Net::HTTP::Post.new(request_uri == "" ? "/" : request_uri)
|
|
@@ -158,20 +168,27 @@ module Flare
|
|
|
158
168
|
)
|
|
159
169
|
end
|
|
160
170
|
|
|
161
|
-
def retry_with_backoff(max_attempts)
|
|
171
|
+
def retry_with_backoff(max_attempts, deadline:)
|
|
162
172
|
attempts_remaining = max_attempts
|
|
163
173
|
last_error = nil
|
|
164
174
|
|
|
165
175
|
while attempts_remaining > 0
|
|
166
176
|
begin
|
|
177
|
+
return [nil, DeadlineExceeded.new("metric submission deadline exceeded")] if deadline.expired?
|
|
178
|
+
|
|
167
179
|
result, should_retry = yield
|
|
180
|
+
return [nil, DeadlineExceeded.new("metric submission deadline exceeded")] if deadline.expired?
|
|
168
181
|
return [result, nil] unless should_retry
|
|
169
|
-
rescue SubmissionError, Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, Errno::ECONNRESET => e
|
|
182
|
+
rescue SubmissionError, Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Errno::ECONNREFUSED, Errno::ECONNRESET => e
|
|
170
183
|
last_error = e
|
|
171
184
|
attempts_remaining -= 1
|
|
172
185
|
|
|
173
186
|
if attempts_remaining > 0
|
|
174
187
|
sleep_time = @backoff_policy.next_interval / 1000.0
|
|
188
|
+
remaining = deadline.remaining
|
|
189
|
+
if remaining && sleep_time >= remaining
|
|
190
|
+
return [nil, DeadlineExceeded.new("metric submission deadline exceeded")]
|
|
191
|
+
end
|
|
175
192
|
sleep(sleep_time)
|
|
176
193
|
end
|
|
177
194
|
next
|
|
@@ -184,6 +201,12 @@ module Flare
|
|
|
184
201
|
[nil, last_error]
|
|
185
202
|
end
|
|
186
203
|
|
|
204
|
+
def effective_timeout(configured_timeout, remaining)
|
|
205
|
+
return configured_timeout unless remaining
|
|
206
|
+
|
|
207
|
+
[configured_timeout, remaining].min
|
|
208
|
+
end
|
|
209
|
+
|
|
187
210
|
def gzip(string)
|
|
188
211
|
io = StringIO.new
|
|
189
212
|
io.set_encoding("BINARY")
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
require "opentelemetry/sdk"
|
|
5
|
+
|
|
6
|
+
require_relative "deadline"
|
|
7
|
+
|
|
8
|
+
module Flare
|
|
9
|
+
# An asynchronous, bounded span processor that exports every ended recording
|
|
10
|
+
# span. OpenTelemetry's BatchSpanProcessor only accepts sampled spans, which
|
|
11
|
+
# excludes RECORD_ONLY spans needed by Flare's local development dashboard.
|
|
12
|
+
class RecordingBatchSpanProcessor
|
|
13
|
+
SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
14
|
+
FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE
|
|
15
|
+
TIMEOUT = OpenTelemetry::SDK::Trace::Export::TIMEOUT
|
|
16
|
+
|
|
17
|
+
def initialize(exporter, exporter_timeout: 30_000, schedule_delay: 5_000,
|
|
18
|
+
max_queue_size: 2_048, max_export_batch_size: 512, logger: nil)
|
|
19
|
+
raise ArgumentError if max_export_batch_size > max_queue_size
|
|
20
|
+
|
|
21
|
+
@exporter = exporter
|
|
22
|
+
@exporter_timeout = exporter_timeout / 1_000.0
|
|
23
|
+
@schedule_delay = schedule_delay / 1_000.0
|
|
24
|
+
@max_queue_size = max_queue_size
|
|
25
|
+
@max_export_batch_size = max_export_batch_size
|
|
26
|
+
@logger = logger || Logger.new($stderr, level: Logger::WARN)
|
|
27
|
+
@pid = $$
|
|
28
|
+
initialize_synchronization
|
|
29
|
+
start_worker
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def on_start(_span, _parent_context); end
|
|
33
|
+
|
|
34
|
+
def on_finish(span)
|
|
35
|
+
detect_forking
|
|
36
|
+
|
|
37
|
+
@mutex.synchronize do
|
|
38
|
+
overflow = @queue.length + 1 - @max_queue_size
|
|
39
|
+
@queue.shift(overflow) if overflow.positive?
|
|
40
|
+
@queue << span
|
|
41
|
+
@condition.signal if @queue.length >= @max_export_batch_size
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def force_flush(timeout: nil)
|
|
46
|
+
detect_forking
|
|
47
|
+
deadline = Deadline.new(timeout)
|
|
48
|
+
return TIMEOUT unless begin_flush(deadline)
|
|
49
|
+
|
|
50
|
+
snapshot = snapshot_for_flush
|
|
51
|
+
operation = start_flush_export(snapshot, deadline)
|
|
52
|
+
result = wait_for_flush_export(operation, deadline)
|
|
53
|
+
[@flush_prior_result, result].max
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
log_export_error(e)
|
|
56
|
+
FAILURE
|
|
57
|
+
ensure
|
|
58
|
+
finish_flush if @flush_owner == Thread.current
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def shutdown(timeout: nil)
|
|
62
|
+
detect_forking
|
|
63
|
+
deadline = Deadline.new(timeout)
|
|
64
|
+
|
|
65
|
+
worker = @mutex.synchronize do
|
|
66
|
+
@stopped = true
|
|
67
|
+
@condition.broadcast
|
|
68
|
+
@worker
|
|
69
|
+
end
|
|
70
|
+
worker&.join(deadline.remaining)
|
|
71
|
+
return TIMEOUT if worker&.alive? || deadline.expired?
|
|
72
|
+
|
|
73
|
+
result = force_flush(timeout: deadline.remaining)
|
|
74
|
+
return result unless result == SUCCESS
|
|
75
|
+
return TIMEOUT if deadline.expired?
|
|
76
|
+
|
|
77
|
+
exporter_result = @exporter.shutdown(timeout: deadline.remaining)
|
|
78
|
+
deadline.expired? ? TIMEOUT : exporter_result
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
log_export_error(e)
|
|
81
|
+
FAILURE
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def initialize_synchronization
|
|
87
|
+
@mutex = Mutex.new
|
|
88
|
+
@condition = ConditionVariable.new
|
|
89
|
+
@queue = []
|
|
90
|
+
@active_exports = 0
|
|
91
|
+
@export_completion_sequence = 0
|
|
92
|
+
@last_export_result = SUCCESS
|
|
93
|
+
@flush_prior_result = SUCCESS
|
|
94
|
+
@flush_owner = nil
|
|
95
|
+
@stopped = false
|
|
96
|
+
@worker = nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def worker_loop
|
|
100
|
+
loop do
|
|
101
|
+
batch = @mutex.synchronize do
|
|
102
|
+
while !@stopped && (@queue.empty? || @flush_owner || @active_exports.positive?)
|
|
103
|
+
@condition.wait(@mutex, @schedule_delay)
|
|
104
|
+
break if !@queue.empty? && !@flush_owner && @active_exports.zero?
|
|
105
|
+
end
|
|
106
|
+
return if @stopped
|
|
107
|
+
|
|
108
|
+
@active_exports += 1
|
|
109
|
+
@queue.shift(@max_export_batch_size)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
result = export_batch(batch, timeout: @exporter_timeout)
|
|
113
|
+
ensure
|
|
114
|
+
export_finished(result || FAILURE) if batch
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def begin_flush(deadline)
|
|
119
|
+
@mutex.synchronize do
|
|
120
|
+
initial_sequence = @export_completion_sequence
|
|
121
|
+
while @flush_owner && @flush_owner != Thread.current
|
|
122
|
+
return false if deadline.expired?
|
|
123
|
+
|
|
124
|
+
@condition.wait(@mutex, deadline.remaining)
|
|
125
|
+
end
|
|
126
|
+
@flush_owner = Thread.current
|
|
127
|
+
|
|
128
|
+
while @active_exports.positive?
|
|
129
|
+
return false if deadline.expired?
|
|
130
|
+
|
|
131
|
+
@condition.wait(@mutex, deadline.remaining)
|
|
132
|
+
end
|
|
133
|
+
@flush_prior_result = if @export_completion_sequence > initial_sequence
|
|
134
|
+
@last_export_result
|
|
135
|
+
else
|
|
136
|
+
SUCCESS
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
true
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def finish_flush
|
|
143
|
+
@mutex.synchronize do
|
|
144
|
+
@flush_owner = nil
|
|
145
|
+
@condition.broadcast
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def snapshot_for_flush
|
|
150
|
+
@mutex.synchronize { @queue.shift(@queue.length) }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def export_snapshot(snapshot, deadline)
|
|
154
|
+
until snapshot.empty?
|
|
155
|
+
return TIMEOUT if deadline.expired?
|
|
156
|
+
|
|
157
|
+
batch = snapshot.shift(@max_export_batch_size)
|
|
158
|
+
result = export_batch(batch, timeout: deadline.remaining)
|
|
159
|
+
return result unless result == SUCCESS
|
|
160
|
+
end
|
|
161
|
+
SUCCESS
|
|
162
|
+
ensure
|
|
163
|
+
@mutex.synchronize { @queue.unshift(*snapshot) } if snapshot&.any?
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def start_flush_export(snapshot, deadline)
|
|
167
|
+
operation = { done: false, result: nil }
|
|
168
|
+
@mutex.synchronize { @active_exports += 1 }
|
|
169
|
+
Thread.new do
|
|
170
|
+
result = export_snapshot(snapshot, deadline)
|
|
171
|
+
if result == SUCCESS && !deadline.expired?
|
|
172
|
+
result = @exporter.force_flush(timeout: deadline.remaining)
|
|
173
|
+
end
|
|
174
|
+
operation[:result] = deadline.expired? ? TIMEOUT : result
|
|
175
|
+
rescue StandardError => e
|
|
176
|
+
log_export_error(e)
|
|
177
|
+
operation[:result] = FAILURE
|
|
178
|
+
ensure
|
|
179
|
+
@mutex.synchronize do
|
|
180
|
+
operation[:done] = true
|
|
181
|
+
complete_export(operation[:result])
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
operation
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def wait_for_flush_export(operation, deadline)
|
|
188
|
+
@mutex.synchronize do
|
|
189
|
+
until operation[:done]
|
|
190
|
+
return TIMEOUT if deadline.expired?
|
|
191
|
+
|
|
192
|
+
@condition.wait(@mutex, deadline.remaining)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
operation[:result]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def export_batch(spans, timeout:)
|
|
199
|
+
span_data = spans.map { |span| span.respond_to?(:to_span_data) ? span.to_span_data : span }
|
|
200
|
+
@exporter.export(span_data, timeout: timeout)
|
|
201
|
+
rescue StandardError => e
|
|
202
|
+
log_export_error(e)
|
|
203
|
+
FAILURE
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def export_finished(result)
|
|
207
|
+
@mutex.synchronize do
|
|
208
|
+
complete_export(result)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def complete_export(result)
|
|
213
|
+
@active_exports -= 1
|
|
214
|
+
@export_completion_sequence += 1
|
|
215
|
+
@last_export_result = result || FAILURE
|
|
216
|
+
@condition.broadcast
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def detect_forking
|
|
220
|
+
return if @pid == $$
|
|
221
|
+
|
|
222
|
+
# Only the forking thread survives. Replacing synchronization objects
|
|
223
|
+
# avoids waiting on locks or in-flight state owned by vanished threads.
|
|
224
|
+
@pid = $$
|
|
225
|
+
initialize_synchronization
|
|
226
|
+
start_worker
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def start_worker
|
|
230
|
+
@worker = Thread.new { worker_loop }
|
|
231
|
+
@worker.name = "flare-recording-batch-span-processor"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def log_export_error(error)
|
|
235
|
+
@logger.warn("[Flare::RecordingBatchSpanProcessor] export failed: #{error.class}: #{error.message}")
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "sqlite3"
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
|
+
require_relative "deadline"
|
|
7
|
+
|
|
6
8
|
module Flare
|
|
7
9
|
class SQLiteExporter
|
|
8
10
|
SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
@@ -16,45 +18,61 @@ module Flare
|
|
|
16
18
|
@database_path = database_path
|
|
17
19
|
@mutex = Mutex.new
|
|
18
20
|
@setup = false
|
|
21
|
+
@pid = $$
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
# Maximum number of retry attempts when the database is busy.
|
|
22
25
|
# Mirrors ActiveRecord's retry strategy for SQLite.
|
|
23
26
|
MAX_RETRIES = 3
|
|
27
|
+
ExportDeadlineExceeded = Class.new(StandardError)
|
|
24
28
|
|
|
25
29
|
def export(span_datas, timeout: nil)
|
|
26
|
-
|
|
30
|
+
detect_forking
|
|
31
|
+
deadline = Deadline.new(timeout)
|
|
27
32
|
|
|
28
33
|
retries = 0
|
|
29
34
|
exported = 0
|
|
30
35
|
|
|
31
36
|
begin
|
|
32
|
-
@
|
|
37
|
+
setup_database(deadline) unless @setup
|
|
38
|
+
raise ExportDeadlineExceeded if deadline.expired?
|
|
39
|
+
raise ExportDeadlineExceeded unless lock_before_deadline(deadline)
|
|
40
|
+
|
|
41
|
+
begin
|
|
42
|
+
apply_busy_timeout(deadline)
|
|
33
43
|
connection.transaction do
|
|
34
44
|
span_datas.each do |span_data|
|
|
45
|
+
raise ExportDeadlineExceeded if deadline.expired?
|
|
35
46
|
next if should_ignore_span?(span_data)
|
|
36
47
|
|
|
37
48
|
create_span(span_data)
|
|
38
49
|
exported += 1
|
|
39
50
|
end
|
|
40
51
|
end
|
|
52
|
+
ensure
|
|
53
|
+
@mutex.unlock
|
|
41
54
|
end
|
|
55
|
+
rescue ExportDeadlineExceeded
|
|
56
|
+
return TIMEOUT
|
|
42
57
|
rescue ::SQLite3::BusyException
|
|
43
58
|
retries += 1
|
|
44
59
|
if retries <= MAX_RETRIES
|
|
45
|
-
|
|
60
|
+
sleep_time = 0.1 * retries
|
|
61
|
+
return TIMEOUT if deadline.remaining && sleep_time >= deadline.remaining
|
|
62
|
+
|
|
63
|
+
sleep(sleep_time)
|
|
46
64
|
retry
|
|
47
65
|
end
|
|
48
66
|
warn "[Flare] SQLite export error: database is busy after #{MAX_RETRIES} retries"
|
|
49
67
|
return FAILURE
|
|
50
68
|
end
|
|
51
69
|
|
|
52
|
-
Flare.log "Exported #{exported} spans to SQLite" if exported > 0
|
|
70
|
+
Flare.log "Exported #{exported} spans to SQLite" if exported > 0 && Flare.respond_to?(:log)
|
|
53
71
|
|
|
54
72
|
# Periodically prune old data
|
|
55
|
-
maybe_prune
|
|
73
|
+
maybe_prune unless deadline.expired?
|
|
56
74
|
|
|
57
|
-
SUCCESS
|
|
75
|
+
deadline.expired? ? TIMEOUT : SUCCESS
|
|
58
76
|
rescue => e
|
|
59
77
|
warn "[Flare] SQLite export error: #{e.message}"
|
|
60
78
|
FAILURE
|
|
@@ -70,6 +88,31 @@ module Flare
|
|
|
70
88
|
|
|
71
89
|
private
|
|
72
90
|
|
|
91
|
+
def detect_forking
|
|
92
|
+
return if @pid == $$
|
|
93
|
+
|
|
94
|
+
@pid = $$
|
|
95
|
+
@mutex = Mutex.new
|
|
96
|
+
close_connection
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def lock_before_deadline(deadline)
|
|
100
|
+
return @mutex.lock unless deadline.remaining
|
|
101
|
+
|
|
102
|
+
until @mutex.try_lock
|
|
103
|
+
return false if deadline.expired?
|
|
104
|
+
|
|
105
|
+
sleep([deadline.remaining, 0.001].min)
|
|
106
|
+
end
|
|
107
|
+
true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def apply_busy_timeout(deadline)
|
|
111
|
+
remaining = deadline.remaining
|
|
112
|
+
timeout_ms = remaining ? [(remaining * 1_000).floor, 1].max : 5_000
|
|
113
|
+
connection.busy_timeout = [timeout_ms, 5_000].min
|
|
114
|
+
end
|
|
115
|
+
|
|
73
116
|
def maybe_prune
|
|
74
117
|
return unless rand < PRUNE_PROBABILITY
|
|
75
118
|
|
|
@@ -162,14 +205,16 @@ module Flare
|
|
|
162
205
|
end
|
|
163
206
|
end
|
|
164
207
|
|
|
165
|
-
def setup_database
|
|
166
|
-
|
|
208
|
+
def setup_database(deadline)
|
|
209
|
+
raise ExportDeadlineExceeded unless lock_before_deadline(deadline)
|
|
210
|
+
|
|
211
|
+
begin
|
|
167
212
|
return if @setup
|
|
168
213
|
|
|
169
214
|
db = connection
|
|
170
|
-
configure_pragmas(db)
|
|
215
|
+
configure_pragmas(db, deadline)
|
|
171
216
|
|
|
172
|
-
db
|
|
217
|
+
execute_setup(db, deadline, <<~SQL)
|
|
173
218
|
CREATE TABLE IF NOT EXISTS flare_spans (
|
|
174
219
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
175
220
|
name TEXT NOT NULL,
|
|
@@ -187,23 +232,23 @@ module Flare
|
|
|
187
232
|
)
|
|
188
233
|
SQL
|
|
189
234
|
|
|
190
|
-
db
|
|
235
|
+
execute_setup(db, deadline, <<~SQL)
|
|
191
236
|
CREATE INDEX IF NOT EXISTS idx_spans_span_id ON flare_spans(span_id)
|
|
192
237
|
SQL
|
|
193
238
|
|
|
194
|
-
db
|
|
239
|
+
execute_setup(db, deadline, <<~SQL)
|
|
195
240
|
CREATE INDEX IF NOT EXISTS idx_spans_trace_id ON flare_spans(trace_id)
|
|
196
241
|
SQL
|
|
197
242
|
|
|
198
|
-
db
|
|
243
|
+
execute_setup(db, deadline, <<~SQL)
|
|
199
244
|
CREATE INDEX IF NOT EXISTS idx_spans_parent_span_id ON flare_spans(parent_span_id)
|
|
200
245
|
SQL
|
|
201
246
|
|
|
202
|
-
db
|
|
247
|
+
execute_setup(db, deadline, <<~SQL)
|
|
203
248
|
CREATE INDEX IF NOT EXISTS idx_spans_created_at ON flare_spans(created_at)
|
|
204
249
|
SQL
|
|
205
250
|
|
|
206
|
-
db
|
|
251
|
+
execute_setup(db, deadline, <<~SQL)
|
|
207
252
|
CREATE TABLE IF NOT EXISTS flare_events (
|
|
208
253
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
209
254
|
span_id INTEGER NOT NULL,
|
|
@@ -214,11 +259,11 @@ module Flare
|
|
|
214
259
|
)
|
|
215
260
|
SQL
|
|
216
261
|
|
|
217
|
-
db
|
|
262
|
+
execute_setup(db, deadline, <<~SQL)
|
|
218
263
|
CREATE INDEX IF NOT EXISTS idx_events_span_id ON flare_events(span_id)
|
|
219
264
|
SQL
|
|
220
265
|
|
|
221
|
-
db
|
|
266
|
+
execute_setup(db, deadline, <<~SQL)
|
|
222
267
|
CREATE TABLE IF NOT EXISTS flare_properties (
|
|
223
268
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
224
269
|
key TEXT NOT NULL,
|
|
@@ -231,27 +276,37 @@ module Flare
|
|
|
231
276
|
)
|
|
232
277
|
SQL
|
|
233
278
|
|
|
234
|
-
db
|
|
279
|
+
execute_setup(db, deadline, <<~SQL)
|
|
235
280
|
CREATE INDEX IF NOT EXISTS idx_properties_owner ON flare_properties(owner_type, owner_id)
|
|
236
281
|
SQL
|
|
237
282
|
|
|
238
|
-
db
|
|
283
|
+
execute_setup(db, deadline, <<~SQL)
|
|
239
284
|
CREATE INDEX IF NOT EXISTS idx_properties_key ON flare_properties(key)
|
|
240
285
|
SQL
|
|
241
286
|
|
|
242
287
|
close_connection # avoid inheriting connection across fork
|
|
243
288
|
@setup = true
|
|
289
|
+
ensure
|
|
290
|
+
@mutex.unlock
|
|
244
291
|
end
|
|
245
292
|
end
|
|
246
293
|
|
|
247
294
|
# Applies the same SQLite pragmas that ActiveRecord uses for good
|
|
248
295
|
# concurrency and performance with threaded/multi-process access.
|
|
249
|
-
def configure_pragmas(db)
|
|
250
|
-
db
|
|
251
|
-
db
|
|
252
|
-
db
|
|
253
|
-
db
|
|
254
|
-
db
|
|
296
|
+
def configure_pragmas(db, deadline)
|
|
297
|
+
execute_setup(db, deadline, "PRAGMA journal_mode=WAL")
|
|
298
|
+
execute_setup(db, deadline, "PRAGMA synchronous=NORMAL")
|
|
299
|
+
execute_setup(db, deadline, "PRAGMA mmap_size=134217728") # 128MB
|
|
300
|
+
execute_setup(db, deadline, "PRAGMA journal_size_limit=67108864") # 64MB
|
|
301
|
+
execute_setup(db, deadline, "PRAGMA cache_size=2000")
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def execute_setup(db, deadline, statement)
|
|
305
|
+
raise ExportDeadlineExceeded if deadline.expired?
|
|
306
|
+
|
|
307
|
+
apply_busy_timeout(deadline)
|
|
308
|
+
db.execute(statement)
|
|
309
|
+
raise ExportDeadlineExceeded if deadline.expired?
|
|
255
310
|
end
|
|
256
311
|
|
|
257
312
|
def connection_key
|