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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12bdc1e58974c1c3fca314133800774ca993f23fd01440931443d080cc23ee6f
4
- data.tar.gz: 6e4f28118f8281ff3959f631e2e8b7c6fc8fbc47d0b1692f533e866edbfce916
3
+ metadata.gz: 612d0f2f3077fdd5b7e74b343649a162101c2961c8b4cda8329ca10fbd831e6b
4
+ data.tar.gz: bdd01c95971bcbfab5999b3754f8d019be35a7202e5f766d5df19191462e5563
5
5
  SHA512:
6
- metadata.gz: 88f858df9c99323f322479bd0f20f41d28137d45ee216a45f5f49317128bbfbe5312f5becee71ef559e03e0ea377ddf8e59e1e7ac177aa735b3c2b024e48f7e3
7
- data.tar.gz: e8738af5e5031e7fbe2c5118ea9c08c9b9e5943078d8b0ead60d6b72ca9c2cb29a3dfe847f03d83b8824fd7cd2e6b6b323a35e05904710bf9774626ebfdfb5f5
6
+ metadata.gz: 69a03983e12372fe0ab0258ec08262b2672624d49d2d63a0c35d0a89b4c8c85b3fd89ece2ce3c71de112652cd327868ee0fa7633ae283facb3b0bf57035d5724
7
+ data.tar.gz: 44ead0bb47b690b19652982496f8a48d698f5bb78ea4f88c01e56b7774d4c79f6b6015b50e91cb70e241fc00a13bf842a72828792c7f716644cacf12bfb8bb2d
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ci-queue (0.99.0)
4
+ ci-queue (0.101.0)
5
5
  logger
6
6
 
7
7
  GEM
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
- dir = Pathname.new(CI::Queue::RELEASE_SCRIPTS_ROOT).relative_path_from(Pathname.new(__dir__).realpath)
9
- lua_scripts = Dir[dir.join('*.lua').to_s]
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
@@ -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
- # Hosted Redis servers use self signed certificates
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(url: redis_url, timeout: DEFAULT_TIMEOUT)
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(url: redis_url, reconnect_attempts: [0, 0, 0.1, 0.5, 1, 3, 5])
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
- local pivot = redis.call('lrange', queue_key, -1 - offset, 0 - offset)[1]
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
- local function insert_with_offset(test)
17
- local pivot = redis.call('lrange', queue_key, -1 - defer_offset, 0 - defer_offset)[1]
18
- if pivot then
19
- redis.call('linsert', queue_key, 'BEFORE', pivot, test)
20
- else
21
- redis.call('lpush', queue_key, test)
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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  module CI
4
4
  module Queue
5
- VERSION = '0.99.0'
5
+ VERSION = '0.101.0'
6
6
  DEV_SCRIPTS_ROOT = ::File.expand_path('../../../../../redis', __FILE__)
7
7
  RELEASE_SCRIPTS_ROOT = ::File.expand_path('../redis', __FILE__)
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ci-queue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.99.0
4
+ version: 0.101.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier