workhorse 1.2.15 → 1.2.17.rc0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 772b634a73940718f57f2363859977dbc7f20ff4b7c12edada5c4cc499ff5f6c
4
- data.tar.gz: 8c44df6872578300e6577155626f851ae1cb9202653f835b4dabbcc70fd78c70
3
+ metadata.gz: 25908f294cd9623ebde44ec8bba5181ee54965703f4fb93df18876b216718eb8
4
+ data.tar.gz: 568e620571a9ded165fee1fc4420efbb6810f073edd4365c6c8efbe7c3676052
5
5
  SHA512:
6
- metadata.gz: 751c63f5b1d86238d3e403332972ea0d177ee6c92acab793f3477d9c68f8223d755c633140e2622c2979b404f369327b6e507303f0d2532e1bcb408edf4a02f6
7
- data.tar.gz: e5209a6b1472239ce48734eb99d0a1c7cfcd9c20d800c6bb5bb77dff001f84ce7b362b444d0d2d3b0ae2b92b035f75d59484df5dc9d4111c7816ad10aa1b17ef
6
+ metadata.gz: 83b6ac441e3762f251e35f04dd213044d6fb904a005155235546391cdecd4f9c0c1db24be7c30371da4704b27e6f18aa6ce0b448b6c6c1dbc8c77eeb81faa9e3
7
+ data.tar.gz: 9c2940a6bfb7cd0be49170d81af2378f27395dbf597f6d8fa2c777bdc812b26d06cc849454ba5925fcf5f26b7724779b3d6e941bc6c9237d6dc251b7d071188f
@@ -31,6 +31,11 @@ jobs:
31
31
  sudo /etc/init.d/mysql start
32
32
  mysql -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} -e 'CREATE DATABASE ${{ env.DB_DATABASE }};'
33
33
  - name: Run rake tests
34
- run: bundle exec rake test TESTOPTS='--verbose'
34
+ uses: nick-fields/retry@v2
35
+ with:
36
+ timeout_seconds: 120
37
+ retry_on: error
38
+ max_attempts: 3
39
+ command: bundle exec rake test TESTOPTS='--verbose'
35
40
  - name: Run rubocop
36
41
  run: bundle exec rubocop
data/.rubocop.yml CHANGED
@@ -1,81 +1,158 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.3
3
-
2
+ DisplayCopNames: true
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+ TargetRubyVersion: 2.5
4
6
  Exclude:
7
+ - 'local/**/*'
5
8
  - 'vendor/**/*'
6
9
  - 'tmp/**/*'
10
+ - 'target/**/*'
7
11
  - 'log/**/*'
12
+ - 'db/schema.rb'
13
+ - 'locale/translations.rb'
14
+ - 'config/initializers/assets.rb'
15
+ - 'config/puma.rb'
16
+ - 'config_scripts/release_notes'
17
+ - 'config/spring.rb'
18
+ - 'bin/yarn'
8
19
  - '*.gemspec'
9
20
 
10
- DisplayCopNames: true
21
+ # Make sure accessors are on separate lines for diff readability.
22
+ Style/AccessorGrouping:
23
+ EnforcedStyle: separated
24
+
25
+ # Cop would break a lot of existing code.
26
+ Style/OptionalBooleanParameter:
27
+ Enabled: false
28
+
29
+ # Multiline hashes should be aligned cleanly as a table to improve readability.
30
+ Layout/HashAlignment:
31
+ EnforcedHashRocketStyle: table
32
+ EnforcedColonStyle: table
11
33
 
34
+ # Template style is easier on the eyes.
35
+ Style/FormatStringToken:
36
+ EnforcedStyle: template
37
+
38
+ # file. This will be addressed when approaching the first ruby 3 application.
12
39
  Style/FrozenStringLiteralComment:
13
40
  Enabled: false
14
41
 
42
+ # Double negation is very useful to make sure you have a boolean in hand. Use it
43
+ # wisely though and know what you're doing.
15
44
  Style/DoubleNegation:
16
45
  Enabled: false
17
46
 
47
+ # Depending on the case, [].include? can be a lot harder to read and less
48
+ # expressive than multiple comparisons.
49
+ Style/MultipleComparison:
50
+ Enabled: false
51
+
52
+ # Over time, the ruby guide changed from raise to fail back to raise. Both fail
53
+ # and raise are programatically exactly the same and our decision fell to "fail"
54
+ # for all kinds of exceptions.
18
55
  Style/SignalException:
19
56
  EnforcedStyle: only_fail
20
57
 
21
- Lint/RescueWithoutErrorClass:
22
- Enabled: False
23
-
24
- Lint/RescueException:
25
- Enabled: False
26
-
58
+ # Enforced styles can sometimes be hard to read.
27
59
  Style/ConditionalAssignment:
28
60
  Enabled: false
29
61
 
30
- Layout/IndentArray:
62
+ # Enforce consistent array indentation.
63
+ Layout/FirstArrayElementIndentation:
31
64
  EnforcedStyle: consistent
32
65
 
66
+ # Disable layout cop because methods just consisting of a number of returns
67
+ # would look very odd with an extra empty line between each return.
68
+ Layout/EmptyLineAfterGuardClause:
69
+ Enabled: false
70
+
71
+ # While you should try to keep your code as expressive and short as possible,
72
+ # limitting lengths hardly is over the top.
33
73
  Metrics/MethodLength:
34
74
  Enabled: false
35
75
 
76
+ # While you should try to keep your code as expressive and short as possible,
77
+ # limitting lengths hardly is over the top.
36
78
  Metrics/ClassLength:
37
79
  Enabled: false
38
80
 
81
+ # While you should try to keep your code as expressive and short as possible,
82
+ # limitting lengths hardly is over the top.
39
83
  Metrics/ModuleLength:
40
84
  Enabled: false
41
85
 
86
+ # While you should try to keep your code as expressive and short as possible,
87
+ # limitting lengths hardly is over the top.
88
+ Metrics/BlockLength:
89
+ Enabled: false
90
+
91
+ # While not always desirable, it can be useful to have a lot of keyword
92
+ # arguments on certain methods. Try to avoid it though.
42
93
  Metrics/ParameterLists:
43
94
  Max: 5
44
95
  CountKeywordArgs: false
45
96
 
97
+ # The results of this cop sometimes seemed arbitrary and can signifficantly
98
+ # restrict certain styles of coding.
46
99
  Metrics/AbcSize:
47
100
  Enabled: False
48
101
 
102
+ # The results of this cop sometimes seemed arbitrary and can signifficantly
103
+ # restrict certain styles of coding.
49
104
  Metrics/CyclomaticComplexity:
50
105
  Enabled: False
51
106
 
107
+ # The results of this cop sometimes seemed arbitrary and can signifficantly
108
+ # restrict certain styles of coding.
52
109
  Metrics/PerceivedComplexity:
53
110
  Enabled: False
54
111
 
55
- Metrics/LineLength:
56
- Max: 160
57
-
112
+ # In certain cases, "excessive" block nesting might just be useful. Try to keep
113
+ # this down as much as possible though.
58
114
  Metrics/BlockNesting:
59
115
  Enabled: false
60
116
 
61
- Metrics/BlockLength:
62
- Enabled: false
117
+ # A line length of 80 is not considered to be temporary anymore. That's why line
118
+ # length is doubled to 160. If absolutely necessary, create a temporary rubocop
119
+ # exclusion for the lines in question.
120
+ Layout/LineLength:
121
+ Max: 160
63
122
 
123
+ # Prefer variable_1 over variable1 for aesthetic reasons. Do not check symbols,
124
+ # as they often need to be another case for use in external palces (e.g. :md5).
125
+ Naming/VariableNumber:
126
+ EnforcedStyle: snake_case
127
+ CheckSymbols: false
128
+
129
+ # Depending on the surrounding code, even simple if/unless clauses may be more
130
+ # descriptive when on multiple lines.
64
131
  Style/IfUnlessModifier:
65
132
  Enabled: false
66
133
 
134
+ # In most cases, timing does not allow documenting each and every bit of source
135
+ # code. Do not hesitate to enable this cop otherwise.
67
136
  Style/Documentation:
68
137
  Enabled: false
69
138
 
139
+ # Return should be used whenever there is more than one statement or line in a
140
+ # method. This helps avoiding programming mistakes. This is not enforced yet as
141
+ # this would require a custom cop. However, to allow this style of programming,
142
+ # the RedundantReturn cop needs to be disabled.
70
143
  Style/RedundantReturn:
71
144
  Enabled: false
72
145
 
146
+ # Non-ascii comments can be useful sometimes.
73
147
  Style/AsciiComments:
74
148
  Enabled: false
75
149
 
150
+ # Depending on the case, if/unless can be more descriptive than guard clauses.
76
151
  Style/GuardClause:
77
152
  Enabled: false
78
153
 
154
+ # For technical reasons, nested and compact styles must be mixed in certain
155
+ # applications.
79
156
  Style/ClassAndModuleChildren:
80
157
  Enabled: false
81
158
  EnforcedStyle: compact
@@ -83,8 +160,30 @@ Style/ClassAndModuleChildren:
83
160
  - nested
84
161
  - compact
85
162
 
163
+ # Depending on the case, it may be more descriptive to use i.e. == 0 instead of
164
+ # .zero?, especially when testing against multiple numbers.
86
165
  Style/NumericPredicate:
87
166
  Enabled: false
88
167
 
89
- Layout/IndentHeredoc:
168
+ # Detection is not implemented in a reliable manner for all cases which can lead
169
+ # to false positives and negatives.
170
+ Style/FormatString:
171
+ Enabled: false
172
+
173
+ # Do not require MFA, as gems checked with sitrox_standards are only pushed to the
174
+ # internal repo
175
+ Gemspec/RequireMFA:
176
+ Enabled: false
177
+
178
+ # Use explicit style
179
+ Naming/BlockForwarding:
180
+ Enabled: true
181
+ EnforcedStyle: explicit
182
+
183
+ Style/HashSyntax:
184
+ # Use `either` style for `EnforcedShorthandyntax` (see #106550)
185
+ EnforcedShorthandSyntax: either
186
+
187
+ # Allow rescue 'Exception', necessary for Workhorse
188
+ Lint/RescueException:
90
189
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Workhorse Changelog
2
2
 
3
+ ## 1.2.17.rc0 - 2024-02-05
4
+
5
+ * Add option `config.max_worker_memory_mb` for automatic restart of workers
6
+ exceeding the specified memory threshold using the `watch` command. Default is
7
+ `0`, deactivating this feature. See [memory
8
+ handling](README.md#memory-handling) for more information.
9
+
10
+ Sitrox reference: #121312.
11
+
12
+ ## 1.2.16 - 2023-09-18
13
+
14
+ * Add support for `--skip-initializer` flag to install generator.
15
+
16
+ Sitrox reference: #114673.
17
+
18
+ * Add option `config.clean_stuck_jobs` that enabled automatic cleaning of stuck
19
+ jobs whenever a worker starts up.
20
+
21
+ Sitrox reference: #113708
22
+
23
+ * Add `retry-step` to actions such that failed unit tests are executed again
24
+
25
+ Sitrox reference: #115888
26
+
3
27
  ## 1.2.15 - 2023-08-28
4
28
 
5
29
  * Add capability to skip transactions for enqueued RailsOps operations.
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2017 - 2023 Sitrox
3
+ Copyright (c) 2017 - 2024 Sitrox
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -62,6 +62,7 @@ What it does not do:
62
62
 
63
63
  * A database migration for creating a table named `jobs`
64
64
  * The initializer `config/initializers/workhorse.rb` for global configuration
65
+ * This can be skipped using the `--skip-initializer` flag
65
66
  * The daemon worker script `bin/workhorse.rb`
66
67
 
67
68
  Please customize the initializer and worker script to your liking.
@@ -463,6 +464,29 @@ jobs database on a regular interval. Workhorse provides the job
463
464
  `Workhorse::Jobs::CleanupSucceededJobs` for this purpose that cleans up all
464
465
  succeeded jobs. You can run this using your scheduler in a specific interval.
465
466
 
467
+ ## Memory handling
468
+
469
+ When dealing with jobs that may exhibit a large memory footprint, it's important
470
+ to note that Ruby might not release consumed memory back to the operating
471
+ system. Consequently, your job workers could accumulate a significant amount of
472
+ memory over time. To address this, Workhorse provides the
473
+ `config.max_worker_memory_mb` option.
474
+
475
+ If `config.max_worker_memory_mb` is set to a value above `0`, the `watch`
476
+ command will check the memory footprint (RSS / resident size) of all worker
477
+ processes. If any worker exceeds the specified footprint, Workhorse will
478
+ silently restart it to ensure proper memory release. This process does not
479
+ produce any output in the `watch` command.
480
+
481
+ Example configuration:
482
+
483
+ ```ruby
484
+ # config/initializers/workhorse.rb
485
+ Workhorse.setup do |config|
486
+ config.max_worker_memory_mb = 512 # Set the memory threshold to 512 megabytes
487
+ end
488
+ ```
489
+
466
490
  ## Load hooks
467
491
 
468
492
  Using the load hook `:workhorse_db_job`, you can inject custom code into the
@@ -527,10 +551,16 @@ exception is thrown (which may cause a notification if you configured
527
551
  `on_exception` accordingly). See the job's API documentation for more
528
552
  information.
529
553
 
554
+ Starting with Workhorse 1.2.16, there is also a feature that automatically
555
+ checks for stuck jobs (jobs in state `locked` or `started` running on the same
556
+ host where the corresponding PID does not have a process anymore) when starting
557
+ up the worker / poller. This feature can be turned on using the setting
558
+ `config.clean_stuck_jobs`. This is turned off by default.
559
+
530
560
  ## Frequently asked questions
531
561
 
532
562
  Please consult the [FAQ](FAQ.md).
533
563
 
534
564
  ## Copyright
535
565
 
536
- Copyright © 2017 - 2023 Sitrox. See `LICENSE` for further details.
566
+ Copyright © 2017 - 2024 Sitrox. See `LICENSE` for further details.
data/RUBY_VERSION CHANGED
@@ -1 +1 @@
1
- ruby-2.6.2-p47
1
+ ruby-3.2.1-p31
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  task :gemspec do
2
2
  gemspec = Gem::Specification.new do |spec|
3
3
  spec.name = 'workhorse'
4
- spec.version = IO.read('VERSION').chomp
4
+ spec.version = File.read('VERSION').chomp
5
5
  spec.authors = ['Sitrox']
6
6
  spec.summary = %(
7
7
  Multi-threaded job backend with database queuing for ruby.
@@ -13,7 +13,7 @@ task :gemspec do
13
13
 
14
14
  spec.add_development_dependency 'bundler'
15
15
  spec.add_development_dependency 'rake'
16
- spec.add_development_dependency 'rubocop', '0.51.0'
16
+ spec.add_development_dependency 'rubocop', '~> 1.28.0' # Latest version supported with Ruby 2.5
17
17
  spec.add_development_dependency 'minitest'
18
18
  spec.add_development_dependency 'mysql2'
19
19
  spec.add_development_dependency 'colorize'
@@ -25,7 +25,7 @@ task :gemspec do
25
25
  spec.add_dependency 'concurrent-ruby'
26
26
  end
27
27
 
28
- File.open('workhorse.gemspec', 'w') { |f| f.write(gemspec.to_ruby.strip) }
28
+ File.write('workhorse.gemspec', gemspec.to_ruby.strip)
29
29
  end
30
30
 
31
31
  require 'rake/testtask'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.15
1
+ 1.2.17.rc0
@@ -10,11 +10,11 @@ module ActiveJob
10
10
  #
11
11
  # Rails.application.config.active_job.queue_adapter = :workhorse
12
12
  class WorkhorseAdapter
13
- def enqueue(job) #:nodoc:
13
+ def enqueue(job) # :nodoc:
14
14
  Workhorse.enqueue_active_job(job)
15
15
  end
16
16
 
17
- def enqueue_at(job, timestamp = Time.now) #:nodoc:
17
+ def enqueue_at(job, timestamp = Time.now) # :nodoc:
18
18
  Workhorse.enqueue_active_job(job, perform_at: timestamp)
19
19
  end
20
20
  end
@@ -2,7 +2,10 @@ module Workhorse
2
2
  class InstallGenerator < Rails::Generators::Base
3
3
  include Rails::Generators::Migration
4
4
 
5
- source_root File.expand_path('../templates', __FILE__)
5
+ class_option :skip_initializer, type: :boolean, default: false,
6
+ desc: 'Skip generating the initializer file'
7
+
8
+ source_root File.expand_path('templates', __dir__)
6
9
 
7
10
  def self.next_migration_number(_dir)
8
11
  Time.now.utc.strftime('%Y%m%d%H%M%S')
@@ -18,7 +21,7 @@ module Workhorse
18
21
  end
19
22
 
20
23
  def install_initializer
21
- template 'config/initializers/workhorse.rb'
24
+ template 'config/initializers/workhorse.rb' unless options[:skip_initializer]
22
25
  end
23
26
  end
24
27
  end
@@ -40,7 +40,7 @@ module Workhorse
40
40
  end
41
41
 
42
42
  exit 0
43
- rescue => e
43
+ rescue StandardError => e
44
44
  warn "#{e.message}\n#{e.backtrace.join("\n")}"
45
45
  exit 99
46
46
  ensure
@@ -49,44 +49,44 @@ module Workhorse
49
49
  end
50
50
 
51
51
  def self.usage
52
- warn <<USAGE
53
- Usage: #{$PROGRAM_NAME} start|stop|status|watch|restart|usage
52
+ warn <<~USAGE
53
+ Usage: #{$PROGRAM_NAME} start|stop|status|watch|restart|usage
54
54
 
55
- Options:
55
+ Options:
56
56
 
57
- start
58
- Start the daemon
57
+ start
58
+ Start the daemon
59
59
 
60
- stop
61
- Stop the daemon
60
+ stop
61
+ Stop the daemon
62
62
 
63
- kill
64
- Kill the daemon
63
+ kill
64
+ Kill the daemon
65
65
 
66
- status
67
- Query the status of the daemon. Exit with status 1 if any worker is
68
- not running.
66
+ status
67
+ Query the status of the daemon. Exit with status 1 if any worker is
68
+ not running.
69
69
 
70
- watch
71
- Checks the status (running or stopped) and whether it is as
72
- expected. Starts the daemon if it is expected to run but is not.
70
+ watch
71
+ Checks the status (running or stopped) and whether it is as
72
+ expected. Starts the daemon if it is expected to run but is not.
73
73
 
74
- restart
75
- Shortcut for consecutive 'stop' and 'start'.
74
+ restart
75
+ Shortcut for consecutive 'stop' and 'start'.
76
76
 
77
- restart-logging
78
- Re-opens log files, useful e.g. after the log files have been moved or
79
- removed by log rotation.
77
+ restart-logging
78
+ Re-opens log files, useful e.g. after the log files have been moved or
79
+ removed by log rotation.
80
80
 
81
- usage
82
- Show this message
81
+ usage
82
+ Show this message
83
83
 
84
- Exit status:
85
- 0 if OK,
86
- 1 on fatal errors outside of workhorse,
87
- 2 if at least one worker has an unexpected status,
88
- 99 on all other errors.
89
- USAGE
84
+ Exit status:
85
+ 0 if OK,
86
+ 1 on fatal errors outside of workhorse,
87
+ 2 if at least one worker has an unexpected status,
88
+ 99 on all other errors.
89
+ USAGE
90
90
  end
91
91
  end
92
92
  end
@@ -4,6 +4,7 @@ module Workhorse
4
4
  attr_reader :id
5
5
  attr_reader :name
6
6
  attr_reader :block
7
+ attr_accessor :pid
7
8
 
8
9
  def initialize(id, name, &block)
9
10
  @id = id
@@ -12,6 +13,9 @@ module Workhorse
12
13
  end
13
14
  end
14
15
 
16
+ # @private
17
+ attr_reader :workers
18
+
15
19
  def initialize(pidfile: nil, quiet: false, &_block)
16
20
  @pidfile = pidfile
17
21
  @quiet = quiet
@@ -68,7 +72,7 @@ module Workhorse
68
72
 
69
73
  if pid_file && pid
70
74
  puts "Worker (#{worker.name}) ##{worker.id}: Stopping"
71
- stop_worker pid_file, pid, kill
75
+ stop_worker pid_file, pid, kill: kill
72
76
  elsif pid_file
73
77
  File.delete pid_file
74
78
  puts "Worker (#{worker.name}) ##{worker.id}: Already stopped (stale PID file)"
@@ -109,10 +113,14 @@ module Workhorse
109
113
  end
110
114
 
111
115
  if should_be_running && status(quiet: true) != 0
112
- return start(quiet: Workhorse.silence_watcher)
116
+ code = start(quiet: Workhorse.silence_watcher)
113
117
  else
114
- return 0
118
+ code = 0
115
119
  end
120
+
121
+ watch_memory! if should_be_running
122
+
123
+ return code
116
124
  end
117
125
 
118
126
  def restart
@@ -140,6 +148,30 @@ module Workhorse
140
148
 
141
149
  private
142
150
 
151
+ def watch_memory!
152
+ return if Workhorse.max_worker_memory_mb == 0
153
+
154
+ for_each_worker do |worker|
155
+ pid_file, pid = read_pid(worker)
156
+ next unless pid_file && pid
157
+
158
+ memory = memory_for(pid)
159
+ next unless memory
160
+
161
+ if memory > Workhorse.max_worker_memory_mb
162
+ stop_worker pid_file, pid
163
+ start_worker worker
164
+ end
165
+ end
166
+ end
167
+
168
+ # Returns the memory (RSS) in MB for the given process.
169
+ def memory_for(pid)
170
+ mem = `ps -p #{pid} -o rss=`&.strip
171
+ return nil if mem.blank?
172
+ return mem.to_i / 1024
173
+ end
174
+
143
175
  def for_each_worker(&block)
144
176
  @workers.each(&block)
145
177
  end
@@ -147,20 +179,20 @@ module Workhorse
147
179
  def start_worker(worker)
148
180
  pid = fork do
149
181
  $0 = process_name(worker)
150
-
151
182
  # Reopen pipes to prevent #107576
152
- STDIN.reopen File.open('/dev/null', 'r')
183
+ $stdin.reopen File.open('/dev/null', 'r')
153
184
  null_out = File.open '/dev/null', 'w'
154
- STDOUT.reopen null_out
155
- STDERR.reopen null_out
185
+ $stdout.reopen null_out
186
+ $stderr.reopen null_out
156
187
 
157
188
  worker.block.call
158
189
  end
159
- IO.write(pid_file_for(worker), pid)
190
+ worker.pid = pid
191
+ File.write(pid_file_for(worker), pid)
160
192
  Process.detach(pid)
161
193
  end
162
194
 
163
- def stop_worker(pid_file, pid, kill = false)
195
+ def stop_worker(pid_file, pid, kill: false)
164
196
  signals = kill ? %w[KILL] : %w[TERM INT]
165
197
 
166
198
  loop do
@@ -207,8 +239,9 @@ module Workhorse
207
239
  file = pid_file_for(worker)
208
240
 
209
241
  if File.exist?(file)
210
- raw_pid = IO.read(file)
242
+ raw_pid = File.read(file)
211
243
  return nil, nil if raw_pid.blank?
244
+
212
245
  pid = Integer(raw_pid)
213
246
  return file, process?(pid) ? pid : nil
214
247
  else
@@ -6,6 +6,8 @@ module Workhorse
6
6
  STATE_SUCCEEDED = :succeeded
7
7
  STATE_FAILED = :failed
8
8
 
9
+ EXP_LOCKED_BY = /^(.*?)\.(\d+?)\.([^.]+)$/.freeze
10
+
9
11
  if respond_to?(:attr_accessible)
10
12
  attr_accessible :queue, :priority, :perform_at, :handler, :description
11
13
  end
@@ -32,6 +34,31 @@ module Workhorse
32
34
  where(state: STATE_FAILED)
33
35
  end
34
36
 
37
+ # @private
38
+ def self.with_split_locked_by
39
+ select(<<~SQL)
40
+ #{table_name}.*,
41
+
42
+ -- random string
43
+ substring_index(locked_by, '.', -1) as locked_by_rnd,
44
+
45
+ -- pid
46
+ substring_index(
47
+ substring_index(locked_by, '.', -2),
48
+ '.',
49
+ 1
50
+ ) as locked_by_pid,
51
+
52
+ -- get host
53
+ substring(
54
+ locked_by,
55
+ 1,
56
+ length(locked_by) -
57
+ length(substring_index(locked_by, '.', -2)) - 1
58
+ ) as locked_by_host
59
+ SQL
60
+ end
61
+
35
62
  # Resets job to state "waiting" and clears all meta fields
36
63
  # set by workhorse in course of processing this job.
37
64
  #
@@ -69,11 +96,6 @@ module Workhorse
69
96
  fail "Dirty jobs can't be locked."
70
97
  end
71
98
 
72
- # TODO: Remove this debug output
73
- # if Workhorse::DbJob.lock.find(id).locked_at
74
- # puts "Already locked (with FOR UPDATE)"
75
- # end
76
-
77
99
  if locked_at
78
100
  # TODO: Remove this debug output
79
101
  # puts "Already locked. Job: #{self.id} Worker: #{worker_id}"
@@ -3,11 +3,11 @@ module Workhorse
3
3
  # Enqueue any object that is serializable and has a `perform` method
4
4
  def enqueue(job, queue: nil, priority: 0, perform_at: Time.now, description: nil)
5
5
  return DbJob.create!(
6
- queue: queue,
7
- priority: priority,
8
- perform_at: perform_at,
6
+ queue: queue,
7
+ priority: priority,
8
+ perform_at: perform_at,
9
9
  description: description,
10
- handler: Marshal.dump(job)
10
+ handler: Marshal.dump(job)
11
11
  )
12
12
  end
13
13
 
@@ -18,7 +18,7 @@ module Workhorse::Jobs
18
18
  private
19
19
 
20
20
  def seconds_ago(days)
21
- Time.now - days * 24 * 60 * 60
21
+ Time.now - (days * 24 * 60 * 60)
22
22
  end
23
23
  end
24
24
  end
@@ -27,7 +27,7 @@ module Workhorse::Jobs
27
27
  ids = rel.pluck(:id)
28
28
 
29
29
  unless ids.empty?
30
- messages << "Detected #{ids.size} jobs that were locked more than "\
30
+ messages << "Detected #{ids.size} jobs that were locked more than " \
31
31
  "#{@locked_to_started_threshold}s ago and might be stale: #{ids.inspect}."
32
32
  end
33
33
  end
@@ -39,7 +39,7 @@ module Workhorse::Jobs
39
39
  ids = rel.pluck(:id)
40
40
 
41
41
  unless ids.empty?
42
- messages << "Detected #{ids.size} jobs that are running for longer than "\
42
+ messages << "Detected #{ids.size} jobs that are running for longer than " \
43
43
  "#{@run_time_threshold}s ago and might be stale: #{ids.inspect}."
44
44
  end
45
45
  end
@@ -27,6 +27,8 @@ module Workhorse
27
27
  fail 'Poller is already running.' if running?
28
28
  @running = true
29
29
 
30
+ clean_stuck_jobs! if Workhorse.clean_stuck_jobs
31
+
30
32
  @thread = Thread.new do
31
33
  loop do
32
34
  break unless running?
@@ -65,6 +67,61 @@ module Workhorse
65
67
 
66
68
  private
67
69
 
70
+ def clean_stuck_jobs!
71
+ with_global_lock timeout: MAX_LOCK_TIMEOUT do
72
+ Workhorse.tx_callback.call do
73
+ # Basic relation: Fetch jobs locked by current host in state 'locked' or
74
+ # 'started'
75
+ rel = Workhorse::DbJob.select('*').from(<<~SQL)
76
+ (#{Workhorse::DbJob.with_split_locked_by.to_sql}) #{Workhorse::DbJob.table_name}
77
+ SQL
78
+ rel.where!(
79
+ locked_by_host: worker.hostname,
80
+ state: [Workhorse::DbJob::STATE_LOCKED, Workhorse::DbJob::STATE_STARTED]
81
+ )
82
+
83
+ # Select all pids
84
+ job_pids = rel.distinct.pluck(:locked_by_pid).to_set(&:to_i)
85
+
86
+ # Get pids without active process
87
+ orphaned_pids = job_pids.select do |pid|
88
+ Process.getpgid(pid)
89
+ false
90
+ rescue Errno::ESRCH
91
+ true
92
+ end
93
+
94
+ # Reset jobs in state 'locked'
95
+ rel.where(locked_by_pid: orphaned_pids.to_a, state: Workhorse::DbJob::STATE_LOCKED).each do |job|
96
+ worker.log(
97
+ "Job ##{job.id} has been locked but not yet startet by PID #{job.locked_by_pid} on host " \
98
+ "#{job.locked_by_host}, but the process is not running anymore. This job has therefore been " \
99
+ "reset (set to 'waiting') by the Workhorse cleanup logic.",
100
+ :warn
101
+ )
102
+ job.reset!(true)
103
+ end
104
+
105
+ # Mark jobs in state 'started' as failed
106
+ rel.where(locked_by_pid: orphaned_pids.to_a, state: Workhorse::DbJob::STATE_STARTED).each do |job|
107
+ worker.log(
108
+ "Job ##{job.id} has been started by PID #{job.locked_by_pid} on host #{job.locked_by_host} " \
109
+ 'but the process is not running anymore. This job has therefore been marked as ' \
110
+ 'failed by the Workhorse cleanup logic.',
111
+ :warn
112
+ )
113
+ exception = Exception.new(
114
+ "Job has been started by PID #{job.locked_by_pid} on host #{job.locked_by_host} " \
115
+ 'but the process is not running anymore. This job has therefore been marked as ' \
116
+ 'failed by the Workhorse cleanup logic.'
117
+ )
118
+ exception.set_backtrace []
119
+ job.mark_failed!(exception)
120
+ end
121
+ end
122
+ end
123
+ end
124
+
68
125
  def sleep
69
126
  remaining = worker.polling_interval
70
127
 
@@ -101,15 +158,15 @@ module Workhorse
101
158
  if @global_lock_fails > Workhorse.max_global_lock_fails && !@max_global_lock_fails_reached
102
159
  @max_global_lock_fails_reached = true
103
160
 
104
- worker.log 'Could not obtain global lock, retrying with next poll. '\
105
- 'This will be the last such message for this worker until '\
161
+ worker.log 'Could not obtain global lock, retrying with next poll. ' \
162
+ 'This will be the last such message for this worker until ' \
106
163
  'the issue is resolved.', :warn
107
164
 
108
165
  message = "Worker reached maximum number of consecutive times (#{Workhorse.max_global_lock_fails}) " \
109
166
  "where the global lock could no be acquired within the specified timeout (#{timeout}). " \
110
167
  'A worker that obtained this lock may have crashed without ending the database ' \
111
168
  'connection properly. On MySQL, use "show processlist;" to see which connection(s) ' \
112
- 'is / are holding the lock for a long period of time and consider killing them using '\
169
+ 'is / are holding the lock for a long period of time and consider killing them using ' \
113
170
  "MySQL's \"kill <Id>\" command. This message will be issued only once per worker " \
114
171
  'and may only be re-triggered if the error happens again *after* the lock has ' \
115
172
  'been solved in the meantime.'
@@ -137,7 +194,6 @@ module Workhorse
137
194
  @instant_repoll.make_false
138
195
 
139
196
  timeout = [MIN_LOCK_TIMEOUT, [MAX_LOCK_TIMEOUT, worker.polling_interval].min].max
140
-
141
197
  with_global_lock timeout: timeout do
142
198
  job_ids = []
143
199
 
@@ -199,9 +255,9 @@ module Workhorse
199
255
  # uses the keyword 'AS' in SQL generated for Oracle, which is invalid for
200
256
  # table aliases.
201
257
  union_query_sql = '('
202
- union_query_sql += 'SELECT * FROM (' + union_parts.shift.to_sql + ') union_0'
258
+ union_query_sql += "SELECT * FROM (#{union_parts.shift.to_sql}) union_0"
203
259
  union_parts.each_with_index do |part, idx|
204
- union_query_sql += ' UNION SELECT * FROM (' + part.to_sql + ") union_#{idx + 1}"
260
+ union_query_sql += " UNION SELECT * FROM (#{part.to_sql}) union_#{idx + 1}"
205
261
  end
206
262
  union_query_sql += ') subselect'
207
263
 
@@ -277,7 +333,7 @@ module Workhorse
277
333
  unless worker.queues.empty?
278
334
  if worker.queues.include?(nil)
279
335
  where = table[:queue].eq(nil)
280
- remaining_queues = worker.queues.reject(&:nil?)
336
+ remaining_queues = worker.queues.compact
281
337
  unless remaining_queues.empty?
282
338
  where = where.or(table[:queue].in(remaining_queues))
283
339
  end
@@ -6,11 +6,11 @@ module Workhorse
6
6
  def initialize(size)
7
7
  @size = size
8
8
  @executor = Concurrent::ThreadPoolExecutor.new(
9
- min_threads: 0,
10
- max_threads: @size,
11
- max_queue: 0,
9
+ min_threads: 0,
10
+ max_threads: @size,
11
+ max_queue: 0,
12
12
  fallback_policy: :abort,
13
- auto_terminate: false
13
+ auto_terminate: false
14
14
  )
15
15
  @mutex = Mutex.new
16
16
  @active_threads = Concurrent::AtomicFixnum.new(0)
@@ -33,12 +33,10 @@ module Workhorse
33
33
  active_threads.increment
34
34
 
35
35
  @executor.post do
36
- begin
37
- yield
38
- ensure
39
- active_threads.decrement
40
- @on_idle.try(:call)
41
- end
36
+ yield
37
+ ensure
38
+ active_threads.decrement
39
+ @on_idle.try(:call)
42
40
  end
43
41
  end
44
42
  end
@@ -43,7 +43,7 @@ module Workhorse
43
43
  # `Rails.logger`.
44
44
  def initialize(queues: [], pool_size: nil, polling_interval: 300, auto_terminate: true, quiet: true, instant_repolling: false, logger: nil)
45
45
  @queues = queues
46
- @pool_size = pool_size || queues.size + 1
46
+ @pool_size = pool_size || (queues.size + 1)
47
47
  @polling_interval = polling_interval
48
48
  @auto_terminate = auto_terminate
49
49
  @state = :initialized
@@ -54,7 +54,7 @@ module Workhorse
54
54
  @poller = Workhorse::Poller.new(self)
55
55
  @logger = logger
56
56
 
57
- unless (@polling_interval / 0.1).round(2).modulo(1) == 0.0
57
+ unless (@polling_interval / 0.1).round(2).modulo(1).zero?
58
58
  fail 'Polling interval must be a multiple of 0.1.'
59
59
  end
60
60
 
@@ -74,7 +74,15 @@ module Workhorse
74
74
  end
75
75
 
76
76
  def id
77
- @id ||= "#{Socket.gethostname}.#{Process.pid}.#{SecureRandom.hex(3)}"
77
+ @id ||= "#{hostname}.#{pid}.#{SecureRandom.hex(3)}"
78
+ end
79
+
80
+ def pid
81
+ @pid ||= Process.pid
82
+ end
83
+
84
+ def hostname
85
+ @hostname ||= Socket.gethostname
78
86
  end
79
87
 
80
88
  # Starts the worker. This call is not blocking - call {wait} for this
@@ -135,12 +143,10 @@ module Workhorse
135
143
  log "Posting job #{db_job_id} to thread pool"
136
144
 
137
145
  @pool.post do
138
- begin
139
- Workhorse::Performer.new(db_job_id, self).perform
140
- rescue Exception => e
141
- log %(#{e.message}\n#{e.backtrace.join("\n")}), :error
142
- Workhorse.on_exception.call(e)
143
- end
146
+ Workhorse::Performer.new(db_job_id, self).perform
147
+ rescue Exception => e
148
+ log %(#{e.message}\n#{e.backtrace.join("\n")}), :error
149
+ Workhorse.on_exception.call(e)
144
150
  end
145
151
  end
146
152
  rescue Exception => e
data/lib/workhorse.rb CHANGED
@@ -17,7 +17,7 @@ module Workhorse
17
17
  # Returns the performer currently performing the active job. This can only be
18
18
  # called from within a job and the same thread.
19
19
  def self.performer
20
- Thread.current[:workhorse_current_performer]\
20
+ Thread.current[:workhorse_current_performer] \
21
21
  || fail('No performer is associated with the current thread. This method must always be called inside of a job.')
22
22
  end
23
23
 
@@ -58,6 +58,11 @@ module Workhorse
58
58
  mattr_accessor :perform_jobs_in_tx
59
59
  self.perform_jobs_in_tx = true
60
60
 
61
+ # If enabled, each poller will attempt to clean jobs that are stuck in state
62
+ # 'locked' or 'running' when it is starting up.
63
+ mattr_accessor :clean_stuck_jobs
64
+ self.clean_stuck_jobs = false
65
+
61
66
  # This setting is for {Workhorse::Jobs::DetectStaleJobsJob} and specifies the
62
67
  # maximum number of seconds a job is allowed to stay 'locked' before this job
63
68
  # throws an exception. Set this to 0 to skip this check.
@@ -70,6 +75,12 @@ module Workhorse
70
75
  mattr_accessor :stale_detection_run_time_threshold
71
76
  self.stale_detection_run_time_threshold = 12 * 60
72
77
 
78
+ # Maximum memory for a worker in MB. If this memory limit (RSS / resident
79
+ # size) is reached for a worker process, the 'watch' command will restart said
80
+ # worker. Set this to 0 disable this feature.
81
+ mattr_accessor :max_worker_memory_mb
82
+ self.max_worker_memory_mb = 0
83
+
73
84
  def self.setup
74
85
  yield self
75
86
  end
@@ -92,5 +103,5 @@ if RUBY_PLATFORM != 'java'
92
103
  end
93
104
 
94
105
  if defined?(ActiveJob)
95
- require 'active_job/queue_adapters/workhorse_adapter.rb'
106
+ require 'active_job/queue_adapters/workhorse_adapter'
96
107
  end
@@ -44,10 +44,10 @@ end
44
44
 
45
45
  ActiveRecord::Base.establish_connection(
46
46
  adapter: 'mysql2',
47
- database: ENV['DB_NAME'] || 'workhorse',
48
- username: ENV['DB_USERNAME'] || 'root',
49
- password: ENV['DB_PASSWORD'] || '',
50
- host: ENV['DB_HOST'] || '127.0.0.1',
47
+ database: ENV.fetch('DB_NAME', nil) || 'workhorse',
48
+ username: ENV.fetch('DB_USERNAME', nil) || 'root',
49
+ password: ENV.fetch('DB_PASSWORD', nil) || '',
50
+ host: ENV.fetch('DB_HOST', nil) || '127.0.0.1',
51
51
  pool: 10
52
52
  )
53
53
 
@@ -41,7 +41,7 @@ class Workhorse::PollerTest < WorkhorseTest
41
41
 
42
42
  begin
43
43
  fail 'Some exception'
44
- rescue => e
44
+ rescue StandardError => e
45
45
  last_job.mark_failed!(e)
46
46
  end
47
47
 
@@ -148,9 +148,10 @@ class Workhorse::PollerTest < WorkhorseTest
148
148
  assert_equal 25, used_workers
149
149
  end
150
150
 
151
- # rubocop: disable Style/GlobalVars
152
151
  def test_connection_loss
152
+ # rubocop: disable Style/GlobalVars
153
153
  $thread_conn = nil
154
+ # rubocop: enable Style/GlobalVars
154
155
 
155
156
  Workhorse.enqueue BasicJob.new(sleep_time: 3)
156
157
 
@@ -175,10 +176,95 @@ class Workhorse::PollerTest < WorkhorseTest
175
176
 
176
177
  assert_equal 1, Workhorse::DbJob.succeeded.count
177
178
  end
178
- # rubocop: enable Style/GlobalVars
179
+
180
+ def test_clean_stuck_jobs_locked
181
+ [true, false].each do |clean|
182
+ Workhorse::DbJob.delete_all
183
+
184
+ Workhorse.clean_stuck_jobs = clean
185
+ start_deamon
186
+ Workhorse.enqueue BasicJob.new(sleep_time: 5)
187
+ sleep 0.2
188
+ kill_deamon_workers
189
+
190
+ assert_equal 1, Workhorse::DbJob.count
191
+
192
+ Workhorse::DbJob.first.update(
193
+ state: 'locked',
194
+ started_at: nil
195
+ )
196
+
197
+ Workhorse::Worker.new.poller.send(:clean_stuck_jobs!) if clean
198
+
199
+ assert_equal 1, Workhorse::DbJob.count
200
+
201
+ Workhorse::DbJob.first.tap do |job|
202
+ if clean
203
+ assert_equal 'waiting', job.state
204
+ assert_nil job.locked_at
205
+ assert_nil job.locked_by
206
+ assert_nil job.started_at
207
+ assert_nil job.last_error
208
+ else
209
+ assert_equal 'locked', job.state
210
+ end
211
+ end
212
+ ensure
213
+ Workhorse.clean_stuck_jobs = false
214
+ end
215
+ end
216
+
217
+ def test_clean_stuck_jobs_running
218
+ [true, false].each do |clean|
219
+ Workhorse::DbJob.delete_all
220
+
221
+ Workhorse.clean_stuck_jobs = true
222
+ start_deamon
223
+ Workhorse.enqueue BasicJob.new(sleep_time: 5)
224
+ sleep 0.2
225
+ kill_deamon_workers
226
+
227
+ assert_equal 1, Workhorse::DbJob.count
228
+ assert_equal 'started', Workhorse::DbJob.first.state
229
+
230
+ work 0.1 if clean
231
+
232
+ assert_equal 1, Workhorse::DbJob.count
233
+
234
+ Workhorse::DbJob.first.tap do |job|
235
+ if clean
236
+ assert_equal 'failed', job.state
237
+ assert_match(/started by PID #{@daemon.workers.first.pid}/, job.last_error)
238
+ assert_match(/on host #{Socket.gethostname}/, job.last_error)
239
+ else
240
+ assert_equal 'started', job.state
241
+ end
242
+ end
243
+ ensure
244
+ Workhorse.clean_stuck_jobs = false
245
+ end
246
+ end
179
247
 
180
248
  private
181
249
 
250
+ def kill_deamon_workers
251
+ @daemon.workers.each do |worker|
252
+ Process.kill 'KILL', worker.pid
253
+ end
254
+ end
255
+
256
+ def start_deamon
257
+ @daemon = Workhorse::Daemon.new(pidfile: 'tmp/pids/test%s.pid') do |d|
258
+ d.worker 'Test Worker' do
259
+ Workhorse::Worker.start_and_wait(
260
+ pool_size: 1,
261
+ polling_interval: 0.1
262
+ )
263
+ end
264
+ end
265
+ @daemon.start
266
+ end
267
+
182
268
  def setup
183
269
  Workhorse::DbJob.delete_all
184
270
  end
data/workhorse.gemspec CHANGED
@@ -1,61 +1,31 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: workhorse 1.2.15 ruby lib
2
+ # stub: workhorse 1.2.17.rc0 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "workhorse".freeze
6
- s.version = "1.2.15"
6
+ s.version = "1.2.17.rc0"
7
7
 
8
- s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
8
+ s.required_rubygems_version = Gem::Requirement.new("> 1.3.1".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
10
10
  s.authors = ["Sitrox".freeze]
11
- s.date = "2023-08-28"
11
+ s.date = "2024-02-05"
12
12
  s.files = [".github/workflows/ruby.yml".freeze, ".gitignore".freeze, ".releaser_config".freeze, ".rubocop.yml".freeze, "CHANGELOG.md".freeze, "FAQ.md".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "RUBY_VERSION".freeze, "Rakefile".freeze, "VERSION".freeze, "bin/rubocop".freeze, "lib/active_job/queue_adapters/workhorse_adapter.rb".freeze, "lib/generators/workhorse/install_generator.rb".freeze, "lib/generators/workhorse/templates/bin/workhorse.rb".freeze, "lib/generators/workhorse/templates/config/initializers/workhorse.rb".freeze, "lib/generators/workhorse/templates/create_table_jobs.rb".freeze, "lib/workhorse.rb".freeze, "lib/workhorse/active_job_extension.rb".freeze, "lib/workhorse/daemon.rb".freeze, "lib/workhorse/daemon/shell_handler.rb".freeze, "lib/workhorse/db_job.rb".freeze, "lib/workhorse/enqueuer.rb".freeze, "lib/workhorse/jobs/cleanup_succeeded_jobs.rb".freeze, "lib/workhorse/jobs/detect_stale_jobs_job.rb".freeze, "lib/workhorse/jobs/run_active_job.rb".freeze, "lib/workhorse/jobs/run_rails_op.rb".freeze, "lib/workhorse/performer.rb".freeze, "lib/workhorse/poller.rb".freeze, "lib/workhorse/pool.rb".freeze, "lib/workhorse/scoped_env.rb".freeze, "lib/workhorse/worker.rb".freeze, "test/active_job/queue_adapters/workhorse_adapter_test.rb".freeze, "test/lib/db_schema.rb".freeze, "test/lib/jobs.rb".freeze, "test/lib/test_helper.rb".freeze, "test/workhorse/db_job_test.rb".freeze, "test/workhorse/enqueuer_test.rb".freeze, "test/workhorse/performer_test.rb".freeze, "test/workhorse/poller_test.rb".freeze, "test/workhorse/pool_test.rb".freeze, "test/workhorse/worker_test.rb".freeze, "workhorse.gemspec".freeze]
13
- s.rubygems_version = "3.0.3".freeze
13
+ s.rubygems_version = "3.4.6".freeze
14
14
  s.summary = "Multi-threaded job backend with database queuing for ruby.".freeze
15
15
  s.test_files = ["test/active_job/queue_adapters/workhorse_adapter_test.rb".freeze, "test/lib/db_schema.rb".freeze, "test/lib/jobs.rb".freeze, "test/lib/test_helper.rb".freeze, "test/workhorse/db_job_test.rb".freeze, "test/workhorse/enqueuer_test.rb".freeze, "test/workhorse/performer_test.rb".freeze, "test/workhorse/poller_test.rb".freeze, "test/workhorse/pool_test.rb".freeze, "test/workhorse/worker_test.rb".freeze]
16
16
 
17
- if s.respond_to? :specification_version then
18
- s.specification_version = 4
17
+ s.specification_version = 4
19
18
 
20
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
21
- s.add_development_dependency(%q<bundler>.freeze, [">= 0"])
22
- s.add_development_dependency(%q<rake>.freeze, [">= 0"])
23
- s.add_development_dependency(%q<rubocop>.freeze, ["= 0.51.0"])
24
- s.add_development_dependency(%q<minitest>.freeze, [">= 0"])
25
- s.add_development_dependency(%q<mysql2>.freeze, [">= 0"])
26
- s.add_development_dependency(%q<colorize>.freeze, [">= 0"])
27
- s.add_development_dependency(%q<benchmark-ips>.freeze, [">= 0"])
28
- s.add_development_dependency(%q<activejob>.freeze, [">= 0"])
29
- s.add_development_dependency(%q<pry>.freeze, [">= 0"])
30
- s.add_runtime_dependency(%q<activesupport>.freeze, [">= 0"])
31
- s.add_runtime_dependency(%q<activerecord>.freeze, [">= 0"])
32
- s.add_runtime_dependency(%q<concurrent-ruby>.freeze, [">= 0"])
33
- else
34
- s.add_dependency(%q<bundler>.freeze, [">= 0"])
35
- s.add_dependency(%q<rake>.freeze, [">= 0"])
36
- s.add_dependency(%q<rubocop>.freeze, ["= 0.51.0"])
37
- s.add_dependency(%q<minitest>.freeze, [">= 0"])
38
- s.add_dependency(%q<mysql2>.freeze, [">= 0"])
39
- s.add_dependency(%q<colorize>.freeze, [">= 0"])
40
- s.add_dependency(%q<benchmark-ips>.freeze, [">= 0"])
41
- s.add_dependency(%q<activejob>.freeze, [">= 0"])
42
- s.add_dependency(%q<pry>.freeze, [">= 0"])
43
- s.add_dependency(%q<activesupport>.freeze, [">= 0"])
44
- s.add_dependency(%q<activerecord>.freeze, [">= 0"])
45
- s.add_dependency(%q<concurrent-ruby>.freeze, [">= 0"])
46
- end
47
- else
48
- s.add_dependency(%q<bundler>.freeze, [">= 0"])
49
- s.add_dependency(%q<rake>.freeze, [">= 0"])
50
- s.add_dependency(%q<rubocop>.freeze, ["= 0.51.0"])
51
- s.add_dependency(%q<minitest>.freeze, [">= 0"])
52
- s.add_dependency(%q<mysql2>.freeze, [">= 0"])
53
- s.add_dependency(%q<colorize>.freeze, [">= 0"])
54
- s.add_dependency(%q<benchmark-ips>.freeze, [">= 0"])
55
- s.add_dependency(%q<activejob>.freeze, [">= 0"])
56
- s.add_dependency(%q<pry>.freeze, [">= 0"])
57
- s.add_dependency(%q<activesupport>.freeze, [">= 0"])
58
- s.add_dependency(%q<activerecord>.freeze, [">= 0"])
59
- s.add_dependency(%q<concurrent-ruby>.freeze, [">= 0"])
60
- end
19
+ s.add_development_dependency(%q<bundler>.freeze, [">= 0"])
20
+ s.add_development_dependency(%q<rake>.freeze, [">= 0"])
21
+ s.add_development_dependency(%q<rubocop>.freeze, ["~> 1.28.0"])
22
+ s.add_development_dependency(%q<minitest>.freeze, [">= 0"])
23
+ s.add_development_dependency(%q<mysql2>.freeze, [">= 0"])
24
+ s.add_development_dependency(%q<colorize>.freeze, [">= 0"])
25
+ s.add_development_dependency(%q<benchmark-ips>.freeze, [">= 0"])
26
+ s.add_development_dependency(%q<activejob>.freeze, [">= 0"])
27
+ s.add_development_dependency(%q<pry>.freeze, [">= 0"])
28
+ s.add_runtime_dependency(%q<activesupport>.freeze, [">= 0"])
29
+ s.add_runtime_dependency(%q<activerecord>.freeze, [">= 0"])
30
+ s.add_runtime_dependency(%q<concurrent-ruby>.freeze, [">= 0"])
61
31
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: workhorse
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.15
4
+ version: 1.2.17.rc0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sitrox
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-28 00:00:00.000000000 Z
11
+ date: 2024-02-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -42,16 +42,16 @@ dependencies:
42
42
  name: rubocop
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - '='
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 0.51.0
47
+ version: 1.28.0
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - '='
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 0.51.0
54
+ version: 1.28.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: minitest
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -242,11 +242,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
242
242
  version: '0'
243
243
  required_rubygems_version: !ruby/object:Gem::Requirement
244
244
  requirements:
245
- - - ">="
245
+ - - ">"
246
246
  - !ruby/object:Gem::Version
247
- version: '0'
247
+ version: 1.3.1
248
248
  requirements: []
249
- rubygems_version: 3.4.6
249
+ rubygems_version: 3.4.10
250
250
  signing_key:
251
251
  specification_version: 4
252
252
  summary: Multi-threaded job backend with database queuing for ruby.