flare 0.4.0 → 1.0.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.

Potentially problematic release.


This version of flare might be problematic. Click here for more details.

Files changed (70) hide show
  1. data/.document +5 -0
  2. data/.gitignore +22 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +18 -0
  5. data/Rakefile +55 -0
  6. data/VERSION +1 -0
  7. data/flare.gemspec +66 -0
  8. data/lib/flare/active_record.rb +102 -0
  9. data/lib/flare/collection.rb +47 -0
  10. data/lib/flare/configuration.rb +64 -123
  11. data/lib/flare/index_builder.rb +24 -0
  12. data/lib/flare/session.rb +142 -0
  13. data/lib/flare/tasks.rb +18 -0
  14. data/lib/flare.rb +19 -537
  15. data/test/helper.rb +10 -0
  16. data/test/test_flare.rb +7 -0
  17. metadata +89 -242
  18. checksums.yaml +0 -7
  19. data/CHANGELOG.md +0 -3
  20. data/LICENSE.txt +0 -21
  21. data/README.md +0 -148
  22. data/app/controllers/flare/application_controller.rb +0 -30
  23. data/app/controllers/flare/jobs_controller.rb +0 -55
  24. data/app/controllers/flare/requests_controller.rb +0 -73
  25. data/app/controllers/flare/spans_controller.rb +0 -101
  26. data/app/helpers/flare/application_helper.rb +0 -168
  27. data/app/views/flare/jobs/index.html.erb +0 -69
  28. data/app/views/flare/jobs/show.html.erb +0 -323
  29. data/app/views/flare/requests/index.html.erb +0 -120
  30. data/app/views/flare/requests/show.html.erb +0 -498
  31. data/app/views/flare/spans/index.html.erb +0 -112
  32. data/app/views/flare/spans/show.html.erb +0 -184
  33. data/app/views/layouts/flare/application.html.erb +0 -126
  34. data/config/routes.rb +0 -20
  35. data/exe/flare +0 -9
  36. data/lib/flare/backoff_policy.rb +0 -73
  37. data/lib/flare/cli/doctor_command.rb +0 -129
  38. data/lib/flare/cli/output.rb +0 -45
  39. data/lib/flare/cli/setup_command.rb +0 -404
  40. data/lib/flare/cli/status_command.rb +0 -47
  41. data/lib/flare/cli.rb +0 -50
  42. data/lib/flare/client_headers.rb +0 -39
  43. data/lib/flare/deadline.rb +0 -27
  44. data/lib/flare/engine.rb +0 -45
  45. data/lib/flare/filtering_span_processor.rb +0 -429
  46. data/lib/flare/http_metrics_config.rb +0 -101
  47. data/lib/flare/http_transport.rb +0 -76
  48. data/lib/flare/lifecycle.rb +0 -39
  49. data/lib/flare/marker.rb +0 -106
  50. data/lib/flare/metric_counter.rb +0 -51
  51. data/lib/flare/metric_flusher.rb +0 -279
  52. data/lib/flare/metric_key.rb +0 -42
  53. data/lib/flare/metric_span_processor.rb +0 -470
  54. data/lib/flare/metric_storage.rb +0 -67
  55. data/lib/flare/metric_submitter.rb +0 -239
  56. data/lib/flare/recording_batch_span_processor.rb +0 -238
  57. data/lib/flare/rule_manager.rb +0 -141
  58. data/lib/flare/sampler.rb +0 -130
  59. data/lib/flare/source_location.rb +0 -113
  60. data/lib/flare/sqlite_exporter.rb +0 -334
  61. data/lib/flare/storage/sqlite.rb +0 -794
  62. data/lib/flare/storage.rb +0 -54
  63. data/lib/flare/trace_blob.rb +0 -116
  64. data/lib/flare/trace_exporter.rb +0 -182
  65. data/lib/flare/trace_health_reporter.rb +0 -74
  66. data/lib/flare/upload_url_pool.rb +0 -108
  67. data/lib/flare/version.rb +0 -5
  68. data/lib/flare/web_marker_subscriber.rb +0 -76
  69. data/public/flare-assets/flare.css +0 -1245
  70. data/public/flare-assets/images/flipper.png +0 -0
data/lib/flare/sampler.rb DELETED
@@ -1,130 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "concurrent/atomic/atomic_reference"
4
- require "opentelemetry/sdk"
5
-
6
- module Flare
7
- # Path 1 trace sampler. At span start, iterates active rules; returns
8
- # RECORD_AND_SAMPLE when one matches and the deterministic trace_id_ratio
9
- # falls under the rule's rate. Otherwise RECORD_ONLY -- the span still
10
- # records so MetricSpanProcessor sees it; the trace export decision for
11
- # web spans is deferred to Path 2 via Flare::Marker.
12
- #
13
- # Used as the `root` sampler inside an OTel ParentBased sampler so root
14
- # spans go through this logic but child spans inherit upstream decisions.
15
- # The `local_parent_not_sampled` slot of the ParentBased should point at
16
- # Flare::ALWAYS_RECORD_ONLY -- the default ALWAYS_OFF would drop children
17
- # of an unsampled local parent, making them NoOp spans the processors
18
- # never see.
19
- #
20
- # Rules are pushed in via update_rules from Flare::RuleManager; the swap
21
- # is atomic, and malformed rule entries are dropped with a counter so a
22
- # bad server payload can't crash the tracing path.
23
- class Sampler
24
- Decision = OpenTelemetry::SDK::Trace::Samplers::Decision
25
- Result = OpenTelemetry::SDK::Trace::Samplers::Result
26
-
27
- RULE_ID_ATTRIBUTE = "flare.rule_id"
28
-
29
- Rule = Struct.new(:id, :match_attributes, :rate, keyword_init: true)
30
-
31
- attr_reader :dropped_rule_count
32
-
33
- def initialize
34
- @rules_ref = Concurrent::AtomicReference.new([].freeze)
35
- @dropped_rule_count = Concurrent::AtomicFixnum.new(0)
36
- end
37
-
38
- # new_rules: an array of rule hashes from GET /api/rules, e.g.
39
- # [{ "id" => 1, "match_attributes" => {...}, "rate" => 0.5 }, ...]
40
- # Entries that don't validate are skipped (counted in dropped_rule_count).
41
- def update_rules(new_rules)
42
- validated = (new_rules || []).filter_map { |r| validate(r) }
43
- @dropped_rule_count.increment((new_rules || []).length - validated.length)
44
- @rules_ref.set(validated.freeze)
45
- end
46
-
47
- def rules
48
- @rules_ref.get
49
- end
50
-
51
- def should_sample?(trace_id:, parent_context:, links:, name:, kind:, attributes:)
52
- tracestate = tracestate_from(parent_context)
53
-
54
- rules.each do |rule|
55
- next unless matches?(rule, attributes)
56
- next unless trace_id_ratio(trace_id) < rule.rate
57
-
58
- merged = (attributes || {}).merge(RULE_ID_ATTRIBUTE => rule.id)
59
- return Result.new(decision: Decision::RECORD_AND_SAMPLE, attributes: merged, tracestate: tracestate)
60
- end
61
-
62
- Result.new(decision: Decision::RECORD_ONLY, tracestate: tracestate)
63
- end
64
-
65
- def description
66
- "Flare::Sampler"
67
- end
68
-
69
- # Cross-language formula: last 8 bytes of the 16-byte raw trace_id as
70
- # uint64-big-endian, divided by 2^64. Same in every Flare SDK so the
71
- # server can reproduce the decision if it ever needs to.
72
- def trace_id_ratio(trace_id)
73
- bytes = trace_id.is_a?(String) ? trace_id.bytes : Array(trace_id)
74
- tail = bytes.last(8)
75
- n = 0
76
- tail.each { |b| n = (n << 8) | b }
77
- n.to_f / (1 << 64)
78
- end
79
-
80
- private
81
-
82
- def tracestate_from(parent_context)
83
- OpenTelemetry::Trace.current_span(parent_context).context.tracestate ||
84
- OpenTelemetry::Trace::Tracestate::DEFAULT
85
- end
86
-
87
- def matches?(rule, attributes)
88
- return false if attributes.nil?
89
- rule.match_attributes.all? { |k, v| attributes[k] == v }
90
- end
91
-
92
- def validate(raw)
93
- return nil unless raw.is_a?(Hash)
94
-
95
- id = raw["id"] || raw[:id]
96
- match = raw["match_attributes"] || raw[:match_attributes]
97
- rate = raw["rate"] || raw[:rate]
98
-
99
- return nil if id.nil?
100
- return nil unless match.is_a?(Hash) && match.any?
101
- return nil unless match.all? { |k, v| k.is_a?(String) && v.is_a?(String) && !v.empty? }
102
- return nil unless rate.is_a?(Numeric) && rate > 0.0 && rate <= 1.0
103
-
104
- Rule.new(id: id, match_attributes: match, rate: rate.to_f)
105
- rescue StandardError
106
- nil
107
- end
108
- end
109
-
110
- # Tiny sampler whose should_sample? returns RECORD_ONLY for every span.
111
- # Slot this into the ParentBased local_parent_not_sampled position so
112
- # children of an unsampled local parent stay recording (the default
113
- # ALWAYS_OFF turns them into NoOp spans no processor ever sees).
114
- class AlwaysRecordOnly
115
- Decision = OpenTelemetry::SDK::Trace::Samplers::Decision
116
- Result = OpenTelemetry::SDK::Trace::Samplers::Result
117
-
118
- def should_sample?(parent_context: nil, **)
119
- tracestate = OpenTelemetry::Trace.current_span(parent_context).context.tracestate ||
120
- OpenTelemetry::Trace::Tracestate::DEFAULT
121
- Result.new(decision: Decision::RECORD_ONLY, tracestate: tracestate)
122
- end
123
-
124
- def description
125
- "Flare::AlwaysRecordOnly"
126
- end
127
- end
128
-
129
- ALWAYS_RECORD_ONLY = AlwaysRecordOnly.new
130
- end
@@ -1,113 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Flare
4
- # Utility for finding the source location (file, line, method) of app code
5
- # that triggered a database query or other instrumented operation.
6
- module SourceLocation
7
- # How many app code lines to capture for the backtrace
8
- MAX_TRACE_LINES = 8
9
-
10
- # Patterns to filter out from backtraces (gems, framework code)
11
- IGNORE_PATTERNS = [
12
- /\/gems\//,
13
- /\/ruby\//,
14
- /\/rubygems\//,
15
- /lib\/active_record/,
16
- /lib\/active_support/,
17
- /lib\/action_/,
18
- /opentelemetry/,
19
- /flare/,
20
- /<internal:/,
21
- /\/bin\//,
22
- ].freeze
23
-
24
- module_function
25
-
26
- # Find the first app source location from the current backtrace
27
- # Returns a hash with :filepath, :lineno, :function or nil
28
- def find
29
- backtrace = caller(2, 50)
30
- return nil unless backtrace
31
-
32
- # Find the first line that's app code (not gems/framework)
33
- app_line = backtrace.find { |line| app_code?(line) }
34
- return nil unless app_line
35
-
36
- parse_backtrace_line(app_line)
37
- end
38
-
39
- # Find multiple app source locations from the current backtrace
40
- # Returns an array of cleaned backtrace lines (up to MAX_TRACE_LINES)
41
- def find_trace
42
- backtrace = caller(2, 100)
43
- return [] unless backtrace
44
-
45
- # Filter to only app code lines
46
- app_lines = backtrace.select { |line| app_code?(line) }
47
- return [] if app_lines.empty?
48
-
49
- # Clean and format each line, limit to MAX_TRACE_LINES
50
- app_lines.first(MAX_TRACE_LINES).map { |line| clean_backtrace_line(line) }
51
- end
52
-
53
- # Add source location attributes to a hash (for span attributes)
54
- def add_to_attributes(attrs)
55
- location = find
56
- return attrs unless location
57
-
58
- attrs["code.filepath"] = location[:filepath]
59
- attrs["code.lineno"] = location[:lineno]
60
- attrs["code.function"] = location[:function] if location[:function]
61
-
62
- # Add full trace as a single string attribute
63
- trace = find_trace
64
- attrs["code.stacktrace"] = trace.join("\n") if trace.any?
65
-
66
- attrs
67
- end
68
-
69
- def app_code?(line)
70
- # Must contain /app/ (Rails convention) and not match ignore patterns
71
- return false unless line.include?("/app/")
72
-
73
- IGNORE_PATTERNS.none? { |pattern| line.match?(pattern) }
74
- end
75
-
76
- def parse_backtrace_line(line)
77
- # Parse: /path/to/file.rb:123:in `method_name'
78
- if line =~ /\A(.+):(\d+):in [`'](.+)'\z/
79
- {
80
- filepath: clean_path($1),
81
- lineno: $2.to_i,
82
- function: $3
83
- }
84
- elsif line =~ /\A(.+):(\d+)\z/
85
- {
86
- filepath: clean_path($1),
87
- lineno: $2.to_i,
88
- function: nil
89
- }
90
- end
91
- end
92
-
93
- def clean_backtrace_line(line)
94
- # Parse and reformat: "app/models/user.rb:42:in `find_by_email'"
95
- if line =~ /\A(.+):(\d+):in [`'](.+)'\z/
96
- "#{clean_path($1)}:#{$2} in `#{$3}'"
97
- elsif line =~ /\A(.+):(\d+)\z/
98
- "#{clean_path($1)}:#{$2}"
99
- else
100
- clean_path(line)
101
- end
102
- end
103
-
104
- def clean_path(path)
105
- # Remove Rails.root prefix if present
106
- if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
107
- path.sub(/\A#{Regexp.escape(Rails.root.to_s)}\//, "")
108
- else
109
- path
110
- end
111
- end
112
- end
113
- end
@@ -1,334 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "sqlite3"
4
- require "json"
5
-
6
- require_relative "deadline"
7
-
8
- module Flare
9
- class SQLiteExporter
10
- SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
11
- FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE
12
- TIMEOUT = OpenTelemetry::SDK::Trace::Export::TIMEOUT
13
-
14
- # Prune roughly every 100 exports (1% chance per export)
15
- PRUNE_PROBABILITY = 0.01
16
-
17
- def initialize(database_path)
18
- @database_path = database_path
19
- @mutex = Mutex.new
20
- @setup = false
21
- @pid = $$
22
- end
23
-
24
- # Maximum number of retry attempts when the database is busy.
25
- # Mirrors ActiveRecord's retry strategy for SQLite.
26
- MAX_RETRIES = 3
27
- ExportDeadlineExceeded = Class.new(StandardError)
28
-
29
- def export(span_datas, timeout: nil)
30
- detect_forking
31
- deadline = Deadline.new(timeout)
32
-
33
- retries = 0
34
- exported = 0
35
-
36
- begin
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)
43
- connection.transaction do
44
- span_datas.each do |span_data|
45
- raise ExportDeadlineExceeded if deadline.expired?
46
- next if should_ignore_span?(span_data)
47
-
48
- create_span(span_data)
49
- exported += 1
50
- end
51
- end
52
- ensure
53
- @mutex.unlock
54
- end
55
- rescue ExportDeadlineExceeded
56
- return TIMEOUT
57
- rescue ::SQLite3::BusyException
58
- retries += 1
59
- if retries <= MAX_RETRIES
60
- sleep_time = 0.1 * retries
61
- return TIMEOUT if deadline.remaining && sleep_time >= deadline.remaining
62
-
63
- sleep(sleep_time)
64
- retry
65
- end
66
- warn "[Flare] SQLite export error: database is busy after #{MAX_RETRIES} retries"
67
- return FAILURE
68
- end
69
-
70
- Flare.log "Exported #{exported} spans to SQLite" if exported > 0 && Flare.respond_to?(:log)
71
-
72
- # Periodically prune old data
73
- maybe_prune unless deadline.expired?
74
-
75
- deadline.expired? ? TIMEOUT : SUCCESS
76
- rescue => e
77
- warn "[Flare] SQLite export error: #{e.message}"
78
- FAILURE
79
- end
80
-
81
- def force_flush(timeout: nil)
82
- SUCCESS
83
- end
84
-
85
- def shutdown(timeout: nil)
86
- SUCCESS
87
- end
88
-
89
- private
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
-
116
- def maybe_prune
117
- return unless rand < PRUNE_PROBABILITY
118
-
119
- Flare.storage.prune(
120
- retention_hours: Flare.configuration.retention_hours,
121
- max_spans: Flare.configuration.max_spans
122
- )
123
- rescue => e
124
- warn "[Flare] Prune error: #{e.message}"
125
- end
126
-
127
- def should_ignore_span?(span_data)
128
- span_data.name&.start_with?("Flare::")
129
- end
130
-
131
- def create_span(span_data)
132
- now = Time.now.iso8601(6)
133
-
134
- sql = <<~SQL
135
- INSERT INTO flare_spans (name, kind, span_id, trace_id, parent_span_id, start_timestamp, end_timestamp, total_recorded_links, total_recorded_events, total_recorded_properties, created_at, updated_at)
136
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
137
- SQL
138
-
139
- values = [
140
- span_data.name,
141
- span_data.kind.to_s,
142
- span_data.hex_span_id,
143
- span_data.hex_trace_id,
144
- span_data.hex_parent_span_id,
145
- span_data.start_timestamp,
146
- span_data.end_timestamp,
147
- span_data.total_recorded_links,
148
- span_data.total_recorded_events,
149
- span_data.total_recorded_attributes,
150
- now,
151
- now
152
- ]
153
-
154
- connection.execute(sql, values)
155
- span_record_id = connection.last_insert_row_id
156
-
157
- span_data.events&.each do |span_event|
158
- create_event(span_record_id, span_event)
159
- end
160
-
161
- create_properties("Flare::Span", span_record_id, span_data.attributes)
162
- end
163
-
164
- def create_event(span_record_id, span_event)
165
- now = Time.now.iso8601(6)
166
- timestamp = span_event.timestamp ? Time.at(span_event.timestamp / 1_000_000_000.0).iso8601(6) : now
167
-
168
- sql = <<~SQL
169
- INSERT INTO flare_events (span_id, name, created_at, updated_at)
170
- VALUES (?, ?, ?, ?)
171
- SQL
172
-
173
- connection.execute(sql, [span_record_id, span_event.name, timestamp, now])
174
- event_record_id = connection.last_insert_row_id
175
- create_properties("Flare::Event", event_record_id, span_event.attributes)
176
- end
177
-
178
- def create_properties(owner_type, owner_id, attributes)
179
- return unless attributes
180
-
181
- now = Time.now.iso8601(6)
182
-
183
- sql = <<~SQL
184
- INSERT INTO flare_properties (key, value, value_type, owner_type, owner_id, created_at, updated_at)
185
- VALUES (?, ?, ?, ?, ?, ?, ?)
186
- SQL
187
-
188
- attributes.each do |key, value|
189
- next if value.nil?
190
-
191
- value_type = determine_value_type(value)
192
- serialized_value = JSON.generate(value)
193
- connection.execute(sql, [key, serialized_value, value_type, owner_type, owner_id, now, now])
194
- end
195
- end
196
-
197
- def determine_value_type(value)
198
- case value
199
- when String then 0 # string
200
- when Integer then 1 # integer
201
- when Float then 2 # float
202
- when TrueClass, FalseClass then 3 # boolean
203
- when Array then 4 # array
204
- else 0 # default to string
205
- end
206
- end
207
-
208
- def setup_database(deadline)
209
- raise ExportDeadlineExceeded unless lock_before_deadline(deadline)
210
-
211
- begin
212
- return if @setup
213
-
214
- db = connection
215
- configure_pragmas(db, deadline)
216
-
217
- execute_setup(db, deadline, <<~SQL)
218
- CREATE TABLE IF NOT EXISTS flare_spans (
219
- id INTEGER PRIMARY KEY AUTOINCREMENT,
220
- name TEXT NOT NULL,
221
- kind TEXT NOT NULL,
222
- span_id TEXT NOT NULL,
223
- trace_id TEXT NOT NULL,
224
- parent_span_id TEXT,
225
- start_timestamp INTEGER NOT NULL,
226
- end_timestamp INTEGER NOT NULL,
227
- total_recorded_properties INTEGER NOT NULL DEFAULT 0,
228
- total_recorded_events INTEGER NOT NULL DEFAULT 0,
229
- total_recorded_links INTEGER NOT NULL DEFAULT 0,
230
- created_at TEXT NOT NULL,
231
- updated_at TEXT NOT NULL
232
- )
233
- SQL
234
-
235
- execute_setup(db, deadline, <<~SQL)
236
- CREATE INDEX IF NOT EXISTS idx_spans_span_id ON flare_spans(span_id)
237
- SQL
238
-
239
- execute_setup(db, deadline, <<~SQL)
240
- CREATE INDEX IF NOT EXISTS idx_spans_trace_id ON flare_spans(trace_id)
241
- SQL
242
-
243
- execute_setup(db, deadline, <<~SQL)
244
- CREATE INDEX IF NOT EXISTS idx_spans_parent_span_id ON flare_spans(parent_span_id)
245
- SQL
246
-
247
- execute_setup(db, deadline, <<~SQL)
248
- CREATE INDEX IF NOT EXISTS idx_spans_created_at ON flare_spans(created_at)
249
- SQL
250
-
251
- execute_setup(db, deadline, <<~SQL)
252
- CREATE TABLE IF NOT EXISTS flare_events (
253
- id INTEGER PRIMARY KEY AUTOINCREMENT,
254
- span_id INTEGER NOT NULL,
255
- name TEXT NOT NULL,
256
- created_at TEXT NOT NULL,
257
- updated_at TEXT NOT NULL,
258
- FOREIGN KEY (span_id) REFERENCES flare_spans(id)
259
- )
260
- SQL
261
-
262
- execute_setup(db, deadline, <<~SQL)
263
- CREATE INDEX IF NOT EXISTS idx_events_span_id ON flare_events(span_id)
264
- SQL
265
-
266
- execute_setup(db, deadline, <<~SQL)
267
- CREATE TABLE IF NOT EXISTS flare_properties (
268
- id INTEGER PRIMARY KEY AUTOINCREMENT,
269
- key TEXT NOT NULL,
270
- value TEXT,
271
- value_type INTEGER NOT NULL DEFAULT 0,
272
- owner_type TEXT NOT NULL,
273
- owner_id INTEGER NOT NULL,
274
- created_at TEXT NOT NULL,
275
- updated_at TEXT NOT NULL
276
- )
277
- SQL
278
-
279
- execute_setup(db, deadline, <<~SQL)
280
- CREATE INDEX IF NOT EXISTS idx_properties_owner ON flare_properties(owner_type, owner_id)
281
- SQL
282
-
283
- execute_setup(db, deadline, <<~SQL)
284
- CREATE INDEX IF NOT EXISTS idx_properties_key ON flare_properties(key)
285
- SQL
286
-
287
- close_connection # avoid inheriting connection across fork
288
- @setup = true
289
- ensure
290
- @mutex.unlock
291
- end
292
- end
293
-
294
- # Applies the same SQLite pragmas that ActiveRecord uses for good
295
- # concurrency and performance with threaded/multi-process access.
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?
310
- end
311
-
312
- def connection_key
313
- :"flare_sqlite_db_#{@database_path.hash}"
314
- end
315
-
316
- def connection
317
- Thread.current[connection_key] ||= begin
318
- dir = File.dirname(@database_path)
319
- FileUtils.mkdir_p(dir) unless File.directory?(dir)
320
- db = ::SQLite3::Database.new(@database_path, results_as_hash: true)
321
- db.busy_timeout = 5000
322
- db
323
- end
324
- end
325
-
326
- def close_connection
327
- key = connection_key
328
- if db = Thread.current[key]
329
- db.close rescue nil
330
- Thread.current[key] = nil
331
- end
332
- end
333
- end
334
- end