minitest-distributed 0.2.13 → 0.3.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: 1605e583ce4427da1909789380c1ac20ffe629a5f1ddf96452f93e45d50188f2
4
- data.tar.gz: 052ba1210bee26b878052bb28207b800a67682f023e6fdcc1e03bebaea758376
3
+ metadata.gz: 5d4be59a24cfaf9122de65b8b5919dda05f39bcecd0a6bf9998c44f085edabe5
4
+ data.tar.gz: 4672adf9de55fde54385954316756688b27c0f37ba6318b73b1288d89af5b6d9
5
5
  SHA512:
6
- metadata.gz: fb550faaa06a888561a3557b10f35fc867ca341e6e4008a49eb5c5137aa79b3490e6a5b24444d67c85ffc79c2e683aff7a21f948e7867b1b78bbafeccdebe9b8
7
- data.tar.gz: ad60584710485b804af453696912a2676d56905e69642ead265111689ea10212768925f5c48017fd3c6da7547a94e8450b181dd2f89b5db82dc480d79d843d10
6
+ metadata.gz: 40768ad960753cef68edfc3cdcba75fa86c6203b0612f4eb35d387ead059d745d40ffeffaacdc6bac6487a7f3795de1be17c72cd8f2262b1d9ff8b34e6e44b8b
7
+ data.tar.gz: 536c77381692428674e69e3492903ae60acaf455e2b320686f5eee26f2ad88c80614f4c7dd1a769379fdecfa7c66eefe545e702dbaf704bfc06d2a2f4755c8c6
data/README.md CHANGED
@@ -81,6 +81,30 @@ them to fail.
81
81
  - `--worker-id=IDENTIFIER` or `ENV[MINITEST_WORKER_ID]`: The ID of the worker,
82
82
  which should be unique to the cluster. We will default to a UUID if this is
83
83
  not set, which generally is fine.
84
+ - `--key-ttl=SECONDS` or `ENV[MINITEST_KEY_TTL_SECONDS]` (default: 86400, i.e.
85
+ 24 hours). The expiry applied to every Redis key a run owns, refreshed on
86
+ every write. A run's statistics deliberately outlive the run itself so retry
87
+ mode can read them, and this is what eventually reclaims them. Raise it if you
88
+ retry runs more than a day later. Once these keys expire, a later invocation
89
+ with the same run ID performs a full run rather than a selective retry. Do not
90
+ set it below the duration of a run: required state expiring mid-run aborts the
91
+ run to avoid reporting incomplete results. Active workers must use the same
92
+ TTL. A later retry may increase the retained run's TTL, but cannot decrease it
93
+ while that state exists, because doing so could invalidate another worker's
94
+ completion-grace wait.
95
+ - `--stall-timeout=SECONDS` or `ENV[MINITEST_STALL_TIMEOUT_SECONDS]` (default:
96
+ 300, i.e. 5 minutes). After this long without processing a batch or observing
97
+ the run counters change, inspect the Redis stream. If it has no pending or
98
+ undelivered tests but `acks` does not equal `size`, abort with a diagnostic
99
+ instead of waiting silently forever. A producer heartbeat keeps slow test
100
+ discovery from being mistaken for a stall. Keep this comfortably above the
101
+ normal end-of-run wait for your suite.
102
+ - `--completion-grace=SECONDS` or `ENV[MINITEST_COMPLETION_GRACE_SECONDS]`
103
+ (default: 30). Before reusing a recently completed run ID, wait this long for
104
+ late workers from the previous cohort to finish. The key TTL must exceed this
105
+ grace period by at least one second so the retained retry snapshot cannot
106
+ expire while a worker is waiting. Lower this only when your CI guarantees
107
+ tighter worker-start synchronization.
84
108
  - `--exclude-file=PATH_TO_FILE`: Specify a file of tests to be excluded
85
109
  from running. The file should include test identifiers seperated by
86
110
  newlines.
@@ -88,6 +112,12 @@ them to fail.
88
112
  the test run. The file should include test identifiers seperated by
89
113
  newlines.
90
114
 
115
+ Version 0.3 uses the versioned Redis namespace `minitest/v3/...`; it does not
116
+ share streams or counters with the legacy 0.2 protocol. All workers in one
117
+ cohort should still use the same gem version. Mixed-version cohorts run in
118
+ separate namespaces and can duplicate test execution, but cannot mutate each
119
+ other's coordinator state.
120
+
91
121
  **Limitations**
92
122
 
93
123
  **Parallel tests not supported:** Minitest comes bundled with a parallel test
@@ -11,6 +11,14 @@ module Minitest
11
11
  DEFAULT_MAX_ATTEMPTS = 1
12
12
  DEFAULT_TEST_TIMEOUT_SECONDS = 30.0 # seconds
13
13
 
14
+ # Expiry applied to every Redis key the coordinator writes, refreshed on
15
+ # every write, so a finished run's bookkeeping does not live in the
16
+ # coordinator forever.
17
+ DEFAULT_KEY_TTL_SECONDS = 86_400 # 24 hours
18
+ DEFAULT_STALL_TIMEOUT_SECONDS = 300.0 # 5 minutes
19
+ DEFAULT_COMPLETION_GRACE_SECONDS = 30.0
20
+ MIN_COMPLETION_GRACE_TTL_MARGIN_SECONDS = 1.0
21
+
14
22
  class << self
15
23
  extend T::Sig
16
24
 
@@ -24,6 +32,11 @@ module Minitest
24
32
  test_batch_size: Integer(env["MINITEST_TEST_BATCH_SIZE"] || DEFAULT_BATCH_SIZE),
25
33
  max_attempts: Integer(env["MINITEST_MAX_ATTEMPTS"] || DEFAULT_MAX_ATTEMPTS),
26
34
  max_failures: (max_failures_env = env["MINITEST_MAX_FAILURES"]) ? Integer(max_failures_env) : nil,
35
+ key_ttl_seconds: Integer(env["MINITEST_KEY_TTL_SECONDS"] || DEFAULT_KEY_TTL_SECONDS),
36
+ stall_timeout_seconds: Float(env["MINITEST_STALL_TIMEOUT_SECONDS"] || DEFAULT_STALL_TIMEOUT_SECONDS),
37
+ completion_grace_seconds: Float(
38
+ env["MINITEST_COMPLETION_GRACE_SECONDS"] || DEFAULT_COMPLETION_GRACE_SECONDS,
39
+ ),
27
40
  )
28
41
  end
29
42
 
@@ -83,6 +96,18 @@ module Minitest
83
96
  configuration.shuffle_suites = enabled
84
97
  end
85
98
 
99
+ opts.on("--key-ttl=SECONDS", "Expiry for the coordinator's Redis keys, refreshed on every write") do |ttl|
100
+ configuration.key_ttl_seconds = Integer(ttl)
101
+ end
102
+
103
+ opts.on("--stall-timeout=SECONDS", "Abort a drained Redis queue that has stopped making progress") do |timeout|
104
+ configuration.stall_timeout_seconds = Float(timeout)
105
+ end
106
+
107
+ opts.on("--completion-grace=SECONDS", "Wait before reusing a recently completed run ID") do |grace|
108
+ configuration.completion_grace_seconds = Float(grace)
109
+ end
110
+
86
111
  configuration
87
112
  end
88
113
  end
@@ -102,9 +127,13 @@ module Minitest
102
127
  prop :exclude_file, T.nilable(String)
103
128
  prop :include_file, T.nilable(String)
104
129
  prop :shuffle_suites, T::Boolean, default: true
130
+ prop :key_ttl_seconds, Integer, default: DEFAULT_KEY_TTL_SECONDS
131
+ prop :stall_timeout_seconds, Float, default: DEFAULT_STALL_TIMEOUT_SECONDS
132
+ prop :completion_grace_seconds, Float, default: DEFAULT_COMPLETION_GRACE_SECONDS
105
133
 
106
134
  sig { returns(Coordinators::CoordinatorInterface) }
107
135
  def coordinator
136
+ validate!
108
137
  @coordinator ||= T.let(
109
138
  case coordinator_uri.scheme
110
139
  when "redis"
@@ -117,6 +146,23 @@ module Minitest
117
146
  T.nilable(Coordinators::CoordinatorInterface),
118
147
  )
119
148
  end
149
+
150
+ sig { void }
151
+ def validate!
152
+ raise ArgumentError, "key_ttl_seconds must be greater than zero" unless key_ttl_seconds.positive?
153
+
154
+ unless stall_timeout_seconds.positive? && stall_timeout_seconds.finite?
155
+ raise ArgumentError, "stall_timeout_seconds must be finite and greater than zero"
156
+ end
157
+
158
+ unless completion_grace_seconds.positive? && completion_grace_seconds.finite?
159
+ raise ArgumentError, "completion_grace_seconds must be finite and greater than zero"
160
+ end
161
+
162
+ if key_ttl_seconds < completion_grace_seconds + MIN_COMPLETION_GRACE_TTL_MARGIN_SECONDS
163
+ raise ArgumentError, "key_ttl_seconds must exceed completion_grace_seconds by at least one second"
164
+ end
165
+ end
120
166
  end
121
167
  end
122
168
  end
@@ -21,6 +21,12 @@ module Minitest
21
21
  sig { abstract.returns(T::Boolean) }
22
22
  def aborted?; end
23
23
 
24
+ sig { abstract.returns(T::Boolean) }
25
+ def valid_combined_results?; end
26
+
27
+ sig { abstract.returns(T::Boolean) }
28
+ def persisted_truncation?; end
29
+
24
30
  sig { abstract.params(test_selector: TestSelector).void }
25
31
  def produce(test_selector:); end
26
32
 
@@ -39,6 +39,16 @@ module Minitest
39
39
  @aborted
40
40
  end
41
41
 
42
+ sig { override.returns(T::Boolean) }
43
+ def valid_combined_results?
44
+ combined_results.valid?
45
+ end
46
+
47
+ sig { override.returns(T::Boolean) }
48
+ def persisted_truncation?
49
+ combined_results.abort?
50
+ end
51
+
42
52
  sig { override.params(test_selector: TestSelector).void }
43
53
  def produce(test_selector:)
44
54
  if @leader.try_lock