specbandit 1.0.1 → 1.2.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/README.md +3 -2
- data/lib/specbandit/cli.rb +4 -0
- data/lib/specbandit/configuration.rb +20 -1
- data/lib/specbandit/redis_queue.rb +28 -6
- data/lib/specbandit/version.rb +1 -1
- data/lib/specbandit/worker.rb +12 -10
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 24148d640548adc9f3bd6714ad39f4bfd380eeaf8e904f0fffd4b777a9b13d4c
|
|
4
|
+
data.tar.gz: '082673e08f9b40e9c8babae73f658d91b7627a40002b1e326d62fb49c5242d4f'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 06ced4b5b9530b9f836d90a0e05a663acf88d16f008c6f181acc19a1711ee6260465f42b7e27c76d3e16ec8d28ae621bfd4a614d2ec2aae26312927cb40fd3fb
|
|
7
|
+
data.tar.gz: 6f1578c3db2a446bce8677d16020014e4fa1c30e2483789f41311c1d18ba75567875a65728fca9f48136951b722b91631d674e995d2a66b1ebe26b99b910e128
|
data/README.md
CHANGED
|
@@ -319,7 +319,7 @@ The full decision table:
|
|
|
319
319
|
| Yes | Drained / empty | Empty | **OK, exit 0.** Worker arriving late -- everything was already taken by peers and this runner has no re-run memory. |
|
|
320
320
|
| Yes | Has data | Empty | **Steal.** Classic run: pop batches from the shared queue (and record them to the rerun key if one is configured). |
|
|
321
321
|
| Yes | Drained / empty | Has data | **Replay.** Classic re-run: ignore the shared queue and re-run exactly the recorded files. |
|
|
322
|
-
| Yes | Has data | Has data | **
|
|
322
|
+
| Yes | Has data | Has data | **Full rerun.** The queue was re-pushed while this runner still holds rerun memory from a previous run. The stale rerun key is deleted and the runner steals from the shared queue like a classic run, re-recording as it goes. |
|
|
323
323
|
|
|
324
324
|
> **Empty key names count as "not provided".** A `--key-rerun` whose value is an empty string -- e.g. `--key-rerun "$VAR"` where `$VAR` is unset in CI -- is treated exactly like `--key-rerun` being absent. The same applies to `--key-failed`: an empty/unset name is treated as "not configured" and no failed files are recorded.
|
|
325
325
|
|
|
@@ -351,7 +351,7 @@ The full decision table:
|
|
|
351
351
|
|
|
352
352
|
Key details:
|
|
353
353
|
|
|
354
|
-
- **Replay reads non-destructively** (`LRANGE`, not `LPOP`).
|
|
354
|
+
- **Replay reads non-destructively** (`LRANGE`, not `LPOP`). Replay never consumes the rerun key, so re-running the same runner multiple times replays the same files every time. The one case where the rerun key is deleted (`DEL`) is a **full rerun** -- the shared queue has data again while the rerun key still holds files from a previous run -- where the stale memory is reset and rebuilt from the newly stolen batches.
|
|
355
355
|
- **The shared queue is never touched in replay mode**. Other runners are unaffected.
|
|
356
356
|
- **Each runner has its own rerun key**. Only the re-run runner enters replay mode; runners that aren't re-run don't start at all.
|
|
357
357
|
|
|
@@ -429,6 +429,7 @@ specbandit push --key "pr-42-run-100" --key-ttl 259200 --pattern 'spec/**/*_spec
|
|
|
429
429
|
- **Steal** uses `LPOP key count` (Redis 6.2+), which atomically pops up to N elements. No Lua scripts, no locks, no race conditions.
|
|
430
430
|
- **Record** (when `--key-rerun` is set): after each steal, the batch is also `RPUSH`ed to the per-runner rerun key.
|
|
431
431
|
- **Replay** (when the rerun key has data and the shared queue is drained): reads all files from the rerun key via `LRANGE` (non-destructive), splits into batches, and runs them locally. The shared queue is never touched.
|
|
432
|
+
- **Full rerun** (when both the shared queue and the rerun key have data): the stale rerun key is removed with `DEL`, then the runner steals from the shared queue and re-records as in a classic run.
|
|
432
433
|
- **Mode selection** is derived entirely from Redis (published marker + queue/rerun state) -- see the decision table above. There is no re-run flag or environment variable.
|
|
433
434
|
- **Run** delegates to the configured adapter:
|
|
434
435
|
- **CLI adapter**: spawns a shell command per batch via `Open3`, appending file paths as arguments. Works with any test runner.
|
data/lib/specbandit/cli.rb
CHANGED
|
@@ -131,6 +131,10 @@ module Specbandit
|
|
|
131
131
|
Specbandit.configuration.report = v
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
+
opts.on('--redis-max-attempts N', Integer, 'Redis connection retry attempts (default: 5)') do |v|
|
|
135
|
+
Specbandit.configuration.redis_max_attempts = v
|
|
136
|
+
end
|
|
137
|
+
|
|
134
138
|
opts.on('--verbose', 'Show per-batch file list and full command output (default: quiet)') do
|
|
135
139
|
Specbandit.configuration.verbose = true
|
|
136
140
|
end
|
|
@@ -5,7 +5,9 @@ module Specbandit
|
|
|
5
5
|
attr_accessor :redis_url, :batch_size, :key, :rspec_opts, :key_ttl,
|
|
6
6
|
:key_rerun, :verbose,
|
|
7
7
|
:adapter, :command, :command_opts,
|
|
8
|
-
:key_failed, :report
|
|
8
|
+
:key_failed, :report,
|
|
9
|
+
:redis_max_attempts, :redis_connect_timeout, :redis_timeout,
|
|
10
|
+
:redis_reconnect_attempts
|
|
9
11
|
|
|
10
12
|
DEFAULT_REDIS_URL = 'redis://localhost:6379'
|
|
11
13
|
DEFAULT_BATCH_SIZE = 5
|
|
@@ -16,6 +18,17 @@ module Specbandit
|
|
|
16
18
|
DEFAULT_KEY_TTL = 604_800 # 1 week in seconds
|
|
17
19
|
DEFAULT_ADAPTER = 'cli'
|
|
18
20
|
|
|
21
|
+
# Redis connection resilience. Redis is a best-effort coordination store for
|
|
22
|
+
# a distributed test run, and CI runners can sit a WAN hop away from it
|
|
23
|
+
# (e.g. cross-datacenter mesh), so a transient blip must not red the build.
|
|
24
|
+
# We retry a handful of times with capped exponential backoff, and give the
|
|
25
|
+
# underlying client explicit connect/read/write timeouts + reconnects rather
|
|
26
|
+
# than relying on library defaults.
|
|
27
|
+
DEFAULT_REDIS_MAX_ATTEMPTS = 5
|
|
28
|
+
DEFAULT_REDIS_CONNECT_TIMEOUT = 3.0
|
|
29
|
+
DEFAULT_REDIS_TIMEOUT = 5.0
|
|
30
|
+
DEFAULT_REDIS_RECONNECT_ATTEMPTS = 3
|
|
31
|
+
|
|
19
32
|
def initialize
|
|
20
33
|
@redis_url = ENV.fetch('SPECBANDIT_REDIS_URL', DEFAULT_REDIS_URL)
|
|
21
34
|
@batch_size = Integer(ENV.fetch('SPECBANDIT_BATCH_SIZE', DEFAULT_BATCH_SIZE))
|
|
@@ -29,12 +42,18 @@ module Specbandit
|
|
|
29
42
|
@command_opts = parse_space_separated(ENV.fetch('SPECBANDIT_COMMAND_OPTS', nil))
|
|
30
43
|
@key_failed = ENV.fetch('SPECBANDIT_KEY_FAILED', nil)
|
|
31
44
|
@report = ENV.fetch('SPECBANDIT_REPORT', nil)
|
|
45
|
+
|
|
46
|
+
@redis_max_attempts = Integer(ENV.fetch('SPECBANDIT_REDIS_MAX_ATTEMPTS', DEFAULT_REDIS_MAX_ATTEMPTS))
|
|
47
|
+
@redis_connect_timeout = Float(ENV.fetch('SPECBANDIT_REDIS_CONNECT_TIMEOUT', DEFAULT_REDIS_CONNECT_TIMEOUT))
|
|
48
|
+
@redis_timeout = Float(ENV.fetch('SPECBANDIT_REDIS_TIMEOUT', DEFAULT_REDIS_TIMEOUT))
|
|
49
|
+
@redis_reconnect_attempts = Integer(ENV.fetch('SPECBANDIT_REDIS_RECONNECT_ATTEMPTS', DEFAULT_REDIS_RECONNECT_ATTEMPTS))
|
|
32
50
|
end
|
|
33
51
|
|
|
34
52
|
def validate!
|
|
35
53
|
raise Error, 'key is required (set via --key or SPECBANDIT_KEY)' if key.nil? || key.empty?
|
|
36
54
|
raise Error, 'batch_size must be a positive integer' unless batch_size.positive?
|
|
37
55
|
raise Error, 'key_ttl must be a positive integer' unless key_ttl.positive?
|
|
56
|
+
raise Error, 'redis_max_attempts must be a positive integer' unless redis_max_attempts.positive?
|
|
38
57
|
end
|
|
39
58
|
|
|
40
59
|
private
|
|
@@ -6,12 +6,28 @@ module Specbandit
|
|
|
6
6
|
class RedisQueue
|
|
7
7
|
attr_reader :redis
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
# Cap the exponential backoff so a real outage degrades/fails within a
|
|
10
|
+
# bounded window instead of sleeping for minutes on the last attempts.
|
|
11
|
+
MAX_BACKOFF_SECONDS = 10
|
|
12
|
+
|
|
13
|
+
def initialize(
|
|
14
|
+
redis_url: Specbandit.configuration.redis_url,
|
|
15
|
+
connect_timeout: Specbandit.configuration.redis_connect_timeout,
|
|
16
|
+
read_timeout: Specbandit.configuration.redis_timeout,
|
|
17
|
+
write_timeout: Specbandit.configuration.redis_timeout,
|
|
18
|
+
reconnect_attempts: Specbandit.configuration.redis_reconnect_attempts,
|
|
19
|
+
max_attempts: Specbandit.configuration.redis_max_attempts
|
|
20
|
+
)
|
|
21
|
+
@max_attempts = max_attempts
|
|
22
|
+
@redis = Redis.new(
|
|
23
|
+
url: redis_url,
|
|
24
|
+
connect_timeout: connect_timeout,
|
|
25
|
+
read_timeout: read_timeout,
|
|
26
|
+
write_timeout: write_timeout,
|
|
27
|
+
reconnect_attempts: reconnect_attempts
|
|
28
|
+
)
|
|
11
29
|
end
|
|
12
30
|
|
|
13
|
-
MAX_ATTEMPTS = 3
|
|
14
|
-
|
|
15
31
|
# Push file paths onto the queue and set an expiry on the key.
|
|
16
32
|
# Returns the new length of the list.
|
|
17
33
|
def push(key, files, ttl: nil)
|
|
@@ -72,6 +88,12 @@ module Specbandit
|
|
|
72
88
|
with_retries { redis.lrange(key, 0, -1) }
|
|
73
89
|
end
|
|
74
90
|
|
|
91
|
+
# Remove a key entirely. Used to discard stale rerun memory when a
|
|
92
|
+
# full rerun starts over from the shared queue.
|
|
93
|
+
def delete(key)
|
|
94
|
+
with_retries { redis.del(key) }
|
|
95
|
+
end
|
|
96
|
+
|
|
75
97
|
def close
|
|
76
98
|
redis.close
|
|
77
99
|
end
|
|
@@ -83,7 +105,7 @@ module Specbandit
|
|
|
83
105
|
"#{key}:published"
|
|
84
106
|
end
|
|
85
107
|
|
|
86
|
-
def with_retries(attempts:
|
|
108
|
+
def with_retries(attempts: @max_attempts)
|
|
87
109
|
retries = 0
|
|
88
110
|
begin
|
|
89
111
|
yield
|
|
@@ -91,7 +113,7 @@ module Specbandit
|
|
|
91
113
|
retries += 1
|
|
92
114
|
raise if retries >= attempts
|
|
93
115
|
|
|
94
|
-
delay = 2**retries
|
|
116
|
+
delay = [2**retries, MAX_BACKOFF_SECONDS].min
|
|
95
117
|
warn "[specbandit] Redis connection failed (attempt #{retries}/#{attempts}): #{e.message}. Retrying in #{delay}s..."
|
|
96
118
|
sleep(delay)
|
|
97
119
|
retry
|
data/lib/specbandit/version.rb
CHANGED
data/lib/specbandit/worker.rb
CHANGED
|
@@ -76,7 +76,7 @@ module Specbandit
|
|
|
76
76
|
# Yes | empty/drained | empty | OK: worker arriving late (0)
|
|
77
77
|
# Yes | has data | empty | Steal (record if rerun key set)
|
|
78
78
|
# Yes | empty/drained | has data | Replay recorded files
|
|
79
|
-
# Yes | has data | has data |
|
|
79
|
+
# Yes | has data | has data | Full rerun: reset rerun key, steal
|
|
80
80
|
#
|
|
81
81
|
# "Published" is a durable marker written by `specbandit push`; it is the
|
|
82
82
|
# only reliable signal that work was ever enqueued, because Redis
|
|
@@ -89,7 +89,7 @@ module Specbandit
|
|
|
89
89
|
rerun_files = key_present?(key_rerun) ? queue.read_all(key_rerun) : []
|
|
90
90
|
|
|
91
91
|
if key_has_data && rerun_files.any?
|
|
92
|
-
|
|
92
|
+
run_full_rerun
|
|
93
93
|
elsif rerun_files.any?
|
|
94
94
|
run_replay(rerun_files)
|
|
95
95
|
elsif key_has_data
|
|
@@ -119,14 +119,16 @@ module Specbandit
|
|
|
119
119
|
1
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
-
# Both the shared queue and this runner's rerun key hold files
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
output.puts
|
|
129
|
-
|
|
122
|
+
# Both the shared queue and this runner's rerun key hold files: the queue
|
|
123
|
+
# was re-pushed while this runner still carries rerun memory from a
|
|
124
|
+
# previous run (a full rerun). The stored memory is stale -- discard it
|
|
125
|
+
# and steal from the shared queue like a classic run, re-recording each
|
|
126
|
+
# stolen batch as we go.
|
|
127
|
+
def run_full_rerun
|
|
128
|
+
output.puts "[specbandit] Shared queue '#{key}' and rerun key '#{key_rerun}' both have files. " \
|
|
129
|
+
"Full rerun: resetting '#{key_rerun}' and working from '#{key}'."
|
|
130
|
+
queue.delete(key_rerun)
|
|
131
|
+
run_steal(record: true)
|
|
130
132
|
end
|
|
131
133
|
|
|
132
134
|
# Replay mode: run a known list of files in local batches.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: specbandit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ferran Basora
|
|
@@ -109,7 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
110
|
version: '0'
|
|
111
111
|
requirements: []
|
|
112
|
-
rubygems_version: 4.0.
|
|
112
|
+
rubygems_version: 4.0.16
|
|
113
113
|
specification_version: 4
|
|
114
114
|
summary: Distributed test runner using Redis as a work queue
|
|
115
115
|
test_files: []
|