ci-queue 0.99.0 → 0.101.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/Gemfile.lock +1 -1
- data/README.md +14 -0
- data/ci-queue.gemspec +4 -3
- data/lib/ci/queue/configuration.rb +4 -1
- data/lib/ci/queue/redis/base.rb +24 -7
- data/lib/ci/queue/redis/monitor.rb +17 -1
- data/lib/ci/queue/redis/requeue.lua +22 -7
- data/lib/ci/queue/redis/reserve.lua +18 -7
- data/lib/ci/queue/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: 612d0f2f3077fdd5b7e74b343649a162101c2961c8b4cda8329ca10fbd831e6b
|
|
4
|
+
data.tar.gz: bdd01c95971bcbfab5999b3754f8d019be35a7202e5f766d5df19191462e5563
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69a03983e12372fe0ab0258ec08262b2672624d49d2d63a0c35d0a89b4c8c85b3fd89ece2ce3c71de112652cd327868ee0fa7633ae283facb3b0bf57035d5724
|
|
7
|
+
data.tar.gz: 44ead0bb47b690b19652982496f8a48d698f5bb78ea4f88c01e56b7774d4c79f6b6015b50e91cb70e241fc00a13bf842a72828792c7f716644cacf12bfb8bb2d
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -260,3 +260,17 @@ After merging changes to `main`, follow these steps to release and propagate the
|
|
|
260
260
|
`ci-queue` expects the Redis server to have an [eviction policy](https://redis.io/docs/manual/eviction/#eviction-policies) of `allkeys-lru`.
|
|
261
261
|
|
|
262
262
|
You can also use `--redis-ttl` to set a custom expiration time for all CI Queue keys, this defaults to 8 hours (28,800 seconds)
|
|
263
|
+
|
|
264
|
+
## Redis over TLS
|
|
265
|
+
|
|
266
|
+
Use a `rediss://` queue URL to connect to Redis over TLS. The server certificate and hostname are verified using the system's trusted CAs.
|
|
267
|
+
|
|
268
|
+
> **Upgrading from 0.99.0 or earlier:** those versions did not verify certificates for `rediss://` URLs. If your Redis presents a self-signed or otherwise untrusted certificate, connections now fail with `certificate verify failed`. Workers treat this like an unreachable Redis: they report `Ran 0 tests` and exit successfully, and the error only surfaces when `report` fails with `Redis::CannotConnectError`. Trust the issuing CA or opt out as described below.
|
|
269
|
+
|
|
270
|
+
If your Redis server presents a certificate signed by a private CA, point OpenSSL at that CA (for example with `SSL_CERT_FILE=/path/to/ca.pem`).
|
|
271
|
+
|
|
272
|
+
If your hosted Redis only offers a self-signed certificate that can't be verified, you can opt out of verification with `CI_QUEUE_REDIS_SSL_VERIFY=0` (or `CI::Queue::Configuration#redis_ssl_verify = false`). The connection stays encrypted, but the server isn't authenticated, so anyone who can intercept traffic between your CI workers and Redis can impersonate the server and read the credentials in the queue URL. Only use this when you accept that risk.
|
|
273
|
+
|
|
274
|
+
| Environment variable | Description |
|
|
275
|
+
|---|---|
|
|
276
|
+
| `CI_QUEUE_REDIS_SSL_VERIFY=0` | Disable TLS certificate verification for `rediss://` connections. Defaults to enabled. No CLI equivalent. |
|
data/ci-queue.gemspec
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
lib = File.expand_path('../lib', __FILE__)
|
|
4
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
5
|
require 'ci/queue/version'
|
|
6
|
-
require 'pathname'
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
# Release Lua scripts are gitignored copies made by `rake scripts:copy`, so
|
|
8
|
+
# `git ls-files` misses them. Glob without touching CI::Queue constants:
|
|
9
|
+
# Dependabot evaluates this file with the `require` above rescued away.
|
|
10
|
+
lua_scripts = Dir.glob('lib/ci/queue/redis/*.lua', base: __dir__)
|
|
10
11
|
|
|
11
12
|
Gem::Specification.new do |spec|
|
|
12
13
|
spec.name = 'ci-queue'
|
|
@@ -11,6 +11,7 @@ module CI
|
|
|
11
11
|
attr_writer :lazy_load_streaming_timeout
|
|
12
12
|
attr_accessor :lazy_load_test_helpers
|
|
13
13
|
attr_accessor :skip_stale_tests
|
|
14
|
+
attr_accessor :redis_ssl_verify
|
|
14
15
|
attr_reader :circuit_breakers
|
|
15
16
|
attr_writer :seed, :build_id
|
|
16
17
|
attr_writer :queue_init_timeout, :report_timeout, :inactive_workers_timeout
|
|
@@ -34,6 +35,7 @@ module CI
|
|
|
34
35
|
lazy_load_streaming_timeout: (env['CI_QUEUE_LAZY_LOAD_STREAM_TIMEOUT'] || env['CI_QUEUE_STREAM_TIMEOUT'])&.to_i,
|
|
35
36
|
lazy_load_test_helpers: env['CI_QUEUE_LAZY_LOAD_TEST_HELPERS'] || env['CI_QUEUE_TEST_HELPERS'],
|
|
36
37
|
skip_stale_tests: %w(1 true).include?(env['CI_QUEUE_SKIP_STALE_TESTS']&.strip&.downcase),
|
|
38
|
+
redis_ssl_verify: !%w(0 false).include?(env['CI_QUEUE_REDIS_SSL_VERIFY']&.strip&.downcase),
|
|
37
39
|
)
|
|
38
40
|
end
|
|
39
41
|
|
|
@@ -60,7 +62,7 @@ module CI
|
|
|
60
62
|
queue_init_timeout: nil, redis_ttl: 8 * 60 * 60, report_timeout: nil, inactive_workers_timeout: nil,
|
|
61
63
|
export_flaky_tests_file: nil, warnings_file: nil, debug_log: nil, max_missed_heartbeat_seconds: nil, heartbeat_max_test_duration: nil,
|
|
62
64
|
lazy_load: false, lazy_load_stream_batch_size: nil, lazy_load_streaming_timeout: nil, lazy_load_test_helpers: nil,
|
|
63
|
-
skip_stale_tests: false)
|
|
65
|
+
skip_stale_tests: false, redis_ssl_verify: true)
|
|
64
66
|
@build_id = build_id
|
|
65
67
|
@circuit_breakers = [CircuitBreaker::Disabled]
|
|
66
68
|
@failure_file = failure_file
|
|
@@ -94,6 +96,7 @@ module CI
|
|
|
94
96
|
@lazy_load_streaming_timeout = lazy_load_streaming_timeout
|
|
95
97
|
@lazy_load_test_helpers = lazy_load_test_helpers
|
|
96
98
|
@skip_stale_tests = skip_stale_tests
|
|
99
|
+
@redis_ssl_verify = redis_ssl_verify
|
|
97
100
|
end
|
|
98
101
|
|
|
99
102
|
def lazy_load_test_helper_paths
|
data/lib/ci/queue/redis/base.rb
CHANGED
|
@@ -41,19 +41,33 @@ module CI
|
|
|
41
41
|
# it makes sense to retry for a while before giving up.
|
|
42
42
|
reconnect_attempts: reconnect_attempts,
|
|
43
43
|
middlewares: custom_middlewares,
|
|
44
|
-
|
|
45
|
-
# (because they do not own the domain they're running on such as compute-1.amazonaws.com)
|
|
46
|
-
# therefore a full SSL connection verification will fail.
|
|
47
|
-
# ci-queue should not contain any sensitive data, so we can just disable the verification.
|
|
48
|
-
ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE },
|
|
44
|
+
ssl_params: self.class.redis_ssl_params(ssl_verify: redis_ssl_verify?),
|
|
49
45
|
custom: custom_config,
|
|
50
46
|
timeout: DEFAULT_TIMEOUT,
|
|
51
47
|
)
|
|
52
48
|
else
|
|
53
|
-
@redis = ::Redis.new(
|
|
49
|
+
@redis = ::Redis.new(
|
|
50
|
+
url: redis_url,
|
|
51
|
+
timeout: DEFAULT_TIMEOUT,
|
|
52
|
+
ssl_params: self.class.redis_ssl_params(ssl_verify: redis_ssl_verify?),
|
|
53
|
+
)
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# TLS (`rediss://`) connections verify the server certificate and hostname
|
|
58
|
+
# by default. Some hosted Redis providers present self-signed certificates;
|
|
59
|
+
# those deployments can either trust the issuing CA (e.g. via `SSL_CERT_FILE`)
|
|
60
|
+
# or explicitly opt out of verification with `CI_QUEUE_REDIS_SSL_VERIFY=0`.
|
|
61
|
+
def self.redis_ssl_params(ssl_verify:)
|
|
62
|
+
ssl_verify ? {} : { verify_mode: OpenSSL::SSL::VERIFY_NONE }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def redis_ssl_verify?
|
|
66
|
+
return true unless @config.respond_to?(:redis_ssl_verify)
|
|
67
|
+
|
|
68
|
+
@config.redis_ssl_verify != false
|
|
69
|
+
end
|
|
70
|
+
|
|
57
71
|
def reconnect_attempts
|
|
58
72
|
return [] if ENV["CI_QUEUE_DISABLE_RECONNECT_ATTEMPTS"]
|
|
59
73
|
|
|
@@ -271,8 +285,9 @@ module CI
|
|
|
271
285
|
# on every heartbeat, which fires once per running test per worker.
|
|
272
286
|
TICK_COMMAND = 'tick!'.freeze
|
|
273
287
|
|
|
274
|
-
def initialize(redis_url, zset_key, owners_key, leases_key)
|
|
288
|
+
def initialize(redis_url, zset_key, owners_key, leases_key, ssl_verify: true)
|
|
275
289
|
@redis_url = redis_url
|
|
290
|
+
@ssl_verify = ssl_verify
|
|
276
291
|
@zset_key = zset_key
|
|
277
292
|
@owners_key = owners_key
|
|
278
293
|
@leases_key = leases_key
|
|
@@ -283,6 +298,7 @@ module CI
|
|
|
283
298
|
ready_pipe, child_write = IO.pipe
|
|
284
299
|
@pipe.binmode
|
|
285
300
|
@pid = Process.spawn(
|
|
301
|
+
{ 'CI_QUEUE_REDIS_SSL_VERIFY' => @ssl_verify ? '1' : '0' },
|
|
286
302
|
RbConfig.ruby,
|
|
287
303
|
::File.join(__dir__, "monitor.rb"),
|
|
288
304
|
@redis_url,
|
|
@@ -380,6 +396,7 @@ module CI
|
|
|
380
396
|
key('running'),
|
|
381
397
|
key('owners'),
|
|
382
398
|
key('leases'),
|
|
399
|
+
ssl_verify: redis_ssl_verify?,
|
|
383
400
|
)
|
|
384
401
|
end
|
|
385
402
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
5
|
require 'logger'
|
|
6
|
+
require 'openssl'
|
|
6
7
|
require 'redis'
|
|
7
8
|
require 'json'
|
|
8
9
|
|
|
@@ -18,7 +19,11 @@ module CI
|
|
|
18
19
|
@owners_key = owners_key
|
|
19
20
|
@leases_key = leases_key
|
|
20
21
|
@logger = logger
|
|
21
|
-
@redis = ::Redis.new(
|
|
22
|
+
@redis = ::Redis.new(
|
|
23
|
+
url: redis_url,
|
|
24
|
+
reconnect_attempts: [0, 0, 0.1, 0.5, 1, 3, 5],
|
|
25
|
+
ssl_params: ssl_params,
|
|
26
|
+
)
|
|
22
27
|
@shutdown = false
|
|
23
28
|
@pipe = pipe
|
|
24
29
|
@self_pipe_reader, @self_pipe_writer = IO.pipe
|
|
@@ -30,6 +35,17 @@ module CI
|
|
|
30
35
|
end
|
|
31
36
|
end
|
|
32
37
|
|
|
38
|
+
# Mirrors CI::Queue::Redis::Base.redis_ssl_params. This script runs as a
|
|
39
|
+
# standalone subprocess that doesn't load ci-queue or its Configuration,
|
|
40
|
+
# so the parent passes the setting through the environment.
|
|
41
|
+
def ssl_params
|
|
42
|
+
if %w(0 false).include?(ENV['CI_QUEUE_REDIS_SSL_VERIFY']&.strip&.downcase)
|
|
43
|
+
{ verify_mode: OpenSSL::SSL::VERIFY_NONE }
|
|
44
|
+
else
|
|
45
|
+
{}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
33
49
|
def soft_signal(sig)
|
|
34
50
|
@queue << sig
|
|
35
51
|
@self_pipe_writer << '.'
|
|
@@ -12,10 +12,30 @@ local leases_key = KEYS[9]
|
|
|
12
12
|
local max_requeues = tonumber(ARGV[1])
|
|
13
13
|
local global_max_requeues = tonumber(ARGV[2])
|
|
14
14
|
local entry = ARGV[3]
|
|
15
|
-
local offset = ARGV[4]
|
|
15
|
+
local offset = tonumber(ARGV[4]) or 0
|
|
16
16
|
local ttl = tonumber(ARGV[5])
|
|
17
17
|
local lease_id = ARGV[6]
|
|
18
18
|
|
|
19
|
+
-- Inserts `entry` behind the `offset` + 1 entries nearest the tail, the end RPOP reserves
|
|
20
|
+
-- from: where `LINSERT BEFORE <entry at index -(offset + 1)>` would put it. LINSERT finds its
|
|
21
|
+
-- pivot by scanning from the head, which is O(queue length) and blocks Redis on large queues;
|
|
22
|
+
-- this only touches the tail, so it is O(offset). Queues of at most `offset` + 1 entries, and
|
|
23
|
+
-- offsets of zero or less, push to the head instead.
|
|
24
|
+
-- Keep in sync with reserve.lua: the Python client does not resolve `-- @include`.
|
|
25
|
+
local function insert_with_offset(queue_key, entry, offset)
|
|
26
|
+
if offset <= 0 or redis.call('llen', queue_key) <= offset + 1 then
|
|
27
|
+
redis.call('lpush', queue_key, entry)
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
local ahead = redis.call('lrange', queue_key, -1 - offset, -1)
|
|
32
|
+
redis.call('ltrim', queue_key, 0, -2 - offset)
|
|
33
|
+
redis.call('rpush', queue_key, entry)
|
|
34
|
+
for _, ahead_entry in ipairs(ahead) do
|
|
35
|
+
redis.call('rpush', queue_key, ahead_entry)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
19
39
|
-- Only the current lease holder can requeue a test.
|
|
20
40
|
-- If the lease was transferred (e.g. via reserve_lost), reject the stale
|
|
21
41
|
-- worker's requeue so the running entry stays intact for the new holder.
|
|
@@ -42,12 +62,7 @@ redis.call('hincrby', requeues_count_key, entry, 1)
|
|
|
42
62
|
|
|
43
63
|
redis.call('hdel', error_reports_key, entry)
|
|
44
64
|
|
|
45
|
-
|
|
46
|
-
if pivot then
|
|
47
|
-
redis.call('linsert', queue_key, 'BEFORE', pivot, entry)
|
|
48
|
-
else
|
|
49
|
-
redis.call('lpush', queue_key, entry)
|
|
50
|
-
end
|
|
65
|
+
insert_with_offset(queue_key, entry, offset)
|
|
51
66
|
|
|
52
67
|
redis.call('hset', requeued_by_key, entry, worker_queue_key)
|
|
53
68
|
if ttl and ttl > 0 then
|
|
@@ -13,12 +13,23 @@ local current_time = ARGV[1]
|
|
|
13
13
|
local defer_offset = tonumber(ARGV[2]) or 0
|
|
14
14
|
local max_skip_attempts = 4
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
-- Inserts `entry` behind the `offset` + 1 entries nearest the tail, the end RPOP reserves
|
|
17
|
+
-- from: where `LINSERT BEFORE <entry at index -(offset + 1)>` would put it. LINSERT finds its
|
|
18
|
+
-- pivot by scanning from the head, which is O(queue length) and blocks Redis on large queues;
|
|
19
|
+
-- this only touches the tail, so it is O(offset). Queues of at most `offset` + 1 entries, and
|
|
20
|
+
-- offsets of zero or less, push to the head instead.
|
|
21
|
+
-- Keep in sync with requeue.lua: the Python client does not resolve `-- @include`.
|
|
22
|
+
local function insert_with_offset(queue_key, entry, offset)
|
|
23
|
+
if offset <= 0 or redis.call('llen', queue_key) <= offset + 1 then
|
|
24
|
+
redis.call('lpush', queue_key, entry)
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
local ahead = redis.call('lrange', queue_key, -1 - offset, -1)
|
|
29
|
+
redis.call('ltrim', queue_key, 0, -2 - offset)
|
|
30
|
+
redis.call('rpush', queue_key, entry)
|
|
31
|
+
for _, ahead_entry in ipairs(ahead) do
|
|
32
|
+
redis.call('rpush', queue_key, ahead_entry)
|
|
22
33
|
end
|
|
23
34
|
end
|
|
24
35
|
|
|
@@ -45,7 +56,7 @@ for attempt = 1, max_skip_attempts do
|
|
|
45
56
|
return claim_test(test)
|
|
46
57
|
end
|
|
47
58
|
|
|
48
|
-
insert_with_offset(test)
|
|
59
|
+
insert_with_offset(queue_key, test, defer_offset)
|
|
49
60
|
|
|
50
61
|
-- If this worker only finds its own requeued tests, defer once by returning nil,
|
|
51
62
|
-- then allow pickup on a subsequent reserve attempt.
|
data/lib/ci/queue/version.rb
CHANGED