skiplock 1.1.8 → 1.1.9
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/LICENSE.txt +0 -0
- data/README.md +13 -3
- data/lib/active_job/queue_adapters/skiplock_adapter.rb +0 -0
- data/lib/generators/skiplock/install_generator.rb +0 -0
- data/lib/generators/skiplock/templates/migration.rb.erb +0 -0
- data/lib/skiplock/counter.rb +0 -0
- data/lib/skiplock/cron.rb +0 -0
- data/lib/skiplock/extension.rb +0 -0
- data/lib/skiplock/job.rb +1 -1
- data/lib/skiplock/manager.rb +52 -4
- data/lib/skiplock/patch.rb +0 -0
- data/lib/skiplock/version.rb +1 -1
- data/lib/skiplock/worker.rb +34 -14
- data/lib/skiplock.rb +0 -0
- metadata +45 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 21372493169f916f319d425482e65f015777befaa5e4a65a9c29895a65f87719
|
|
4
|
+
data.tar.gz: c4ddc48122b091efb1bfc378e23c41e53f748251c914cc42b34f2bc6c0a51a97
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8d4bb1df0948887cc5ad7a70e6e586bb021040817848cf649030918102cfacb09ca4a70e051d259a17348133ed88a287ba20cbe8e68e3cff817c56b3bae068da
|
|
7
|
+
data.tar.gz: 2ea888f4e274f58e0617548857bcdc3936793e6e1270d70e0b51fcdfa3c06f1793fbf37f41cbe9deb9e83eed5bc8d5861924659cab95205433d48cb7fa42c955
|
data/LICENSE.txt
CHANGED
|
File without changes
|
data/README.md
CHANGED
|
@@ -76,7 +76,7 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
|
|
|
76
76
|
- **log_level** (*string*): sets logging level (`debug, info, warn, error, fatal, unknown`)
|
|
77
77
|
- **log_count** (*integer*): number of log files to keep (ie: log rotation)
|
|
78
78
|
- **log_size** (*integer*): maximum size per log file (in bytes)
|
|
79
|
-
- **namespace** (*string*):
|
|
79
|
+
- **namespace** (*string*): routes a category of jobs to workers configured with the same namespace; an empty value selects the default, unnamespaced category. Cron jobs always use the default category.
|
|
80
80
|
- **notification** (*string*): sets the library to be used for notifying errors and exceptions (`auto, airbrake, bugsnag, exception_notification, custom`); using `auto` will detect library if available. See `Notification system` for more details
|
|
81
81
|
- **extensions** (*multi*): enable or disable the class method extension. See `ClassMethod extension` for more details
|
|
82
82
|
- **purge_completion** (*boolean*): when set to **true** will delete jobs after they were completed successfully; if set to **false** then the completed jobs should be purged periodically to maximize performance (eg. clean up old jobs after 3 months); queued jobs can manually override using `purge` option
|
|
@@ -86,8 +86,12 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
|
|
|
86
86
|
#### **Async mode**
|
|
87
87
|
When **workers** is set to **0** then the jobs will be performed in the web server process using separate threads. If using multi-worker cluster mode web server like Puma, then all the Puma workers will also be able to perform `Skiplock` jobs.
|
|
88
88
|
|
|
89
|
+
In async mode the application server owns process signals. Skiplock does not install `INT` or `TERM` handlers; it shuts down its thread pool from a process-local, idempotent exit callback. This allows Puma workers to finish jobs for up to `graceful_shutdown` seconds without Skiplock interfering with Puma's signal handling. Puma's own worker shutdown timeout must allow at least the same amount of time. Async workers must be initialized after Puma forks; if `preload_app!` is enabled, use a Puma worker-boot hook or run Skiplock in standalone mode because worker threads are not inherited across a fork.
|
|
90
|
+
|
|
89
91
|
#### **Standalone mode**
|
|
90
92
|
`Skiplock` standalone mode can be launched by using the `skiplock` executable; command line options can be provided to override the `Skiplock` configuration file.
|
|
93
|
+
|
|
94
|
+
In standalone multi-process mode the Skiplock parent owns its child processes. On `INT` or `TERM`, it forwards `TERM` to every child, shuts down all worker thread pools against one shared `graceful_shutdown` deadline, and terminates any child that remains after the deadline.
|
|
91
95
|
```
|
|
92
96
|
$ bundle exec skiplock -h
|
|
93
97
|
Usage: skiplock [options]
|
|
@@ -140,6 +144,8 @@ Outside the Rails application:
|
|
|
140
144
|
|
|
141
145
|
## Cron system
|
|
142
146
|
`Skiplock` provides the capability to setup cron jobs for running tasks periodically. It fully supports the cron syntax to specify the frequency of the jobs. To setup a cron job, simply assign a valid cron schedule to the constant `CRON` for the Job Class.
|
|
147
|
+
|
|
148
|
+
Cron schedules are global and are intentionally stored in the default, empty Skiplock namespace. Ruby module namespaces remain part of the complete job class name, so `Reports::CleanupJob` and `Mail::CleanupJob` are distinct schedules. At least one unnamespaced worker must be running to execute cron jobs; workers configured with a nonempty Skiplock namespace do not process them. Both async and standalone master workers reconcile cron definitions at startup.
|
|
143
149
|
- setup `MyJob` to run as cron job every hour at 30 minutes past
|
|
144
150
|
|
|
145
151
|
```ruby
|
|
@@ -176,7 +182,7 @@ Outside the Rails application:
|
|
|
176
182
|
```
|
|
177
183
|
If the retry attempt limit configured in ActiveJob has been reached, then the control will be passed back to `Skiplock` to be marked as an expired job.
|
|
178
184
|
|
|
179
|
-
If the `retry_on` block is not defined, then the built-in retry system of `Skiplock` will kick in automatically.
|
|
185
|
+
If the `retry_on` block is not defined, then the built-in retry system of `Skiplock` will kick in automatically. The delay before each retry uses the exponential formula `5 + 2**attempt` seconds. The first retry is delayed 7 seconds; at the maximum supported retry count of 20, the final individual delay is approximately 12.1 days. The cumulative delay across all 20 retries is approximately 24.3 days. The `Skiplock` configuration `max_retries` determines the limit of attempts before the failing job is marked as expired.
|
|
180
186
|
|
|
181
187
|
## Notification system
|
|
182
188
|
`Skiplock` can use existing exception notification library to notify errors and exceptions. It supports `airbrake`, `bugsnag`, and `exception_notification`. Custom notification can also be called whenever an exception occurs; it can be configured in an initializer like below:
|
|
@@ -213,12 +219,16 @@ To enable extension for specific classes and modules only then set the configura
|
|
|
213
219
|
```
|
|
214
220
|
|
|
215
221
|
## Fault tolerant
|
|
216
|
-
`Skiplock` ensures that jobs will be executed
|
|
222
|
+
`Skiplock` ensures that jobs will be executed successfully only once even if database connection is lost during or after the job was dispatched. Successful jobs are marked as completed or removed (with `purge_completion` global configuration or `purge` job option); failed or interrupted jobs are marked for retry.
|
|
223
|
+
|
|
224
|
+
"Successfully only once" means that a job record can be recorded as successfully completed only once. Failed or interrupted attempts may execute again and do not count as a successful completion. Jobs that produce external, non-transactional side effects such as SMTP delivery or HTTP requests should therefore be idempotent, because a process can be interrupted after the external system accepts an operation but before Skiplock records the successful completion.
|
|
217
225
|
|
|
218
226
|
However, when the database connection is dropped for any reasons and the commit is lost, `Skiplock` will then save the commit data to local disk (as `tmp/skiplock/<job_id>`) and synchronize with the database when the connection resumes.
|
|
219
227
|
|
|
220
228
|
This also protects long running in-progress jobs that are terminated abruptly during a graceful shutdown with timeout; these will be queued for retry.
|
|
221
229
|
|
|
230
|
+
Worker ownership is host-affine by design. If a job is assigned to a worker on a particular host and that host becomes unavailable, another host does not take ownership of it. The job waits until the original host returns, removes its obsolete worker records, and releases the job for retry. Hosts running Skiplock should therefore be supervised and stale worker heartbeats should be monitored.
|
|
231
|
+
|
|
222
232
|
## Scalability
|
|
223
233
|
`Skiplock` can scale both vertically and horizontally. To scale vertically, simply increase the number of `Skiplock` workers per host. To scale horizontally, simply deploy `Skiplock` to multiple hosts sharing the same PostgreSQL database.
|
|
224
234
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
data/lib/skiplock/counter.rb
CHANGED
|
File without changes
|
data/lib/skiplock/cron.rb
CHANGED
|
File without changes
|
data/lib/skiplock/extension.rb
CHANGED
|
File without changes
|
data/lib/skiplock/job.rb
CHANGED
|
@@ -69,7 +69,7 @@ module Skiplock
|
|
|
69
69
|
if (self.executions.to_i >= self.max_retries + 1) || self.exception_executions.key?('activejob_error') || self.exception.is_a?(Skiplock::Extension::ProxyError)
|
|
70
70
|
self.expired_at = Time.now
|
|
71
71
|
else
|
|
72
|
-
self.scheduled_at = Time.now + (5
|
|
72
|
+
self.scheduled_at = Time.now + (5 + 2**self.executions.to_i)
|
|
73
73
|
end
|
|
74
74
|
elsif self.finished_at
|
|
75
75
|
if self.cron
|
data/lib/skiplock/manager.rb
CHANGED
|
@@ -12,14 +12,28 @@ module Skiplock
|
|
|
12
12
|
configure
|
|
13
13
|
Worker.cleanup(@hostname)
|
|
14
14
|
@worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname)
|
|
15
|
+
@worker_pid = Process.pid
|
|
15
16
|
Cron.setup if @worker.master
|
|
16
17
|
@worker.start(**@config)
|
|
17
|
-
at_exit {
|
|
18
|
+
at_exit { shutdown }
|
|
18
19
|
rescue Exception => ex
|
|
19
20
|
@logger.error(ex.to_s)
|
|
20
21
|
@logger.error(ex.backtrace.join("\n"))
|
|
21
22
|
end
|
|
22
23
|
|
|
24
|
+
# Puma owns process signals in async mode. Its worker exit invokes this through
|
|
25
|
+
# at_exit, so shutdown must be safe when callbacks run more than once and when
|
|
26
|
+
# an application was preloaded before Puma forked its workers.
|
|
27
|
+
def shutdown
|
|
28
|
+
return unless @worker && @worker_pid == Process.pid
|
|
29
|
+
@shutdown_mutex ||= Mutex.new
|
|
30
|
+
@shutdown_mutex.synchronize do
|
|
31
|
+
return if @shutdown_complete
|
|
32
|
+
@worker.shutdown
|
|
33
|
+
@shutdown_complete = true
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
23
37
|
def standalone(**options)
|
|
24
38
|
@config.merge!(options)
|
|
25
39
|
@config[:standalone] = true
|
|
@@ -34,9 +48,12 @@ module Skiplock
|
|
|
34
48
|
Signal.trap('HUP') { setup_logger }
|
|
35
49
|
Worker.cleanup(@hostname)
|
|
36
50
|
@worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname)
|
|
51
|
+
@worker_pid = Process.pid
|
|
52
|
+
Cron.setup if @worker.master
|
|
37
53
|
ActiveRecord::Base.connection.disconnect! if @config[:workers] > 1
|
|
54
|
+
@child_pids = []
|
|
38
55
|
(@config[:workers] - 1).times do |n|
|
|
39
|
-
fork do
|
|
56
|
+
@child_pids << fork do
|
|
40
57
|
sleep(0.25*n + 1)
|
|
41
58
|
ActiveRecord::Base.establish_connection
|
|
42
59
|
worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname, master: false)
|
|
@@ -55,8 +72,13 @@ module Skiplock
|
|
|
55
72
|
break if @shutdown
|
|
56
73
|
end
|
|
57
74
|
@logger.info "[Skiplock] Terminating signal... Waiting for jobs to finish (up to #{@config[:graceful_shutdown]} seconds)..." if @config[:graceful_shutdown]
|
|
58
|
-
Process.
|
|
59
|
-
@
|
|
75
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @config[:graceful_shutdown] if @config[:graceful_shutdown]
|
|
76
|
+
signal_children(@child_pids, 'TERM')
|
|
77
|
+
begin
|
|
78
|
+
shutdown
|
|
79
|
+
ensure
|
|
80
|
+
reap_children(@child_pids, deadline)
|
|
81
|
+
end
|
|
60
82
|
rescue Exception => ex
|
|
61
83
|
@logger.error(ex.to_s)
|
|
62
84
|
@logger.error(ex.backtrace.join("\n"))
|
|
@@ -64,6 +86,32 @@ module Skiplock
|
|
|
64
86
|
|
|
65
87
|
private
|
|
66
88
|
|
|
89
|
+
def signal_children(child_pids, signal)
|
|
90
|
+
child_pids.each do |pid|
|
|
91
|
+
Process.kill(signal, pid)
|
|
92
|
+
rescue Errno::ESRCH
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def reap_children(child_pids, deadline)
|
|
97
|
+
remaining = child_pids.dup
|
|
98
|
+
while remaining.any?
|
|
99
|
+
remaining.delete_if do |pid|
|
|
100
|
+
Process.waitpid(pid, Process::WNOHANG)
|
|
101
|
+
rescue Errno::ECHILD
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
break if remaining.empty?
|
|
105
|
+
break if deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
106
|
+
sleep 0.05
|
|
107
|
+
end
|
|
108
|
+
signal_children(remaining, 'KILL')
|
|
109
|
+
remaining.each do |pid|
|
|
110
|
+
Process.waitpid(pid)
|
|
111
|
+
rescue Errno::ECHILD
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
67
115
|
def banner
|
|
68
116
|
title = "Skiplock #{Skiplock::VERSION} (Rails #{Rails::VERSION::STRING} | Ruby #{RUBY_VERSION}-p#{RUBY_PATCHLEVEL})"
|
|
69
117
|
@logger.info "-"*(title.length)
|
data/lib/skiplock/patch.rb
CHANGED
|
File without changes
|
data/lib/skiplock/version.rb
CHANGED
data/lib/skiplock/worker.rb
CHANGED
|
@@ -19,10 +19,25 @@ module Skiplock
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def shutdown
|
|
22
|
-
@
|
|
23
|
-
@
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
@shutdown_mutex ||= Mutex.new
|
|
23
|
+
@shutdown_mutex.synchronize do
|
|
24
|
+
return if @shutdown_complete
|
|
25
|
+
@running = false
|
|
26
|
+
@dispatch_mutex ||= Mutex.new
|
|
27
|
+
if @executor
|
|
28
|
+
@dispatch_mutex.synchronize { @executor.shutdown }
|
|
29
|
+
@executor.kill unless @executor.wait_for_termination((@config || {})[:graceful_shutdown])
|
|
30
|
+
end
|
|
31
|
+
@shutdown_complete = true
|
|
32
|
+
begin
|
|
33
|
+
self.delete
|
|
34
|
+
rescue StandardError => ex
|
|
35
|
+
Skiplock.logger.error("[Skiplock] Unable to delete worker #{self.id} during shutdown: #{ex}") if Skiplock.logger
|
|
36
|
+
end
|
|
37
|
+
worker_num = @num.to_i
|
|
38
|
+
worker_count = (@config || {})[:workers].to_i
|
|
39
|
+
Skiplock.logger.info "[Skiplock] Shutdown of #{self.master ? 'master' : 'cluster'} worker#{(' ' + worker_num.to_s) if worker_num > 0 && worker_count > 2} (PID: #{self.pid}) was completed." if Skiplock.logger
|
|
40
|
+
end
|
|
26
41
|
end
|
|
27
42
|
|
|
28
43
|
def start(worker_num: 0, **config)
|
|
@@ -32,6 +47,7 @@ module Skiplock
|
|
|
32
47
|
@namespace_query = Skiplock.namespace.nil? ? "namespace IS NULL" : "namespace = '#{Skiplock.namespace}'"
|
|
33
48
|
@queues_order_query = @config[:queues].map { |q,v| "WHEN queue_name = '#{q}' THEN #{v}" }.join(' ') if @config[:queues].is_a?(Hash) && @config[:queues].count > 0
|
|
34
49
|
@running = true
|
|
50
|
+
@dispatch_mutex = Mutex.new
|
|
35
51
|
@map = ::PG::TypeMapByOid.new
|
|
36
52
|
@map.add_coder(::PG::TextDecoder::Boolean.new(oid: 16, name: 'bool'))
|
|
37
53
|
@map.add_coder(::PG::TextDecoder::Integer.new(oid: 20, name: 'int8'))
|
|
@@ -73,18 +89,22 @@ module Skiplock
|
|
|
73
89
|
next_schedule_at = Time.now.to_f
|
|
74
90
|
end
|
|
75
91
|
if Time.now.to_f >= next_schedule_at && @executor.remaining_capacity > 1 # reserves 1 slot in queue for Job.flush in case of pg_connection error
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
@dispatch_mutex.synchronize do
|
|
93
|
+
if @running
|
|
94
|
+
result = nil
|
|
95
|
+
@connection.transaction do |conn|
|
|
96
|
+
conn.exec("SELECT id, running, scheduled_at FROM skiplock.jobs WHERE running = FALSE AND expired_at IS NULL AND finished_at IS NULL AND #{@namespace_query} ORDER BY scheduled_at ASC NULLS FIRST,#{@queues_order_query ? ' CASE ' + @queues_order_query + ' ELSE NULL END ASC NULLS LAST,' : ''} priority ASC NULLS LAST, created_at ASC FOR UPDATE SKIP LOCKED LIMIT 1") do |r|
|
|
97
|
+
result = r.first
|
|
98
|
+
conn.exec("UPDATE skiplock.jobs SET running = TRUE, worker_id = '#{self.id}', updated_at = NOW() WHERE id = '#{result['id']}' RETURNING *") { |r| result = r.first } if result && result['scheduled_at'].to_f <= Time.now.to_f
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
if result && result['running']
|
|
102
|
+
@executor.post { Rails.application.executor.wrap { Job.instantiate(result).execute(purge_completion: @config[:purge_completion], max_retries: @config[:max_retries]) } }
|
|
103
|
+
else
|
|
104
|
+
next_schedule_at = (result ? result['scheduled_at'].to_f : Float::INFINITY)
|
|
105
|
+
end
|
|
81
106
|
end
|
|
82
107
|
end
|
|
83
|
-
if result && result['running']
|
|
84
|
-
@executor.post { Rails.application.executor.wrap { Job.instantiate(result).execute(purge_completion: @config[:purge_completion], max_retries: @config[:max_retries]) } }
|
|
85
|
-
else
|
|
86
|
-
next_schedule_at = (result ? result['scheduled_at'].to_f : Float::INFINITY)
|
|
87
|
-
end
|
|
88
108
|
end
|
|
89
109
|
notifications = { 'skiplock::jobs' => [], 'skiplock::workers' => [] }
|
|
90
110
|
@connection.wait_for_notify(0.2) do |channel, pid, payload|
|
data/lib/skiplock.rb
CHANGED
|
File without changes
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: skiplock
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tin Vo
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activejob
|
|
@@ -66,6 +65,48 @@ dependencies:
|
|
|
66
65
|
- - "~>"
|
|
67
66
|
- !ruby/object:Gem::Version
|
|
68
67
|
version: '0.1'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: minitest
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '5.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '5.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: pg
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: rake
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '12.0'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '12.0'
|
|
69
110
|
description: High performance ActiveJob Queue Adapter for PostgreSQL that provides
|
|
70
111
|
maximum reliability and ACID compliance
|
|
71
112
|
email:
|
|
@@ -94,7 +135,6 @@ homepage: https://github.com/vtt/skiplock
|
|
|
94
135
|
licenses:
|
|
95
136
|
- MIT
|
|
96
137
|
metadata: {}
|
|
97
|
-
post_install_message:
|
|
98
138
|
rdoc_options: []
|
|
99
139
|
require_paths:
|
|
100
140
|
- lib
|
|
@@ -109,8 +149,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
109
149
|
- !ruby/object:Gem::Version
|
|
110
150
|
version: '0'
|
|
111
151
|
requirements: []
|
|
112
|
-
rubygems_version:
|
|
113
|
-
signing_key:
|
|
152
|
+
rubygems_version: 4.0.19
|
|
114
153
|
specification_version: 4
|
|
115
154
|
summary: ActiveJob Queue Adapter for PostgreSQL
|
|
116
155
|
test_files: []
|