solid_queue 1.1.0 → 1.2.1

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.
@@ -5,6 +5,7 @@ module ActiveJob
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  DEFAULT_CONCURRENCY_GROUP = ->(*) { self.class.name }
8
+ CONCURRENCY_ON_CONFLICT_BEHAVIOUR = %i[ block discard ]
8
9
 
9
10
  included do
10
11
  class_attribute :concurrency_key, instance_accessor: false
@@ -12,14 +13,16 @@ module ActiveJob
12
13
 
13
14
  class_attribute :concurrency_limit
14
15
  class_attribute :concurrency_duration, default: SolidQueue.default_concurrency_control_period
16
+ class_attribute :concurrency_on_conflict, default: :block
15
17
  end
16
18
 
17
19
  class_methods do
18
- def limits_concurrency(key:, to: 1, group: DEFAULT_CONCURRENCY_GROUP, duration: SolidQueue.default_concurrency_control_period)
20
+ def limits_concurrency(key:, to: 1, group: DEFAULT_CONCURRENCY_GROUP, duration: SolidQueue.default_concurrency_control_period, on_conflict: :block)
19
21
  self.concurrency_key = key
20
22
  self.concurrency_limit = to
21
23
  self.concurrency_group = group
22
24
  self.concurrency_duration = duration
25
+ self.concurrency_on_conflict = on_conflict.presence_in(CONCURRENCY_ON_CONFLICT_BEHAVIOUR) || :block
23
26
  end
24
27
  end
25
28
 
@@ -7,7 +7,10 @@ module ActiveJob
7
7
  # To use it set the queue_adapter config to +:solid_queue+.
8
8
  #
9
9
  # Rails.application.config.active_job.queue_adapter = :solid_queue
10
- class SolidQueueAdapter
10
+ class SolidQueueAdapter < (Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR == 1 ? Object : AbstractAdapter)
11
+ class_attribute :stopping, default: false, instance_writer: false
12
+ SolidQueue.on_worker_stop { self.stopping = true }
13
+
11
14
  def enqueue_after_transaction_commit?
12
15
  true
13
16
  end
@@ -1,10 +1,15 @@
1
- # production:
1
+ # examples:
2
2
  # periodic_cleanup:
3
3
  # class: CleanSoftDeletedRecordsJob
4
4
  # queue: background
5
5
  # args: [ 1000, { batch_size: 500 } ]
6
6
  # schedule: every hour
7
- # periodic_command:
7
+ # periodic_cleanup_with_command:
8
8
  # command: "SoftDeletedRecord.due.delete_all"
9
9
  # priority: 2
10
10
  # schedule: at 5am every day
11
+
12
+ production:
13
+ clear_solid_queue_finished_jobs:
14
+ command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)"
15
+ schedule: every hour at minute 12
@@ -4,7 +4,7 @@ module SolidQueue
4
4
  module AppExecutor
5
5
  def wrap_in_app_executor(&block)
6
6
  if SolidQueue.app_executor
7
- SolidQueue.app_executor.wrap(&block)
7
+ SolidQueue.app_executor.wrap(source: "application.solid_queue", &block)
8
8
  else
9
9
  yield
10
10
  end
@@ -13,7 +13,8 @@ module SolidQueue
13
13
  banner: "SOLID_QUEUE_RECURRING_SCHEDULE"
14
14
 
15
15
  class_option :skip_recurring, type: :boolean, default: false,
16
- desc: "Whether to skip recurring tasks scheduling"
16
+ desc: "Whether to skip recurring tasks scheduling",
17
+ banner: "SOLID_QUEUE_SKIP_RECURRING"
17
18
 
18
19
  def self.exit_on_failure?
19
20
  true
@@ -2,6 +2,12 @@
2
2
 
3
3
  module SolidQueue
4
4
  class Configuration
5
+ include ActiveModel::Model
6
+
7
+ validate :ensure_configured_processes
8
+ validate :ensure_valid_recurring_tasks
9
+ validate :ensure_correctly_sized_thread_pool
10
+
5
11
  class Process < Struct.new(:kind, :attributes)
6
12
  def instantiate
7
13
  "SolidQueue::#{kind.to_s.titleize}".safe_constantize.new(**attributes)
@@ -36,24 +42,60 @@ module SolidQueue
36
42
  end
37
43
  end
38
44
 
39
- def max_number_of_threads
40
- # At most "threads" in each worker + 1 thread for the worker + 1 thread for the heartbeat task
41
- workers_options.map { |options| options[:threads] }.max + 2
45
+ def error_messages
46
+ if configured_processes.none?
47
+ "No workers or processed configured. Exiting..."
48
+ else
49
+ error_messages = invalid_tasks.map do |task|
50
+ all_messages = task.errors.full_messages.map { |msg| "\t#{msg}" }.join("\n")
51
+ "#{task.key}:\n#{all_messages}"
52
+ end
53
+ .join("\n")
54
+
55
+ "Invalid processes configured:\n#{error_messages}"
56
+ end
42
57
  end
43
58
 
44
59
  private
45
60
  attr_reader :options
46
61
 
62
+ def ensure_configured_processes
63
+ unless configured_processes.any?
64
+ errors.add(:base, "No processes configured")
65
+ end
66
+ end
67
+
68
+ def ensure_valid_recurring_tasks
69
+ unless skip_recurring_tasks? || invalid_tasks.none?
70
+ error_messages = invalid_tasks.map do |task|
71
+ "- #{task.key}: #{task.errors.full_messages.join(", ")}"
72
+ end
73
+
74
+ errors.add(:base, "Invalid recurring tasks:\n#{error_messages.join("\n")}")
75
+ end
76
+ end
77
+
78
+ def ensure_correctly_sized_thread_pool
79
+ if (db_pool_size = SolidQueue::Record.connection_pool&.size) && db_pool_size < estimated_number_of_threads
80
+ errors.add(:base, "Solid Queue is configured to use #{estimated_number_of_threads} threads but the " +
81
+ "database connection pool is #{db_pool_size}. Increase it in `config/database.yml`")
82
+ end
83
+ end
84
+
47
85
  def default_options
48
86
  {
49
87
  config_file: Rails.root.join(ENV["SOLID_QUEUE_CONFIG"] || DEFAULT_CONFIG_FILE_PATH),
50
88
  recurring_schedule_file: Rails.root.join(ENV["SOLID_QUEUE_RECURRING_SCHEDULE"] || DEFAULT_RECURRING_SCHEDULE_FILE_PATH),
51
89
  only_work: false,
52
90
  only_dispatch: false,
53
- skip_recurring: false
91
+ skip_recurring: ActiveModel::Type::Boolean.new.cast(ENV["SOLID_QUEUE_SKIP_RECURRING"])
54
92
  }
55
93
  end
56
94
 
95
+ def invalid_tasks
96
+ recurring_tasks.select(&:invalid?)
97
+ end
98
+
57
99
  def only_work?
58
100
  options[:only_work]
59
101
  end
@@ -99,8 +141,8 @@ module SolidQueue
99
141
 
100
142
  def recurring_tasks
101
143
  @recurring_tasks ||= recurring_tasks_config.map do |id, options|
102
- RecurringTask.from_configuration(id, **options)
103
- end.select(&:valid?)
144
+ RecurringTask.from_configuration(id, **options) if options&.has_key?(:schedule)
145
+ end.compact
104
146
  end
105
147
 
106
148
  def processes_config
@@ -111,7 +153,9 @@ module SolidQueue
111
153
  end
112
154
 
113
155
  def recurring_tasks_config
114
- @recurring_tasks_config ||= config_from options[:recurring_schedule_file]
156
+ @recurring_tasks_config ||= begin
157
+ config_from options[:recurring_schedule_file]
158
+ end
115
159
  end
116
160
 
117
161
 
@@ -147,5 +191,11 @@ module SolidQueue
147
191
  {}
148
192
  end
149
193
  end
194
+
195
+ def estimated_number_of_threads
196
+ # At most "threads" in each worker + 1 thread for the worker + 1 thread for the heartbeat task
197
+ thread_count = workers_options.map { |options| options.fetch(:threads, WORKER_DEFAULTS[:threads]) }.max
198
+ (thread_count || 1) + 2
199
+ end
150
200
  end
151
201
  end
@@ -2,10 +2,14 @@
2
2
 
3
3
  module SolidQueue
4
4
  class Dispatcher < Processes::Poller
5
- attr_accessor :batch_size, :concurrency_maintenance
5
+ include LifecycleHooks
6
+ attr_reader :batch_size
6
7
 
8
+ after_boot :run_start_hooks
7
9
  after_boot :start_concurrency_maintenance
8
10
  before_shutdown :stop_concurrency_maintenance
11
+ before_shutdown :run_stop_hooks
12
+ after_shutdown :run_exit_hooks
9
13
 
10
14
  def initialize(**options)
11
15
  options = options.dup.with_defaults(SolidQueue::Configuration::DISPATCHER_DEFAULTS)
@@ -22,9 +26,12 @@ module SolidQueue
22
26
  end
23
27
 
24
28
  private
29
+ attr_reader :concurrency_maintenance
30
+
25
31
  def poll
26
32
  batch = dispatch_next_batch
27
- batch.size
33
+
34
+ batch.zero? ? polling_interval : 0.seconds
28
35
  end
29
36
 
30
37
  def dispatch_next_batch
@@ -37,20 +44,12 @@ module SolidQueue
37
44
  concurrency_maintenance&.start
38
45
  end
39
46
 
40
- def schedule_recurring_tasks
41
- recurring_schedule.schedule_tasks
42
- end
43
-
44
47
  def stop_concurrency_maintenance
45
48
  concurrency_maintenance&.stop
46
49
  end
47
50
 
48
- def unschedule_recurring_tasks
49
- recurring_schedule.unschedule_tasks
50
- end
51
-
52
51
  def all_work_completed?
53
- SolidQueue::ScheduledExecution.none? && recurring_schedule.empty?
52
+ SolidQueue::ScheduledExecution.none?
54
53
  end
55
54
 
56
55
  def set_procline
@@ -5,7 +5,7 @@ module SolidQueue
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- mattr_reader :lifecycle_hooks, default: { start: [], stop: [] }
8
+ mattr_reader :lifecycle_hooks, default: { start: [], stop: [], exit: [] }
9
9
  end
10
10
 
11
11
  class_methods do
@@ -17,7 +17,12 @@ module SolidQueue
17
17
  self.lifecycle_hooks[:stop] << block
18
18
  end
19
19
 
20
+ def on_exit(&block)
21
+ self.lifecycle_hooks[:exit] << block
22
+ end
23
+
20
24
  def clear_hooks
25
+ self.lifecycle_hooks[:exit] = []
21
26
  self.lifecycle_hooks[:start] = []
22
27
  self.lifecycle_hooks[:stop] = []
23
28
  end
@@ -32,9 +37,13 @@ module SolidQueue
32
37
  run_hooks_for :stop
33
38
  end
34
39
 
40
+ def run_exit_hooks
41
+ run_hooks_for :exit
42
+ end
43
+
35
44
  def run_hooks_for(event)
36
45
  self.class.lifecycle_hooks.fetch(event, []).each do |block|
37
- block.call
46
+ block.call(self)
38
47
  rescue Exception => exception
39
48
  handle_thread_error(exception)
40
49
  end
@@ -145,6 +145,7 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
145
145
  end
146
146
 
147
147
  def replace_fork(event)
148
+ supervisor_pid = event.payload[:supervisor_pid]
148
149
  status = event.payload[:status]
149
150
  attributes = event.payload.slice(:pid).merge \
150
151
  status: (status.exitstatus || "no exit status set"),
@@ -155,7 +156,7 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
155
156
 
156
157
  if replaced_fork = event.payload[:fork]
157
158
  info formatted_event(event, action: "Replaced terminated #{replaced_fork.kind}", **attributes.merge(hostname: replaced_fork.hostname, name: replaced_fork.name))
158
- else
159
+ elsif supervisor_pid != 1 # Running Docker, possibly having some processes that have been reparented
159
160
  warn formatted_event(event, action: "Tried to replace forked process but it had already died", **attributes)
160
161
  end
161
162
  end
@@ -18,20 +18,16 @@ module SolidQueue
18
18
  def post(execution)
19
19
  available_threads.decrement
20
20
 
21
- future = Concurrent::Future.new(args: [ execution ], executor: executor) do |thread_execution|
21
+ Concurrent::Promises.future_on(executor, execution) do |thread_execution|
22
22
  wrap_in_app_executor do
23
23
  thread_execution.perform
24
24
  ensure
25
25
  available_threads.increment
26
26
  mutex.synchronize { on_idle.try(:call) if idle? }
27
27
  end
28
+ end.on_rejection! do |e|
29
+ handle_thread_error(e)
28
30
  end
29
-
30
- future.add_observer do |_, _, error|
31
- handle_thread_error(error) if error
32
- end
33
-
34
- future.execute
35
31
  end
36
32
 
37
33
  def idle_threads
@@ -4,7 +4,8 @@ module SolidQueue
4
4
  module Processes
5
5
  class Base
6
6
  include Callbacks # Defines callbacks needed by other concerns
7
- include AppExecutor, Registrable, Interruptible, Procline
7
+ include AppExecutor, Registrable, Procline
8
+ prepend Interruptible
8
9
 
9
10
  attr_reader :name
10
11
 
@@ -2,28 +2,39 @@
2
2
 
3
3
  module SolidQueue::Processes
4
4
  module Interruptible
5
+ def initialize(...)
6
+ super
7
+ @self_pipe = create_self_pipe
8
+ end
9
+
5
10
  def wake_up
6
11
  interrupt
7
12
  end
8
13
 
9
14
  private
15
+ SELF_PIPE_BLOCK_SIZE = 11
16
+
17
+ attr_reader :self_pipe
10
18
 
11
19
  def interrupt
12
- queue << true
20
+ self_pipe[:writer].write_nonblock(".")
21
+ rescue Errno::EAGAIN, Errno::EINTR
22
+ # Ignore writes that would block and retry
23
+ # if another signal arrived while writing
24
+ retry
13
25
  end
14
26
 
15
27
  def interruptible_sleep(time)
16
- # Invoking from the main thread can result in a 35% slowdown (at least when running the test suite).
17
- # Using some form of Async (Futures) addresses this performance issue.
18
- Concurrent::Promises.future(time) do |timeout|
19
- if timeout > 0 && queue.pop(timeout:)
20
- queue.clear
21
- end
22
- end.value
28
+ if time > 0 && self_pipe[:reader].wait_readable(time)
29
+ loop { self_pipe[:reader].read_nonblock(SELF_PIPE_BLOCK_SIZE) }
30
+ end
31
+ rescue Errno::EAGAIN, Errno::EINTR, IO::EWOULDBLOCKWaitReadable
23
32
  end
24
33
 
25
- def queue
26
- @queue ||= Queue.new
34
+ # Self-pipe for signal-handling (http://cr.yp.to/docs/selfpipe.html)
35
+ def create_self_pipe
36
+ reader, writer = IO.pipe
37
+ { reader: reader, writer: writer }
27
38
  end
28
39
  end
29
40
  end
@@ -25,11 +25,11 @@ module SolidQueue::Processes
25
25
  loop do
26
26
  break if shutting_down?
27
27
 
28
- wrap_in_app_executor do
29
- unless poll > 0
30
- interruptible_sleep(polling_interval)
31
- end
28
+ delay = wrap_in_app_executor do
29
+ poll
32
30
  end
31
+
32
+ interruptible_sleep(delay)
33
33
  end
34
34
  ensure
35
35
  SolidQueue.instrument(:shutdown_process, process: self) do
@@ -4,7 +4,7 @@ module SolidQueue
4
4
  module Processes
5
5
  class ProcessPrunedError < RuntimeError
6
6
  def initialize(last_heartbeat_at)
7
- super("Process was found dead and pruned (last heartbeat at: #{last_heartbeat_at}")
7
+ super("Process was found dead and pruned (last heartbeat at: #{last_heartbeat_at})")
8
8
  end
9
9
  end
10
10
  end
@@ -7,8 +7,7 @@ module SolidQueue::Processes
7
7
  included do
8
8
  after_boot :register, :launch_heartbeat
9
9
 
10
- before_shutdown :stop_heartbeat
11
- after_shutdown :deregister
10
+ after_shutdown :stop_heartbeat, :deregister
12
11
  end
13
12
 
14
13
  def process_id
@@ -3,11 +3,15 @@
3
3
  module SolidQueue
4
4
  class Scheduler < Processes::Base
5
5
  include Processes::Runnable
6
+ include LifecycleHooks
6
7
 
7
- attr_accessor :recurring_schedule
8
+ attr_reader :recurring_schedule
8
9
 
10
+ after_boot :run_start_hooks
9
11
  after_boot :schedule_recurring_tasks
10
12
  before_shutdown :unschedule_recurring_tasks
13
+ before_shutdown :run_stop_hooks
14
+ after_shutdown :run_exit_hooks
11
15
 
12
16
  def initialize(recurring_tasks:, **options)
13
17
  @recurring_schedule = RecurringSchedule.new(recurring_tasks)
@@ -5,15 +5,17 @@ module SolidQueue
5
5
  include LifecycleHooks
6
6
  include Maintenance, Signals, Pidfiled
7
7
 
8
+ after_shutdown :run_exit_hooks
9
+
8
10
  class << self
9
11
  def start(**options)
10
12
  SolidQueue.supervisor = true
11
13
  configuration = Configuration.new(**options)
12
14
 
13
- if configuration.configured_processes.any?
15
+ if configuration.valid?
14
16
  new(configuration).tap(&:start)
15
17
  else
16
- abort "No workers or processed configured. Exiting..."
18
+ abort configuration.errors.full_messages.join("\n") + "\nExiting..."
17
19
  end
18
20
  end
19
21
  end
@@ -170,8 +172,11 @@ module SolidQueue
170
172
  end
171
173
  end
172
174
 
175
+ # When a supervised fork crashes or exits we need to mark all the
176
+ # executions it had claimed as failed so that they can be retried
177
+ # by some other worker.
173
178
  def handle_claimed_jobs_by(terminated_fork, status)
174
- if registered_process = process.supervisees.find_by(name: terminated_fork.name)
179
+ if registered_process = SolidQueue::Process.find_by(name: terminated_fork.name)
175
180
  error = Processes::ProcessExitError.new(status)
176
181
  registered_process.fail_all_claimed_executions_with(error)
177
182
  end
@@ -1,3 +1,3 @@
1
1
  module SolidQueue
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.1"
3
3
  end
@@ -6,13 +6,16 @@ module SolidQueue
6
6
 
7
7
  after_boot :run_start_hooks
8
8
  before_shutdown :run_stop_hooks
9
+ after_shutdown :run_exit_hooks
9
10
 
10
- attr_accessor :queues, :pool
11
+ attr_reader :queues, :pool
11
12
 
12
13
  def initialize(**options)
13
14
  options = options.dup.with_defaults(SolidQueue::Configuration::WORKER_DEFAULTS)
14
15
 
15
- @queues = Array(options[:queues])
16
+ # Ensure that the queues array is deep frozen to prevent accidental modification
17
+ @queues = Array(options[:queues]).map(&:freeze).freeze
18
+
16
19
  @pool = Pool.new(options[:threads], on_idle: -> { wake_up })
17
20
 
18
21
  super(**options)
@@ -29,7 +32,7 @@ module SolidQueue
29
32
  pool.post(execution)
30
33
  end
31
34
 
32
- executions.size
35
+ pool.idle? ? polling_interval : 10.minutes
33
36
  end
34
37
  end
35
38
 
data/lib/solid_queue.rb CHANGED
@@ -41,14 +41,20 @@ module SolidQueue
41
41
  mattr_accessor :clear_finished_jobs_after, default: 1.day
42
42
  mattr_accessor :default_concurrency_control_period, default: 3.minutes
43
43
 
44
- delegate :on_start, :on_stop, to: Supervisor
44
+ delegate :on_start, :on_stop, :on_exit, to: Supervisor
45
45
 
46
- def on_worker_start(...)
47
- Worker.on_start(...)
48
- end
46
+ [ Dispatcher, Scheduler, Worker ].each do |process|
47
+ define_singleton_method(:"on_#{process.name.demodulize.downcase}_start") do |&block|
48
+ process.on_start(&block)
49
+ end
50
+
51
+ define_singleton_method(:"on_#{process.name.demodulize.downcase}_stop") do |&block|
52
+ process.on_stop(&block)
53
+ end
49
54
 
50
- def on_worker_stop(...)
51
- Worker.on_stop(...)
55
+ define_singleton_method(:"on_#{process.name.demodulize.downcase}_exit") do |&block|
56
+ process.on_exit(&block)
57
+ end
52
58
  end
53
59
 
54
60
  def supervisor?
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_queue
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rosa Gutierrez
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-05 00:00:00.000000000 Z
11
+ date: 2025-07-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -84,16 +84,30 @@ dependencies:
84
84
  name: thor
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - "~>"
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
89
  version: 1.3.1
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - "~>"
94
+ - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: 1.3.1
97
+ - !ruby/object:Gem::Dependency
98
+ name: appraisal
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: debug
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -220,6 +234,20 @@ dependencies:
220
234
  - - ">="
221
235
  - !ruby/object:Gem::Version
222
236
  version: '0'
237
+ - !ruby/object:Gem::Dependency
238
+ name: zeitwerk
239
+ requirement: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - '='
242
+ - !ruby/object:Gem::Version
243
+ version: 2.6.0
244
+ type: :development
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - '='
249
+ - !ruby/object:Gem::Version
250
+ version: 2.6.0
223
251
  description: Database-backed Active Job backend.
224
252
  email:
225
253
  - rosa@37signals.com
@@ -307,15 +335,8 @@ metadata:
307
335
  homepage_uri: https://github.com/rails/solid_queue
308
336
  source_code_uri: https://github.com/rails/solid_queue
309
337
  post_install_message: |
310
- Upgrading to Solid Queue 0.9.0? There are some breaking changes about how recurring tasks are configured.
311
-
312
- Upgrading to Solid Queue 0.8.0 from < 0.6.0? You need to upgrade to 0.6.0 first.
313
-
314
- Upgrading to Solid Queue 0.4.x, 0.5.x, 0.6.x or 0.7.x? There are some breaking changes about how Solid Queue is started,
315
- configuration and new migrations.
316
-
317
- --> Check https://github.com/rails/solid_queue/blob/main/UPGRADING.md
318
- for upgrade instructions.
338
+ Upgrading from Solid Queue < 1.0? Check details on breaking changes and upgrade instructions
339
+ --> https://github.com/rails/solid_queue/blob/main/UPGRADING.md
319
340
  rdoc_options: []
320
341
  require_paths:
321
342
  - lib
@@ -323,7 +344,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
323
344
  requirements:
324
345
  - - ">="
325
346
  - !ruby/object:Gem::Version
326
- version: '0'
347
+ version: '3.1'
327
348
  required_rubygems_version: !ruby/object:Gem::Requirement
328
349
  requirements:
329
350
  - - ">="