kimera 0.1.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.
Files changed (171) hide show
  1. checksums.yaml +7 -0
  2. data/.kimera.yml.example +59 -0
  3. data/CHANGELOG.md +36 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +528 -0
  6. data/exe/kimera +22 -0
  7. data/lib/kimera/audit/operator_audit.rb +71 -0
  8. data/lib/kimera/cli/argv.rb +15 -0
  9. data/lib/kimera/cli/baseline.rb +87 -0
  10. data/lib/kimera/cli/changed.rb +27 -0
  11. data/lib/kimera/cli/ci.rb +27 -0
  12. data/lib/kimera/cli/completion.rb +37 -0
  13. data/lib/kimera/cli/doctor.rb +115 -0
  14. data/lib/kimera/cli/flag.rb +55 -0
  15. data/lib/kimera/cli/help_text.rb +45 -0
  16. data/lib/kimera/cli/init.rb +105 -0
  17. data/lib/kimera/cli/mutant.rb +82 -0
  18. data/lib/kimera/cli/report_file.rb +20 -0
  19. data/lib/kimera/cli/run/arguments.rb +58 -0
  20. data/lib/kimera/cli/run/cycle.rb +78 -0
  21. data/lib/kimera/cli/run/digest.rb +49 -0
  22. data/lib/kimera/cli/run/emission.rb +45 -0
  23. data/lib/kimera/cli/run/gate.rb +43 -0
  24. data/lib/kimera/cli/run/options.rb +85 -0
  25. data/lib/kimera/cli/run/pass.rb +100 -0
  26. data/lib/kimera/cli/run/sources.rb +56 -0
  27. data/lib/kimera/cli/run.rb +52 -0
  28. data/lib/kimera/cli/skill.rb +23 -0
  29. data/lib/kimera/cli/suggestion.rb +36 -0
  30. data/lib/kimera/cli/survivors/panel.rb +81 -0
  31. data/lib/kimera/cli/survivors.rb +89 -0
  32. data/lib/kimera/cli/synthesize.rb +66 -0
  33. data/lib/kimera/cli/workflows.rb +8 -0
  34. data/lib/kimera/cli.rb +132 -0
  35. data/lib/kimera/error.rb +3 -0
  36. data/lib/kimera/execution/baseline_failure.rb +47 -0
  37. data/lib/kimera/execution/baseline_pass.rb +123 -0
  38. data/lib/kimera/execution/boot.rb +50 -0
  39. data/lib/kimera/execution/callback.rb +33 -0
  40. data/lib/kimera/execution/child_process.rb +48 -0
  41. data/lib/kimera/execution/composite_isolation.rb +24 -0
  42. data/lib/kimera/execution/harness.rb +150 -0
  43. data/lib/kimera/execution/isolated.rb +64 -0
  44. data/lib/kimera/execution/isolated_child.rb +15 -0
  45. data/lib/kimera/execution/isolated_child_command.rb +44 -0
  46. data/lib/kimera/execution/isolated_child_minitest.rb +34 -0
  47. data/lib/kimera/execution/isolated_plan.rb +54 -0
  48. data/lib/kimera/execution/isolated_scheduling.rb +54 -0
  49. data/lib/kimera/execution/isolated_test_selection.rb +22 -0
  50. data/lib/kimera/execution/isolated_verdict.rb +72 -0
  51. data/lib/kimera/execution/isolated_watchdog.rb +59 -0
  52. data/lib/kimera/execution/isolation.rb +17 -0
  53. data/lib/kimera/execution/null_progress.rb +17 -0
  54. data/lib/kimera/execution/overlay_guard_modules.rb +95 -0
  55. data/lib/kimera/execution/overlay_guards.rb +56 -0
  56. data/lib/kimera/execution/parallel_test_databases.rb +64 -0
  57. data/lib/kimera/execution/pool_driver.rb +101 -0
  58. data/lib/kimera/execution/priority.rb +23 -0
  59. data/lib/kimera/execution/reload.rb +104 -0
  60. data/lib/kimera/execution/rig.rb +41 -0
  61. data/lib/kimera/execution/schedule.rb +105 -0
  62. data/lib/kimera/execution/schemata.rb +88 -0
  63. data/lib/kimera/execution/shift/attempt.rb +34 -0
  64. data/lib/kimera/execution/shift/coverage_channel.rb +43 -0
  65. data/lib/kimera/execution/shift/killer_memory.rb +20 -0
  66. data/lib/kimera/execution/shift/leak_guard.rb +34 -0
  67. data/lib/kimera/execution/shift.rb +137 -0
  68. data/lib/kimera/execution/sweep.rb +30 -0
  69. data/lib/kimera/execution/transaction_isolation.rb +32 -0
  70. data/lib/kimera/execution/trial.rb +16 -0
  71. data/lib/kimera/execution/verdicts.rb +78 -0
  72. data/lib/kimera/execution/worker_pool/fleet.rb +104 -0
  73. data/lib/kimera/execution/worker_pool/stillborn_guard.rb +40 -0
  74. data/lib/kimera/execution/worker_pool/worker.rb +32 -0
  75. data/lib/kimera/execution/worker_pool.rb +101 -0
  76. data/lib/kimera/frameworks/adapter.rb +82 -0
  77. data/lib/kimera/frameworks/minitest_adapter.rb +102 -0
  78. data/lib/kimera/frameworks/rspec_adapter.rb +123 -0
  79. data/lib/kimera/frameworks/rspec_group_index.rb +45 -0
  80. data/lib/kimera/incremental/git_diff.rb +98 -0
  81. data/lib/kimera/incremental/selection.rb +27 -0
  82. data/lib/kimera/incremental/session.rb +109 -0
  83. data/lib/kimera/memoization.rb +104 -0
  84. data/lib/kimera/operators/argument_drop.rb +19 -0
  85. data/lib/kimera/operators/arithmetic.rb +12 -0
  86. data/lib/kimera/operators/audit.rb +3 -0
  87. data/lib/kimera/operators/base.rb +40 -0
  88. data/lib/kimera/operators/binary_swap.rb +23 -0
  89. data/lib/kimera/operators/boolean.rb +26 -0
  90. data/lib/kimera/operators/chain_link_deletion.rb +25 -0
  91. data/lib/kimera/operators/collection_literal.rb +18 -0
  92. data/lib/kimera/operators/comparison.rb +13 -0
  93. data/lib/kimera/operators/conditional.rb +36 -0
  94. data/lib/kimera/operators/default_argument.rb +17 -0
  95. data/lib/kimera/operators/element_drop.rb +31 -0
  96. data/lib/kimera/operators/index_fetch.rb +15 -0
  97. data/lib/kimera/operators/kernel_coercion.rb +17 -0
  98. data/lib/kimera/operators/method_unwrap.rb +20 -0
  99. data/lib/kimera/operators/negation.rb +15 -0
  100. data/lib/kimera/operators/node_swap.rb +20 -0
  101. data/lib/kimera/operators/numeric_literal.rb +39 -0
  102. data/lib/kimera/operators/op_assign.rb +18 -0
  103. data/lib/kimera/operators/plugins.rb +27 -0
  104. data/lib/kimera/operators/rails_association.rb +24 -0
  105. data/lib/kimera/operators/rails_callback.rb +49 -0
  106. data/lib/kimera/operators/rails_declaration.rb +16 -0
  107. data/lib/kimera/operators/rails_permit.rb +34 -0
  108. data/lib/kimera/operators/rails_validation.rb +17 -0
  109. data/lib/kimera/operators/range.rb +15 -0
  110. data/lib/kimera/operators/regexp_literal.rb +16 -0
  111. data/lib/kimera/operators/respond_to_guard.rb +24 -0
  112. data/lib/kimera/operators/return_value.rb +36 -0
  113. data/lib/kimera/operators/safe_navigation.rb +15 -0
  114. data/lib/kimera/operators/selector_swap.rb +24 -0
  115. data/lib/kimera/operators/statement_deletion.rb +26 -0
  116. data/lib/kimera/operators/string_literal.rb +22 -0
  117. data/lib/kimera/operators/symbol_literal.rb +26 -0
  118. data/lib/kimera/operators/vocabulary.rb +82 -0
  119. data/lib/kimera/operators.rb +141 -0
  120. data/lib/kimera/plugins.rb +3 -0
  121. data/lib/kimera/registry/builder.rb +59 -0
  122. data/lib/kimera/registry/mutation_point.rb +146 -0
  123. data/lib/kimera/registry/numbering.rb +15 -0
  124. data/lib/kimera/registry/registry.rb +120 -0
  125. data/lib/kimera/registry/source_file.rb +71 -0
  126. data/lib/kimera/registry/tally.rb +11 -0
  127. data/lib/kimera/registry/walking.rb +55 -0
  128. data/lib/kimera/report/actions.rb +38 -0
  129. data/lib/kimera/report/coloring.rb +25 -0
  130. data/lib/kimera/report/formats.rb +78 -0
  131. data/lib/kimera/report/live.rb +56 -0
  132. data/lib/kimera/report/log.rb +46 -0
  133. data/lib/kimera/report/progress.rb +55 -0
  134. data/lib/kimera/report/screen.rb +44 -0
  135. data/lib/kimera/report/sections.rb +50 -0
  136. data/lib/kimera/report/tally.rb +84 -0
  137. data/lib/kimera/report/text.rb +104 -0
  138. data/lib/kimera/results/result.rb +72 -0
  139. data/lib/kimera/results/run_report.rb +114 -0
  140. data/lib/kimera/rewrite/ast_walk.rb +20 -0
  141. data/lib/kimera/rewrite/directive.rb +61 -0
  142. data/lib/kimera/rewrite/directive_extra_handlers.rb +52 -0
  143. data/lib/kimera/rewrite/directive_handlers.rb +138 -0
  144. data/lib/kimera/rewrite/directive_kwarg_handlers.rb +44 -0
  145. data/lib/kimera/rewrite/numbered_params.rb +50 -0
  146. data/lib/kimera/runtime.rb +51 -0
  147. data/lib/kimera/scope/config.rb +90 -0
  148. data/lib/kimera/scope/file_set.rb +51 -0
  149. data/lib/kimera/scope/ignore_list.rb +55 -0
  150. data/lib/kimera/self_protection.rb +56 -0
  151. data/lib/kimera/support/duration.rb +13 -0
  152. data/lib/kimera/support/errors.rb +8 -0
  153. data/lib/kimera/support/operator_protocol.rb +10 -0
  154. data/lib/kimera/support/syntax.rb +68 -0
  155. data/lib/kimera/support/syntax_types.rb +20 -0
  156. data/lib/kimera/support/unparse.rb +15 -0
  157. data/lib/kimera/support/version.rb +5 -0
  158. data/lib/kimera/support/warnings.rb +15 -0
  159. data/lib/kimera/synthesis/file_weave.rb +78 -0
  160. data/lib/kimera/synthesis/guard_weaver.rb +77 -0
  161. data/lib/kimera/synthesis/guardrail_dispatch.rb +68 -0
  162. data/lib/kimera/synthesis/guardrail_interpolation.rb +59 -0
  163. data/lib/kimera/synthesis/guardrail_value_objects.rb +46 -0
  164. data/lib/kimera/synthesis/overlay.rb +52 -0
  165. data/lib/kimera/synthesis/overlay_splice.rb +101 -0
  166. data/lib/kimera/synthesis/project.rb +58 -0
  167. data/lib/kimera/synthesis/source_map.rb +42 -0
  168. data/lib/kimera/version.rb +3 -0
  169. data/lib/kimera.rb +13 -0
  170. data/skills/kimera/SKILL.md +118 -0
  171. metadata +259 -0
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kimera::Execution::Shift::CoverageChannel
4
+ def initialize(adapter)
5
+ @adapter = adapter
6
+ end
7
+
8
+ def serve(request, response)
9
+ ledger = Kimera::Runtime.start!
10
+ drain(request, response, ledger)
11
+ emit(response, t: "done")
12
+ ensure
13
+ stop(ledger)
14
+ end
15
+
16
+ private
17
+
18
+ def drain(request, response, ledger)
19
+ while (line = request.gets)
20
+ step(response, JSON.parse(line)["id"], ledger)
21
+ emit(response, t: "ready")
22
+ end
23
+ end
24
+
25
+ def step(response, testid, ledger)
26
+ Kimera::Runtime.active = nil
27
+ emit(response, **message(testid, ledger, @adapter.run([testid])))
28
+ end
29
+
30
+ def message(testid, ledger, outcome)
31
+ { t: "result", id: testid, passed: outcome.passed? }
32
+ .merge(touched: Kimera::Runtime.drain!(ledger), failure: outcome.failures[testid])
33
+ end
34
+
35
+ def stop(ledger)
36
+ Kimera::Runtime.stop!(ledger)
37
+ Kimera::Runtime.active = nil
38
+ end
39
+
40
+ def emit(io, **msg)
41
+ io.puts(JSON.generate(msg))
42
+ end
43
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kimera::Execution::Shift::KillerMemory
4
+ LIMIT = 16
5
+
6
+ def initialize
7
+ @recent = []
8
+ end
9
+
10
+ def order(tests)
11
+ likely = @recent & tests
12
+ likely + (tests - likely)
13
+ end
14
+
15
+ def remember(testid)
16
+ @recent.delete(testid)
17
+ @recent.unshift(testid)
18
+ @recent.pop while @recent.size > LIMIT
19
+ end
20
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kimera::Execution::Shift::LeakGuard
4
+ def initialize(every, &evaluator)
5
+ @every = every
6
+ @evaluator = evaluator
7
+ end
8
+
9
+ def check(io, history, index)
10
+ return unless due?(index)
11
+ return if history.empty?
12
+ recheck(io, history.last)
13
+ end
14
+
15
+ private
16
+
17
+ def due?(index)
18
+ return false unless @every&.positive?
19
+ ((index + 1) % @every).zero?
20
+ end
21
+
22
+ def recheck(io, id)
23
+ return if @evaluator.call(id).killed?
24
+ emit(io, t: "leak", id: id, detail: detail(id))
25
+ end
26
+
27
+ def detail(id)
28
+ "mutant #{id} killed earlier but survived re-run (state leakage suspected)"
29
+ end
30
+
31
+ def emit(io, **msg)
32
+ io.puts(JSON.generate(msg))
33
+ end
34
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "timeout"
5
+ require_relative "../results/result"
6
+ require_relative "../runtime"
7
+ require_relative "isolation"
8
+
9
+ class Kimera::Execution::Shift
10
+ Subject = Struct.new(:id, :file)
11
+ Channel =
12
+ Data.define(:requests, :responses) do
13
+ def each_request
14
+ while (line = requests.gets)
15
+ yield(line, responses)
16
+ end
17
+ end
18
+ end
19
+
20
+ def initialize(adapter:, registry:, coverage: nil, **options)
21
+ @adapter = adapter
22
+ @registry = registry
23
+ @coverage = coverage
24
+ @options = options
25
+ end
26
+
27
+ def serve(requests, responses)
28
+ listen(Channel.new(requests, responses))
29
+ emit(responses, t: "done")
30
+ ensure
31
+ Kimera::Runtime.active = nil
32
+ end
33
+
34
+ def coverage(requests, responses) = channel.serve(requests, responses)
35
+
36
+ def run(ids, io)
37
+ history = []
38
+ ids.each_with_index { |id, index| process(id, io, history, index) }
39
+ emit(io, t: "done")
40
+ ensure
41
+ Kimera::Runtime.active = nil
42
+ end
43
+
44
+ def evaluate(id)
45
+ safely(Subject.new(id, @registry.index[id]&.file))
46
+ ensure
47
+ Kimera::Runtime.active = nil
48
+ isolation.reset!
49
+ end
50
+
51
+ private
52
+
53
+ def isolation = @_isolation ||= @options.fetch(:isolation) { Kimera::Execution::Isolation.new }
54
+
55
+ def timeout = @options.fetch(:soft_timeout, 5.0)
56
+
57
+ def attempt
58
+ kind = self.class
59
+ @_attempt ||= kind::Attempt.new(
60
+ adapter: @adapter, isolation: isolation, killers: kind::KillerMemory.new, timeout: timeout
61
+ )
62
+ end
63
+
64
+ def leaks = @_leaks ||= self.class::LeakGuard.new(@options.fetch(:leak_every, 10)) { |id| evaluate(id) }
65
+
66
+ def channel = @_channel ||= self.class::CoverageChannel.new(@adapter)
67
+
68
+ def listen(connection)
69
+ history = []
70
+ index = 0
71
+ connection.each_request { |line, responses| index = step(line, responses, history, index) }
72
+ end
73
+
74
+ def step(line, responses, history, index)
75
+ process(JSON.parse(line)["id"], responses, history, index)
76
+ emit(responses, t: "ready")
77
+ index + 1
78
+ end
79
+
80
+ def safely(subject)
81
+ tests = available(subject.id)
82
+ return result(subject, :no_coverage) if tests.empty?
83
+ outcome(subject, tests)
84
+ rescue StandardError, ScriptError => error
85
+ error(subject, error)
86
+ end
87
+
88
+ def error(subject, error)
89
+ return result(subject, :timeout, timeout) if error.is_a?(Timeout::Error)
90
+ result(subject, :error, detail: "#{error.class}: #{error.message}")
91
+ end
92
+
93
+ def outcome(subject, tests)
94
+ started = monotonic
95
+ outcome = attempt.run(subject.id, tests)
96
+ result(subject, classify(outcome), monotonic - started, failing(outcome), tests)
97
+ end
98
+
99
+ def process(id, io, history, index)
100
+ result = evaluate(id)
101
+ emit(io, **result.message)
102
+ history << id if result.killed?
103
+ leaks.check(io, history, index)
104
+ end
105
+
106
+ def classify(outcome)
107
+ !outcome || outcome.passed? ? :survived : :killed
108
+ end
109
+
110
+ def failing(outcome) = outcome&.failed_ids
111
+
112
+ def available(id)
113
+ return @adapter.test_ids unless @coverage
114
+ @coverage.fetch(id) { @coverage.fetch(id.to_s, []) }
115
+ end
116
+
117
+ def result(subject, status, duration = nil, fails = nil, cover = nil, detail: nil)
118
+ Kimera::MutantResult.new(
119
+ mutant_id: subject.id, status: status, file: subject.file,
120
+ duration: duration, failing_tests: fails,
121
+ covering_tests: cover, detail: detail
122
+ )
123
+ end
124
+
125
+ def emit(io, **message)
126
+ io.puts(JSON.generate(message))
127
+ end
128
+
129
+ def monotonic
130
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
131
+ end
132
+ end
133
+
134
+ require_relative "shift/attempt"
135
+ require_relative "shift/coverage_channel"
136
+ require_relative "shift/killer_memory"
137
+ require_relative "shift/leak_guard"
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ module Execution
5
+ end
6
+ end
7
+
8
+ class Kimera::Execution::Sweep
9
+ def initialize(ids, lock)
10
+ @ids = ids
11
+ @lock = lock
12
+ end
13
+
14
+ def total = @ids.size
15
+
16
+ def results = @_results ||= []
17
+
18
+ def pop = queue.pop(timeout: 0)
19
+
20
+ def each(&) = @ids.each(&)
21
+
22
+ def record(result)
23
+ @lock.synchronize { results << result }
24
+ results.size
25
+ end
26
+
27
+ private
28
+
29
+ def queue = @_queue ||= Queue.new.tap { |pending| @ids.each { |id| pending << id } }
30
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "isolation"
4
+
5
+ class Kimera::Execution::TransactionIsolation < Kimera::Execution::Isolation; end
6
+ class Kimera::Execution::TransactionIsolation::Rollback < StandardError; end
7
+
8
+ class Kimera::Execution::TransactionIsolation
9
+ def initialize(owner)
10
+ super()
11
+ @owner = owner
12
+ end
13
+
14
+ def around
15
+ result = nil
16
+ rollback { result = yield }
17
+ result
18
+ end
19
+
20
+ private
21
+
22
+ def rollback(&)
23
+ @owner.transaction(requires_new: true) { unwind(&) }
24
+ rescue Rollback
25
+ nil
26
+ end
27
+
28
+ def unwind
29
+ yield
30
+ raise(Rollback)
31
+ end
32
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ module Execution
5
+ end
6
+ end
7
+
8
+ Kimera::Execution::Trial =
9
+ Struct.new(:id, :mirror, :synth, :file) do
10
+ def stage(root)
11
+ source = File.read(File.join(root, file), encoding: Encoding::UTF_8)
12
+ target = File.join(mirror, file)
13
+ File.write(target, synth.bake(file, source, id))
14
+ [source, target]
15
+ end
16
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../results/result"
4
+ require_relative "../self_protection"
5
+
6
+ module Kimera
7
+ module Execution
8
+ end
9
+ end
10
+
11
+ class Kimera::Execution::Verdicts
12
+ def initialize(registry)
13
+ @registry = registry
14
+ end
15
+
16
+ def safe?(id)
17
+ point(id).safe?
18
+ end
19
+
20
+ def reloadable?(id)
21
+ !point(id).body?
22
+ end
23
+
24
+ def reclassify!(results)
25
+ results.each { |id, result| reclassify(results, id, result) }
26
+ end
27
+
28
+ def parse(message)
29
+ id = message["id"]
30
+ Kimera::MutantResult.new(
31
+ mutant_id: id, status: message["status"].to_sym, file: path(id),
32
+ duration: message["ms"], failing_tests: message["fails"], covering_tests: message["cover"]
33
+ )
34
+ end
35
+
36
+ def timeout(id, duration)
37
+ Kimera::MutantResult.new(
38
+ mutant_id: id, status: :timeout, file: path(id),
39
+ duration: duration, detail: "hard watchdog timeout"
40
+ )
41
+ end
42
+
43
+ def unjudged(id, detail)
44
+ Kimera::MutantResult.new(mutant_id: id, status: :harness_error, file: path(id), detail: detail)
45
+ end
46
+
47
+ def quarantine(id)
48
+ Kimera::MutantResult.new(mutant_id: id, status: :isolated_only, file: path(id), detail: point(id).unsafe_reason)
49
+ end
50
+
51
+ def point(id)
52
+ @registry.index[id] || Kimera::MutationPoint::NONE
53
+ end
54
+
55
+ def path(id)
56
+ point(id).file
57
+ end
58
+
59
+ private
60
+
61
+ def reclassify(results, id, result)
62
+ return unless suspect?(result)
63
+ results[id] = result.isolated(detail(result.status))
64
+ end
65
+
66
+ def suspect?(result)
67
+ Kimera::Status::SELF_SUSPECT.include?(result.status) && harness?(result)
68
+ end
69
+
70
+ def harness?(result)
71
+ Kimera::SelfProtection.critical?(result.file)
72
+ end
73
+
74
+ def detail(status)
75
+ "#{status == :survived ? "unfalsifiable warm survivor" : "self-crash"} in the in-process runner " \
76
+ "(warm #{status}); evaluate with --isolated"
77
+ end
78
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stillborn_guard"
4
+
5
+ class Kimera::Execution::WorkerPool::Fleet
6
+ include Kimera::Execution::ChildProcess
7
+
8
+ def initialize(queue:, spawner:, jobs:, pool:)
9
+ @source = queue
10
+ @spawner = spawner
11
+ @jobs = jobs
12
+ @pool = pool
13
+ end
14
+
15
+ def bootstrap
16
+ [@jobs, queue.size].min.times { spawn }
17
+ end
18
+
19
+ def each(&)
20
+ workers.each_value(&)
21
+ end
22
+
23
+ def pairs(&)
24
+ workers.to_a.each(&)
25
+ end
26
+
27
+ def fetch(pipe)
28
+ workers.fetch(pipe)
29
+ end
30
+
31
+ def keys
32
+ workers.keys
33
+ end
34
+
35
+ def any?
36
+ workers.any?
37
+ end
38
+
39
+ def take
40
+ until queue.empty?
41
+ id = queue.shift
42
+ return id unless done[id]
43
+ end
44
+ end
45
+
46
+ def done!(id)
47
+ done[id] = true
48
+ end
49
+
50
+ def progress!
51
+ guard.progress!
52
+ end
53
+
54
+ def remove(pipe, reason: :crash)
55
+ worker = workers.delete(pipe)
56
+ slots.push(worker.slot)
57
+ close(worker)
58
+ charge(worker.inflight, reason, reap(worker.pid))
59
+ refill
60
+ end
61
+
62
+ private
63
+
64
+ def queue = @_queue ||= @source.dup
65
+
66
+ def done = @_done ||= {}
67
+
68
+ def workers = @_workers ||= {}
69
+
70
+ def slots = @_slots ||= (0...@jobs).to_a
71
+
72
+ def guard = @_guard ||= Kimera::Execution::WorkerPool::StillbornGuard.new(@jobs)
73
+
74
+ def spawn
75
+ slot = slots.shift
76
+ pid, request, pipe = @spawner.call(slot)
77
+ workers[pipe] = Kimera::Execution::WorkerPool::Worker.new(pid, request, pipe, nil, nil, slot)
78
+ end
79
+
80
+ def close(worker)
81
+ shut(worker.response)
82
+ shut(worker.request)
83
+ end
84
+
85
+ def charge(id, reason, status)
86
+ return unless id
87
+ guard.track(status) if reason == :crash
88
+ done!(id)
89
+ @pool.lost(id, reason)
90
+ end
91
+
92
+ def refill
93
+ missing = @jobs - workers.size
94
+ missing.times do
95
+ break unless queue.any? { |id| !done[id] }
96
+ replace
97
+ end
98
+ end
99
+
100
+ def replace
101
+ spawn
102
+ @pool.assign(workers.values.last)
103
+ end
104
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../error"
4
+
5
+ class Kimera::Execution::WorkerPool::StillbornGuard
6
+ def initialize(jobs)
7
+ @jobs = jobs
8
+ @stillborn = 0
9
+ @progressed = false
10
+ end
11
+
12
+ def progress!
13
+ @progressed = true
14
+ end
15
+
16
+ def track(status)
17
+ return if @progressed
18
+ @stillborn += 1
19
+ abort!(status) if @stillborn > @jobs
20
+ end
21
+
22
+ private
23
+
24
+ def abort!(status)
25
+ raise(Kimera::Error, message(status))
26
+ end
27
+
28
+ def message(status)
29
+ "#{@stillborn} warm workers died before reporting a result " \
30
+ "(last exit: #{describe(status)}). The app raises during " \
31
+ "fork or the suite aborts on load; re-run with --jobs 1 to see " \
32
+ "the error."
33
+ end
34
+
35
+ def describe(status)
36
+ [status].compact.map do |current|
37
+ current.signaled? ? "signal #{current.termsig}" : "status #{current.exitstatus}"
38
+ end.fetch(0, "unknown")
39
+ end
40
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ Kimera::Execution::WorkerPool::Worker =
4
+ Struct.new(:pid, :request, :response, :inflight, :deadline, :slot) do
5
+ def idle!
6
+ self.inflight = nil
7
+ end
8
+
9
+ def busy!(id)
10
+ self.inflight = id
11
+ end
12
+
13
+ def renew(limit)
14
+ self.deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + limit
15
+ end
16
+
17
+ def expired?(now)
18
+ deadline && !(deadline - now).positive?
19
+ end
20
+
21
+ def retire
22
+ self.inflight = nil
23
+ self.deadline = nil
24
+ request
25
+ end
26
+
27
+ def offer(id)
28
+ request.puts(JSON.generate(id: id))
29
+ rescue Errno::EPIPE, IOError
30
+ nil
31
+ end
32
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../error"
5
+ require_relative "child_process"
6
+
7
+ class Kimera::Execution::WorkerPool
8
+ include Kimera::Execution::ChildProcess
9
+
10
+ Context = Data.define(:queue, :spawner, :resolve, :lost, :options)
11
+
12
+ POLL_INTERVAL = 0.2
13
+
14
+ class << self
15
+ def new(queue:, spawner:, resolve:, lost:, **options)
16
+ super(Context.new(queue, spawner, resolve, lost, options))
17
+ end
18
+ end
19
+
20
+ def initialize(context)
21
+ @context = context
22
+ end
23
+
24
+ def run
25
+ bootstrap
26
+ poll while fleet.any?
27
+ end
28
+
29
+ def assign(worker)
30
+ id = fleet.take
31
+ return close(worker) unless id
32
+ worker.busy!(id)
33
+ worker.renew(limit)
34
+ worker.offer(id)
35
+ end
36
+
37
+ def lost(id, reason) = context.lost.call(id, reason)
38
+
39
+ private
40
+
41
+ attr_reader :context
42
+
43
+ def limit = context.options.fetch(:hard_timeout)
44
+
45
+ def fleet
46
+ @_fleet ||= Kimera::Execution::WorkerPool::Fleet.new(
47
+ queue: context.queue, spawner: context.spawner, jobs: context.options.fetch(:jobs), pool: self
48
+ )
49
+ end
50
+
51
+ def bootstrap
52
+ fleet.bootstrap
53
+ fleet.each { |worker| assign(worker) }
54
+ end
55
+
56
+ def poll
57
+ ready, = IO.select(fleet.keys, nil, nil, POLL_INTERVAL)
58
+ Array(ready).each { |pipe| service(pipe) }
59
+ watch
60
+ end
61
+
62
+ def service(pipe)
63
+ worker = fleet.fetch(pipe)
64
+ line = pipe.gets
65
+ return fleet.remove(pipe) unless line
66
+ message = parse(line)
67
+ dispatch(worker, message) if message
68
+ end
69
+
70
+ def dispatch(worker, message)
71
+ case message["t"]
72
+ when "result" then finish(worker, message)
73
+ when "leak" then context.resolve.call(message)
74
+ when "ready" then assign(worker)
75
+ end
76
+ end
77
+
78
+ def finish(worker, message)
79
+ fleet.done!(message["id"])
80
+ fleet.progress!
81
+ context.resolve.call(message)
82
+ worker.idle!
83
+ worker.renew(limit)
84
+ end
85
+
86
+ def close(worker) = shut(worker.retire)
87
+
88
+ def watch
89
+ current = now
90
+ fleet.pairs { |pipe, worker| expire(pipe, worker, current) }
91
+ end
92
+
93
+ def expire(pipe, worker, current)
94
+ return unless worker.expired?(current)
95
+ kill(worker.pid)
96
+ fleet.remove(pipe, reason: :timeout)
97
+ end
98
+ end
99
+
100
+ require_relative "worker_pool/fleet"
101
+ require_relative "worker_pool/worker"