inferno_core 1.4.4 → 1.4.6
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/inferno/apps/cli/templates/%library_name%.gemspec.tt +1 -1
- data/lib/inferno/apps/cli/templates/.gitignore +1 -1
- data/lib/inferno/apps/cli/templates/execution_scripts/%library_name%_script_expected.json.tt +0 -8
- data/lib/inferno/apps/cli/templates/spec/%library_name%/patient_group_spec.rb.tt +3 -3
- data/lib/inferno/apps/web/router.rb +1 -1
- data/lib/inferno/config/boot/db.rb +0 -5
- data/lib/inferno/config/boot/suites.rb +35 -0
- data/lib/inferno/dsl/input_output_handling.rb +101 -5
- data/lib/inferno/dsl/must_support_assessment.rb +12 -24
- data/lib/inferno/dsl/must_support_metadata_extractor.rb +19 -1
- data/lib/inferno/entities/input.rb +57 -0
- data/lib/inferno/entities/test_kit.rb +4 -2
- data/lib/inferno/jobs/execute_test_run.rb +3 -0
- data/lib/inferno/jobs/resume_test_run.rb +4 -0
- data/lib/inferno/public/bundle.js +13 -13
- data/lib/inferno/test_runner.rb +15 -8
- data/lib/inferno/version.rb +1 -1
- metadata +2 -3
- data/lib/inferno/utils/db_concurrency_check.rb +0 -166
data/lib/inferno/test_runner.rb
CHANGED
|
@@ -89,8 +89,12 @@ module Inferno
|
|
|
89
89
|
if result == 'wait'
|
|
90
90
|
# The waiting status and the wait result must become visible to
|
|
91
91
|
# readers atomically, so that pollers never see the run waiting
|
|
92
|
-
# without the waiting test's result being present.
|
|
93
|
-
|
|
92
|
+
# without the waiting test's result being present. Immediate mode
|
|
93
|
+
# takes the sqlite write lock up front () mark_as_waiting reads before
|
|
94
|
+
# it writes) and is used to avoid a SQLITE_BUSY error without waiting
|
|
95
|
+
# out busy_timeout when it tries to upgrade to a write lock that is
|
|
96
|
+
# already taken.
|
|
97
|
+
Inferno::Application['db.connection'].transaction(mode: :immediate) do
|
|
94
98
|
test_runs_repo.mark_as_waiting(test_run.id, test_instance.identifier, test_instance.wait_timeout)
|
|
95
99
|
persist_result(result_params)
|
|
96
100
|
end
|
|
@@ -122,9 +126,13 @@ module Inferno
|
|
|
122
126
|
end
|
|
123
127
|
|
|
124
128
|
def check_inputs(test, _test_instance, inputs)
|
|
129
|
+
available_inputs = test.available_inputs(test_session.suite_options)
|
|
130
|
+
input_values = inputs.transform_keys { |input_identifier| test.config.input_name(input_identifier).to_s }
|
|
131
|
+
|
|
125
132
|
inputs.each do |key, value|
|
|
126
133
|
optional = test.config.input_optional?(key)
|
|
127
|
-
|
|
134
|
+
input = available_inputs[test.config.input_name(key).to_sym]
|
|
135
|
+
if value.nil? && !optional && input.enabled?(input_values, available_inputs)
|
|
128
136
|
raise Exceptions::SkipException,
|
|
129
137
|
"Input '#{test.config.input_name(key)}' is nil, skipping test."
|
|
130
138
|
end
|
|
@@ -276,11 +284,10 @@ module Inferno
|
|
|
276
284
|
# into a single lock acquisition instead of one per insert, cutting
|
|
277
285
|
# down on sqlite write-lock contention with concurrent readers/writers.
|
|
278
286
|
#
|
|
279
|
-
#
|
|
280
|
-
#
|
|
281
|
-
#
|
|
282
|
-
|
|
283
|
-
result = Inferno::Application['db.connection'].transaction do
|
|
287
|
+
# Immediate mode takes the write lock up front so that any reads inside
|
|
288
|
+
# the transaction can't leave it stuck unable to upgrade to a write lock
|
|
289
|
+
# (see the wait transaction in #run_test).
|
|
290
|
+
result = Inferno::Application['db.connection'].transaction(mode: :immediate) do
|
|
284
291
|
results_repo.create(
|
|
285
292
|
params.merge(test_run_id: test_run.id, test_session_id: test_session.id)
|
|
286
293
|
)
|
data/lib/inferno/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: inferno_core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.4.
|
|
4
|
+
version: 1.4.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stephen MacVicar
|
|
@@ -10,7 +10,7 @@ authors:
|
|
|
10
10
|
autorequire:
|
|
11
11
|
bindir: bin
|
|
12
12
|
cert_chain: []
|
|
13
|
-
date: 2026-
|
|
13
|
+
date: 2026-10-13 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: activesupport
|
|
@@ -759,7 +759,6 @@ files:
|
|
|
759
759
|
- lib/inferno/route_storage.rb
|
|
760
760
|
- lib/inferno/spec_support.rb
|
|
761
761
|
- lib/inferno/test_runner.rb
|
|
762
|
-
- lib/inferno/utils/db_concurrency_check.rb
|
|
763
762
|
- lib/inferno/utils/execution_script_runner.rb
|
|
764
763
|
- lib/inferno/utils/ig_downloader.rb
|
|
765
764
|
- lib/inferno/utils/markdown_formatter.rb
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
require 'sequel'
|
|
2
|
-
require 'sqlite3'
|
|
3
|
-
require 'tmpdir'
|
|
4
|
-
require 'securerandom'
|
|
5
|
-
|
|
6
|
-
module Inferno
|
|
7
|
-
module Utils
|
|
8
|
-
# Verifies that the sqlite WAL/busy_timeout/transaction-wrapping in
|
|
9
|
-
# lib/inferno/config/boot/db.rb and TestRunner#persist_result actually
|
|
10
|
-
# prevent SQLITE_BUSY under concurrent test-run writes and status-polling
|
|
11
|
-
# reads against a shared sqlite file.
|
|
12
|
-
#
|
|
13
|
-
# This is deliberately NOT an rspec example. It's a real-time concurrency
|
|
14
|
-
# measurement, and a hardcoded load level that reliably reproduces (or
|
|
15
|
-
# avoids) SQLITE_BUSY on one machine does not transfer to others: a load
|
|
16
|
-
# level tuned to fail reliably without the fix and pass reliably with it,
|
|
17
|
-
# verified repeatedly on one machine, failed consistently in GitHub
|
|
18
|
-
# Actions and on another local machine. Rather than assert against a
|
|
19
|
-
# fixed load level, this calibrates to whatever machine it runs on: it
|
|
20
|
-
# escalates the write volume until it can demonstrate the *unprotected*
|
|
21
|
-
# configuration actually hits SQLITE_BUSY, then checks whether the
|
|
22
|
-
# *protected* configuration avoids it under that same, machine-calibrated
|
|
23
|
-
# load.
|
|
24
|
-
#
|
|
25
|
-
# Run manually with `bundle exec rake db:check_concurrency` after changing
|
|
26
|
-
# the persistence/locking behavior in the files above. It isn't part of
|
|
27
|
-
# the default test suite because it's a real-time measurement, not a
|
|
28
|
-
# deterministic assertion, and shouldn't gate CI.
|
|
29
|
-
module DbConcurrencyCheck
|
|
30
|
-
WRITER_THREADS = 5
|
|
31
|
-
READER_THREADS = 3
|
|
32
|
-
READER_POLL_INTERVAL = 0.2
|
|
33
|
-
MAX_CONNECTIONS = 15
|
|
34
|
-
MAX_CASCADES_PER_WRITER = 5000
|
|
35
|
-
|
|
36
|
-
module_function
|
|
37
|
-
|
|
38
|
-
def run
|
|
39
|
-
cascades = 10
|
|
40
|
-
baseline_errors = []
|
|
41
|
-
|
|
42
|
-
loop do
|
|
43
|
-
baseline_errors = measure(protected: false, cascades_per_writer: cascades)
|
|
44
|
-
break if baseline_errors.any? || cascades > MAX_CASCADES_PER_WRITER
|
|
45
|
-
|
|
46
|
-
cascades *= 2
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
if baseline_errors.empty?
|
|
50
|
-
puts "INCONCLUSIVE: could not reproduce SQLITE_BUSY even at #{cascades} cascades/writer on this machine."
|
|
51
|
-
return true
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
puts "Reproduced #{baseline_errors.size} SQLITE_BUSY error(s) without WAL/busy_timeout/transaction-" \
|
|
55
|
-
"wrapping at #{cascades} cascades/writer (#{WRITER_THREADS} writers, #{READER_THREADS} readers " \
|
|
56
|
-
"polling every #{READER_POLL_INTERVAL}s)."
|
|
57
|
-
|
|
58
|
-
protected_errors = measure(protected: true, cascades_per_writer: cascades)
|
|
59
|
-
|
|
60
|
-
if protected_errors.empty?
|
|
61
|
-
puts 'PASS: the current fix avoided SQLITE_BUSY under the same load that reproduced it above.'
|
|
62
|
-
true
|
|
63
|
-
else
|
|
64
|
-
puts "FAIL: the current fix still hit #{protected_errors.size} SQLITE_BUSY error(s) under the same load:"
|
|
65
|
-
puts protected_errors.first.message
|
|
66
|
-
false
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def measure(protected:, cascades_per_writer:)
|
|
71
|
-
Dir.mktmpdir do |dir|
|
|
72
|
-
db = connect(File.join(dir, 'contention.db'), protected:)
|
|
73
|
-
result_class, request_class, header_class = build_schema_and_models(db)
|
|
74
|
-
errors = []
|
|
75
|
-
mutex = Mutex.new
|
|
76
|
-
stop_reading = false
|
|
77
|
-
|
|
78
|
-
writers = Array.new(WRITER_THREADS) do
|
|
79
|
-
Thread.new do
|
|
80
|
-
cascades_per_writer.times do
|
|
81
|
-
persist_cascade(db, result_class, request_class, header_class, protected:)
|
|
82
|
-
rescue StandardError => e
|
|
83
|
-
mutex.synchronize { errors << e }
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
readers = Array.new(READER_THREADS) do
|
|
89
|
-
Thread.new { poll_until(-> { stop_reading }, result_class, request_class, errors, mutex) }
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
writers.each(&:join)
|
|
93
|
-
stop_reading = true
|
|
94
|
-
readers.each(&:join)
|
|
95
|
-
db.disconnect
|
|
96
|
-
|
|
97
|
-
errors.select { |e| e.is_a?(Sequel::DatabaseError) && e.message =~ /database is locked|SQLITE_BUSY/i }
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def connect(db_path, protected:)
|
|
102
|
-
connect_sqls =
|
|
103
|
-
if protected
|
|
104
|
-
["PRAGMA busy_timeout = #{ENV.fetch('DB_BUSY_TIMEOUT_MS', '15000')}", 'PRAGMA journal_mode = WAL']
|
|
105
|
-
else
|
|
106
|
-
['PRAGMA busy_timeout = 0']
|
|
107
|
-
end
|
|
108
|
-
Sequel.connect(adapter: 'sqlite', database: db_path, max_connections: MAX_CONNECTIONS, connect_sqls:)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def persist_cascade(db, result_class, request_class, header_class, protected:)
|
|
112
|
-
if protected
|
|
113
|
-
db.transaction { create_cascade(result_class, request_class, header_class) }
|
|
114
|
-
else
|
|
115
|
-
create_cascade(result_class, request_class, header_class)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def poll_until(stop, result_class, request_class, errors, mutex)
|
|
120
|
-
until stop.call
|
|
121
|
-
result_class.order(Sequel.desc(:id)).limit(1).all
|
|
122
|
-
request_class.order(Sequel.desc(:id)).limit(1).all
|
|
123
|
-
sleep(READER_POLL_INTERVAL)
|
|
124
|
-
end
|
|
125
|
-
rescue StandardError => e
|
|
126
|
-
mutex.synchronize { errors << e }
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def build_schema_and_models(db)
|
|
130
|
-
db.create_table(:results) do
|
|
131
|
-
primary_key :id
|
|
132
|
-
column :test_id, String
|
|
133
|
-
column :result, String
|
|
134
|
-
end
|
|
135
|
-
db.create_table(:requests) do
|
|
136
|
-
primary_key :id
|
|
137
|
-
foreign_key :result_id, :results
|
|
138
|
-
column :verb, String
|
|
139
|
-
end
|
|
140
|
-
db.create_table(:headers) do
|
|
141
|
-
primary_key :id
|
|
142
|
-
foreign_key :request_id, :requests
|
|
143
|
-
column :name, String
|
|
144
|
-
column :value, String
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
[
|
|
148
|
-
Class.new(Sequel::Model(db[:results])),
|
|
149
|
-
Class.new(Sequel::Model(db[:requests])),
|
|
150
|
-
Class.new(Sequel::Model(db[:headers]))
|
|
151
|
-
]
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def create_cascade(result_class, request_class, header_class)
|
|
155
|
-
result = result_class.create(test_id: SecureRandom.uuid, result: 'pass')
|
|
156
|
-
3.times do
|
|
157
|
-
request = request_class.create(result_id: result.id, verb: 'GET')
|
|
158
|
-
2.times do |i|
|
|
159
|
-
header_class.create(request_id: request.id, name: "Header-#{i}", value: SecureRandom.hex(8))
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
result
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
end
|