specbandit 1.2.0 → 1.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 +4 -4
- data/README.md +42 -0
- data/lib/specbandit/cli.rb +59 -3
- data/lib/specbandit/publisher.rb +23 -1
- data/lib/specbandit/redis_queue.rb +11 -0
- data/lib/specbandit/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d9934a98dc6ad0d278306da5cb4fdc818f56dceee9118e8581b49393f9d26150
|
|
4
|
+
data.tar.gz: a783e737d7d478397c689e82952c8c6c67e0fbab71c4ddc3ccb6ea34fee2e8b6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ab66609668cfd0d41cab6c243d94268da69f4659f9640f2391707dc74381e3566c459a6646010bc7c2197c7552c3818e78d4b066ba7c9f5e7727da2e0ffe50d
|
|
7
|
+
data.tar.gz: f352d8beb6637eabd22b3e11c0d2c62bce44498ba5da20bdd050785b4588913ecdbb9dfe53c799471bd7b75b9e74722e1efb6f4aeb89e6f9779beb4cbb149f35
|
data/README.md
CHANGED
|
@@ -109,6 +109,14 @@ specbandit push --key pr-123-run-456 spec/models/user_spec.rb spec/models/order_
|
|
|
109
109
|
|
|
110
110
|
File input priority: **stdin > --pattern > direct args**.
|
|
111
111
|
|
|
112
|
+
Add `--reset` when the producer can run more than once for the same key:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
specbandit push --key pr-123-run-456 --reset --pattern 'spec/**/*_spec.rb'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
See [Repeated pushes: `--reset`](#repeated-pushes---reset).
|
|
119
|
+
|
|
112
120
|
### 2. Steal and run from multiple workers
|
|
113
121
|
|
|
114
122
|
Each CI runner steals batches and runs them. Start as many runners as you want -- they'll divide the work automatically.
|
|
@@ -137,6 +145,11 @@ specbandit push [options] [files...]
|
|
|
137
145
|
--pattern PATTERN Glob pattern for file discovery
|
|
138
146
|
--redis-url URL Redis URL (default: redis://localhost:6379)
|
|
139
147
|
--key-ttl SECONDS TTL for all Redis keys (default: 604800 / 1 week)
|
|
148
|
+
--reset Empty the key before pushing (see below)
|
|
149
|
+
|
|
150
|
+
specbandit reset [options]
|
|
151
|
+
--key KEY Redis queue key (required)
|
|
152
|
+
--redis-url URL Redis URL (default: redis://localhost:6379)
|
|
140
153
|
|
|
141
154
|
specbandit work [options] [-- extra-opts...]
|
|
142
155
|
--key KEY Redis queue key (required)
|
|
@@ -423,6 +436,34 @@ Override via `--key-ttl` or `SPECBANDIT_KEY_TTL` (set it on `push`, which is whe
|
|
|
423
436
|
specbandit push --key "pr-42-run-100" --key-ttl 259200 --pattern 'spec/**/*_spec.rb'
|
|
424
437
|
```
|
|
425
438
|
|
|
439
|
+
## Repeated pushes: `--reset`
|
|
440
|
+
|
|
441
|
+
The queue key is scoped by CI run, not by CI attempt. It has to be: re-running a single failed runner does not re-run the job that pushed, so that runner must still find the queue and the published marker the first attempt created.
|
|
442
|
+
|
|
443
|
+
The cost is that the producer is not idempotent. A producer that pushes and then fails, or that is re-run as part of the whole workflow, appends a second copy of the work list to the same key. Every file is then enqueued twice, so the suite runs twice, and any two copies that reach the same worker are loaded twice in one process. For test files that define constants at file scope, the second load is fatal.
|
|
444
|
+
|
|
445
|
+
`--reset` makes the push idempotent:
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
specbandit push --key "pr-42-run-100" --reset --pattern 'spec/**/*_spec.rb'
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
The queue and its `<key>:published` marker are deleted, then the work list is pushed, so the key holds exactly one copy however many times the producer runs. The leftover count is logged, and a non-zero one tells you an earlier attempt pushed a list nobody consumed:
|
|
452
|
+
|
|
453
|
+
```
|
|
454
|
+
[specbandit] Reset key 'pr-42-run-100': discarded 4213 queued files from a previous push.
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
There is also a standalone command, for callers that clean up separately from the push:
|
|
458
|
+
|
|
459
|
+
```bash
|
|
460
|
+
specbandit reset --key "pr-42-run-100"
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Both leave the per-runner rerun keys and the failed keys alone, so a single-runner re-run can still replay its own files. A runner that finds data in both the shared queue and its rerun key is the **full rerun** case in the table above, and it resets its own memory.
|
|
464
|
+
|
|
465
|
+
A reset with nothing to reset is not an error. Neither command clears the key when there is nothing to push in its place: dropping the marker on its own would make every worker on that key crash as "never published".
|
|
466
|
+
|
|
426
467
|
## How it works
|
|
427
468
|
|
|
428
469
|
- **Push** uses `RPUSH` to append all file paths to a Redis list in a single command, sets `EXPIRE` on the key, and writes a durable `<key>:published` marker (with the same TTL). Empty Redis lists are auto-deleted, so this marker is what lets `work` tell "never pushed" (crash) apart from "drained, arriving late" (exit 0).
|
|
@@ -430,6 +471,7 @@ specbandit push --key "pr-42-run-100" --key-ttl 259200 --pattern 'spec/**/*_spec
|
|
|
430
471
|
- **Record** (when `--key-rerun` is set): after each steal, the batch is also `RPUSH`ed to the per-runner rerun key.
|
|
431
472
|
- **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
473
|
- **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.
|
|
474
|
+
- **Reset** (`push --reset` or `specbandit reset`) removes the queue and its `<key>:published` marker in a single `DEL`, so a producer that runs twice cannot enqueue the work list twice. Rerun and failed keys are untouched.
|
|
433
475
|
- **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.
|
|
434
476
|
- **Run** delegates to the configured adapter:
|
|
435
477
|
- **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
|
@@ -4,7 +4,7 @@ require 'optparse'
|
|
|
4
4
|
|
|
5
5
|
module Specbandit
|
|
6
6
|
class CLI
|
|
7
|
-
COMMANDS = %w[push work].freeze
|
|
7
|
+
COMMANDS = %w[push work reset].freeze
|
|
8
8
|
|
|
9
9
|
def self.run(argv = ARGV)
|
|
10
10
|
new(argv).execute
|
|
@@ -24,6 +24,8 @@ module Specbandit
|
|
|
24
24
|
run_push
|
|
25
25
|
when 'work'
|
|
26
26
|
run_work
|
|
27
|
+
when 'reset'
|
|
28
|
+
run_reset
|
|
27
29
|
when nil, '-h', '--help'
|
|
28
30
|
print_usage
|
|
29
31
|
0
|
|
@@ -46,7 +48,7 @@ module Specbandit
|
|
|
46
48
|
private
|
|
47
49
|
|
|
48
50
|
def run_push
|
|
49
|
-
options = { pattern: nil }
|
|
51
|
+
options = { pattern: nil, reset: false }
|
|
50
52
|
|
|
51
53
|
parser = OptionParser.new do |opts|
|
|
52
54
|
opts.banner = 'Usage: specbandit push [options] [files...]'
|
|
@@ -67,6 +69,10 @@ module Specbandit
|
|
|
67
69
|
Specbandit.configuration.key_ttl = v
|
|
68
70
|
end
|
|
69
71
|
|
|
72
|
+
opts.on('--reset', 'Empty the key before pushing, so a re-run cannot enqueue a second copy') do
|
|
73
|
+
options[:reset] = true
|
|
74
|
+
end
|
|
75
|
+
|
|
70
76
|
opts.on('-h', '--help', 'Show this help') do
|
|
71
77
|
puts opts
|
|
72
78
|
return 0
|
|
@@ -78,11 +84,51 @@ module Specbandit
|
|
|
78
84
|
|
|
79
85
|
publisher = Publisher.new
|
|
80
86
|
files_arg = argv.empty? ? [] : argv
|
|
81
|
-
count = publisher.publish(files: files_arg, pattern: options[:pattern])
|
|
87
|
+
count = publisher.publish(files: files_arg, pattern: options[:pattern], reset: options[:reset])
|
|
82
88
|
|
|
83
89
|
count.positive? ? 0 : 1
|
|
84
90
|
end
|
|
85
91
|
|
|
92
|
+
# Empty a queue key and its published marker, leaving it as if nothing had
|
|
93
|
+
# ever been pushed. Standalone counterpart to `push --reset`, for callers
|
|
94
|
+
# that clean up separately from the push.
|
|
95
|
+
def run_reset
|
|
96
|
+
parser = OptionParser.new do |opts|
|
|
97
|
+
opts.banner = 'Usage: specbandit reset [options]'
|
|
98
|
+
|
|
99
|
+
opts.on('--key KEY', 'Redis queue key (required, or set SPECBANDIT_KEY)') do |v|
|
|
100
|
+
Specbandit.configuration.key = v
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
opts.on('--redis-url URL', 'Redis URL (default: redis://localhost:6379)') do |v|
|
|
104
|
+
Specbandit.configuration.redis_url = v
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
opts.on('-h', '--help', 'Show this help') do
|
|
108
|
+
puts opts
|
|
109
|
+
return 0
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
parser.parse!(argv)
|
|
114
|
+
Specbandit.configuration.validate!
|
|
115
|
+
|
|
116
|
+
key = Specbandit.configuration.key
|
|
117
|
+
queue = RedisQueue.new
|
|
118
|
+
|
|
119
|
+
begin
|
|
120
|
+
stale = queue.length(key)
|
|
121
|
+
queue.clear(key)
|
|
122
|
+
puts "[specbandit] Reset key '#{key}': discarded #{stale} queued files."
|
|
123
|
+
ensure
|
|
124
|
+
queue.close
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Removing nothing is a valid outcome: the caller asked for an empty key
|
|
128
|
+
# and got one.
|
|
129
|
+
0
|
|
130
|
+
end
|
|
131
|
+
|
|
86
132
|
def run_work
|
|
87
133
|
parser = OptionParser.new do |opts|
|
|
88
134
|
opts.banner = 'Usage: specbandit work [options] [-- extra-opts...]'
|
|
@@ -200,12 +246,22 @@ module Specbandit
|
|
|
200
246
|
Usage:
|
|
201
247
|
specbandit push [options] [files...] Enqueue test files into Redis
|
|
202
248
|
specbandit work [options] [-- extra-opts...] Steal and run test file batches
|
|
249
|
+
specbandit reset [options] Empty a queue key and its published marker
|
|
203
250
|
|
|
204
251
|
Push options:
|
|
205
252
|
--key KEY Redis queue key (required, or set SPECBANDIT_KEY)
|
|
206
253
|
--pattern PATTERN Glob pattern for file discovery (e.g. 'spec/**/*_spec.rb')
|
|
207
254
|
--redis-url URL Redis URL (default: redis://localhost:6379)
|
|
208
255
|
--key-ttl SECONDS TTL for all Redis keys (default: 604800 / 1 week)
|
|
256
|
+
--reset Empty the key before pushing (see Reset options)
|
|
257
|
+
|
|
258
|
+
Reset options:
|
|
259
|
+
--key KEY Redis queue key (required, or set SPECBANDIT_KEY)
|
|
260
|
+
--redis-url URL Redis URL (default: redis://localhost:6379)
|
|
261
|
+
|
|
262
|
+
Deletes the queue list and its ':published' marker, leaving the key as if
|
|
263
|
+
nothing had ever been pushed. Per-runner rerun and failed keys are left
|
|
264
|
+
alone, so a re-run of a single shard can still replay its own files.
|
|
209
265
|
|
|
210
266
|
Work options:
|
|
211
267
|
--key KEY Redis queue key (required, or set SPECBANDIT_KEY)
|
data/lib/specbandit/publisher.rb
CHANGED
|
@@ -17,8 +17,11 @@ module Specbandit
|
|
|
17
17
|
# Resolve files from the three input sources (priority: stdin > pattern > args)
|
|
18
18
|
# and push them onto the Redis queue.
|
|
19
19
|
#
|
|
20
|
+
# With `reset: true` the key is emptied first, so the queue ends up holding
|
|
21
|
+
# exactly this work list even if an earlier attempt already pushed one.
|
|
22
|
+
#
|
|
20
23
|
# Returns the number of files enqueued.
|
|
21
|
-
def publish(files: [], pattern: nil)
|
|
24
|
+
def publish(files: [], pattern: nil, reset: false)
|
|
22
25
|
resolved = resolve_files(files: files, pattern: pattern)
|
|
23
26
|
|
|
24
27
|
if resolved.empty?
|
|
@@ -26,6 +29,11 @@ module Specbandit
|
|
|
26
29
|
return 0
|
|
27
30
|
end
|
|
28
31
|
|
|
32
|
+
# Only reset once there is something to put in its place. Clearing on an
|
|
33
|
+
# empty push would drop the marker too and leave workers crashing on a
|
|
34
|
+
# key that looks like it was never published.
|
|
35
|
+
reset_key if reset
|
|
36
|
+
|
|
29
37
|
push_ms = measure { queue.push(key, resolved, ttl: key_ttl) }
|
|
30
38
|
# Record a durable "published" marker so workers can tell a drained
|
|
31
39
|
# queue ("worker arriving late", OK) apart from one that was never
|
|
@@ -40,6 +48,20 @@ module Specbandit
|
|
|
40
48
|
|
|
41
49
|
private
|
|
42
50
|
|
|
51
|
+
# Empty the key before pushing. The leftover count is reported because a
|
|
52
|
+
# non-zero one means an earlier attempt pushed a list that no worker ever
|
|
53
|
+
# consumed, which is worth seeing in the producer's log.
|
|
54
|
+
def reset_key
|
|
55
|
+
stale = queue.length(key)
|
|
56
|
+
queue.clear(key)
|
|
57
|
+
|
|
58
|
+
if stale.positive?
|
|
59
|
+
output.puts "[specbandit] Reset key '#{key}': discarded #{stale} queued files from a previous push."
|
|
60
|
+
else
|
|
61
|
+
output.puts "[specbandit] Reset key '#{key}': nothing left over."
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
43
65
|
# Run the block and return the wall-clock time it took, in milliseconds.
|
|
44
66
|
# Used to surface how long the Redis round-trips took in the push log.
|
|
45
67
|
def measure
|
|
@@ -94,6 +94,17 @@ module Specbandit
|
|
|
94
94
|
with_retries { redis.del(key) }
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
# Remove a queue and its published marker together, so the key is back to
|
|
98
|
+
# the state it had before anything was ever pushed to it. A producer calls
|
|
99
|
+
# this before pushing: the queue key is not scoped by run attempt, so a
|
|
100
|
+
# second attempt would otherwise stack another copy of the work list on
|
|
101
|
+
# top of whatever the first attempt left behind.
|
|
102
|
+
#
|
|
103
|
+
# Returns the number of keys removed (0, 1 or 2).
|
|
104
|
+
def clear(key)
|
|
105
|
+
with_retries { redis.del(key, published_marker(key)) }
|
|
106
|
+
end
|
|
107
|
+
|
|
97
108
|
def close
|
|
98
109
|
redis.close
|
|
99
110
|
end
|
data/lib/specbandit/version.rb
CHANGED