skiplock 1.0.3 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +6 -6
- data/lib/generators/skiplock/templates/migration.rb.erb +12 -3
- data/lib/skiplock.rb +1 -0
- data/lib/skiplock/dispatcher.rb +27 -12
- data/lib/skiplock/job.rb +53 -53
- data/lib/skiplock/manager.rb +5 -6
- data/lib/skiplock/version.rb +1 -1
- data/lib/skiplock/worker.rb +5 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c53cdb42608339ffd7ba7532393e933ce9128757ee35679a665fd2f1e1d0fba3
|
4
|
+
data.tar.gz: e7dfcc14eed82fef12747d9c0172de006cdb8823fdcaa1d7e944b8d0bde71be1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b32ac8c7b3cf90be24c86310d4e2b2fe250a5ec7b244aad267144f49e09f14d9a46e64214f45afde8093d4a867f1c93245c5e216ac214cacca555f3965770f7c
|
7
|
+
data.tar.gz: 0a61bf8d14f3162fe981420d43278642ac98a4ab5ac6705bc9a9f65ddd3330e26b6ec1d3ce9a196155e2316ed972954cd657f4d36a48bda83554ea97eb205237
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Skiplock
|
2
2
|
|
3
|
-
Skiplock is a background job queuing system that improves the performance and reliability of the job executions while providing the same ACID guarantees as the rest of your data. It is designed for Active Jobs with Ruby on Rails using PostgreSQL database adapter, but it can be modified to work with other frameworks easily.
|
3
|
+
`Skiplock` is a background job queuing system that improves the performance and reliability of the job executions while providing the same ACID guarantees as the rest of your data. It is designed for Active Jobs with Ruby on Rails using PostgreSQL database adapter, but it can be modified to work with other frameworks easily.
|
4
4
|
|
5
5
|
It only uses the `LISTEN/NOTIFY/SKIP LOCKED` features provided natively on PostgreSQL 9.5+ to efficiently and reliably dispatch jobs to worker processes and threads ensuring that each job can be completed successfully **only once**. No other polling or timer is needed.
|
6
6
|
|
@@ -14,7 +14,7 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
|
|
14
14
|
|
15
15
|
## Installation
|
16
16
|
|
17
|
-
1. Add `
|
17
|
+
1. Add `Skiplock` to your application's Gemfile:
|
18
18
|
|
19
19
|
```ruby
|
20
20
|
gem 'skiplock'
|
@@ -26,7 +26,7 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
|
|
26
26
|
$ bundle install
|
27
27
|
```
|
28
28
|
|
29
|
-
3. Run the Skiplock install generator. This will generate a configuration file and database migration to store the job records:
|
29
|
+
3. Run the `Skiplock` install generator. This will generate a configuration file and database migration to store the job records:
|
30
30
|
|
31
31
|
```bash
|
32
32
|
$ rails g skiplock:install
|
@@ -46,7 +46,7 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
|
|
46
46
|
# config/application.rb
|
47
47
|
config.active_job.queue_adapter = :skiplock
|
48
48
|
```
|
49
|
-
2. Skiplock configuration
|
49
|
+
2. `Skiplock` configuration
|
50
50
|
```yaml
|
51
51
|
# config/skiplock.yml
|
52
52
|
---
|
@@ -104,7 +104,7 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
|
|
104
104
|
INSERT INTO skiplock.jobs(job_class,priority,scheduled_at,data) VALUES ('MyJob',10,NOW()+INTERVAL '5 min','{"arguments":[1,2,3]}');
|
105
105
|
```
|
106
106
|
## Cron system
|
107
|
-
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.
|
107
|
+
`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.
|
108
108
|
- setup `MyJob` to run as cron job every hour at 30 minutes past
|
109
109
|
|
110
110
|
```ruby
|
@@ -123,7 +123,7 @@ Skiplock provides the capability to setup cron jobs for running tasks periodical
|
|
123
123
|
- to remove the cron schedule from the job, simply comment out the constant definition or delete the line then re-deploy the application. At startup, the cron jobs that were undefined will be removed automatically
|
124
124
|
|
125
125
|
## Retry system
|
126
|
-
Skiplock fully supports ActiveJob built-in retry system. It also has its own retry system for fallback. To use ActiveJob retry system, define the rescue blocks per ActiveJob's documentation.
|
126
|
+
`Skiplock` fully supports ActiveJob built-in retry system. It also has its own retry system for fallback. To use ActiveJob retry system, define the rescue blocks per ActiveJob's documentation.
|
127
127
|
- configures `MyJob` to retry at maximum 20 attempts on StandardError with fixed delay of 5 seconds
|
128
128
|
```ruby
|
129
129
|
class MyJob < ActiveJob::Base
|
@@ -2,6 +2,7 @@ class CreateSkiplockSchema < ActiveRecord::Migration<%= "[#{ActiveRecord::VERSIO
|
|
2
2
|
def up
|
3
3
|
execute 'CREATE SCHEMA skiplock'
|
4
4
|
create_table 'skiplock.jobs', id: :uuid do |t|
|
5
|
+
t.uuid :worker_id, index: true
|
5
6
|
t.string :job_class, null: false
|
6
7
|
t.string :cron
|
7
8
|
t.string :queue_name
|
@@ -11,12 +12,20 @@ class CreateSkiplockSchema < ActiveRecord::Migration<%= "[#{ActiveRecord::VERSIO
|
|
11
12
|
t.integer :executions
|
12
13
|
t.jsonb :exception_executions
|
13
14
|
t.jsonb :data
|
14
|
-
t.boolean :running, null: false, default: false
|
15
|
+
t.boolean :running, null: false, default: false, index: true
|
15
16
|
t.timestamp :expired_at
|
16
17
|
t.timestamp :finished_at
|
17
18
|
t.timestamp :scheduled_at
|
18
19
|
t.timestamps null: false, default: -> { 'now()' }
|
19
20
|
end
|
21
|
+
create_table 'skiplob.workers', id: :uuid do |t|
|
22
|
+
t.integer :pid, null: false, index: true
|
23
|
+
t.integer :ppid, index: true
|
24
|
+
t.integer :remaining_capacity, null: false
|
25
|
+
t.string :hostname, null: false, index: true
|
26
|
+
t.jsonb :data
|
27
|
+
t.timestamps null: false, index: true, default: -> { 'now()' }
|
28
|
+
end
|
20
29
|
execute %(CREATE OR REPLACE FUNCTION skiplock.notify() RETURNS TRIGGER AS $$
|
21
30
|
BEGIN
|
22
31
|
IF (NEW.finished_at IS NULL AND NEW.expired_at IS NULL) THEN
|
@@ -27,8 +36,8 @@ class CreateSkiplockSchema < ActiveRecord::Migration<%= "[#{ActiveRecord::VERSIO
|
|
27
36
|
$$ LANGUAGE plpgsql
|
28
37
|
)
|
29
38
|
execute "CREATE TRIGGER notify_job AFTER INSERT OR UPDATE ON skiplock.jobs FOR EACH ROW EXECUTE PROCEDURE skiplock.notify()"
|
30
|
-
execute "CREATE INDEX jobs_index ON skiplock.jobs(scheduled_at ASC NULLS FIRST, priority ASC NULLS LAST, created_at ASC) WHERE
|
31
|
-
execute "CREATE INDEX jobs_retry_index ON skiplock.jobs(scheduled_at) WHERE
|
39
|
+
execute "CREATE INDEX jobs_index ON skiplock.jobs(scheduled_at ASC NULLS FIRST, priority ASC NULLS LAST, created_at ASC) WHERE expired_at IS NULL AND finished_at IS NULL"
|
40
|
+
execute "CREATE INDEX jobs_retry_index ON skiplock.jobs(scheduled_at) WHERE executions IS NOT NULL AND expired_at IS NULL AND finished_at IS NULL"
|
32
41
|
execute "CREATE INDEX jobs_cron_index ON skiplock.jobs(scheduled_at ASC NULLS FIRST, priority ASC NULLS LAST, created_at ASC) WHERE cron IS NOT NULL AND finished_at IS NULL"
|
33
42
|
execute "CREATE UNIQUE INDEX jobs_unique_cron_index ON skiplock.jobs (job_class) WHERE cron IS NOT NULL"
|
34
43
|
end
|
data/lib/skiplock.rb
CHANGED
data/lib/skiplock/dispatcher.rb
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
module Skiplock
|
2
2
|
class Dispatcher
|
3
|
-
def initialize(master: true)
|
3
|
+
def initialize(master: true, worker_num: nil, worker_pids: [])
|
4
4
|
@executor = Concurrent::ThreadPoolExecutor.new(min_threads: Settings['min_threads'], max_threads: Settings['max_threads'], max_queue: Settings['max_threads'], idletime: 60, auto_terminate: true, fallback_policy: :discard)
|
5
5
|
@master = master
|
6
|
+
if @master
|
7
|
+
@worker_pids = worker_pids + [ Process.pid ]
|
8
|
+
else
|
9
|
+
@worker_num = worker_num
|
10
|
+
end
|
6
11
|
@next_schedule_at = Time.now.to_f
|
7
12
|
@running = true
|
8
13
|
end
|
@@ -11,9 +16,19 @@ module Skiplock
|
|
11
16
|
Thread.new do
|
12
17
|
Rails.application.reloader.wrap do
|
13
18
|
sleep(0.1) while @running && !Rails.application.initialized?
|
19
|
+
Process.setproctitle("skiplock-#{@master ? 'master' : 'worker[' + @worker_num.to_s + ']'}") if Settings['workers'] > 0
|
14
20
|
ActiveRecord::Base.connection_pool.with_connection do |connection|
|
15
|
-
connection.exec_query('LISTEN skiplock')
|
21
|
+
connection.exec_query('LISTEN "skiplock::jobs"')
|
22
|
+
hostname = `hostname`.strip
|
23
|
+
@worker = Worker.create!(pid: Process.pid, ppid: (@master ? nil : Process.ppid), capacity: Settings['max_threads'], hostname: hostname)
|
16
24
|
if @master
|
25
|
+
# get worker ids that were not shutdown properly on the host
|
26
|
+
dead_worker_ids = Worker.where(hostname: hostname).where.not(pid: @worker_pids).ids
|
27
|
+
if dead_worker_ids.count > 0
|
28
|
+
# reset orphaned jobs of the dead worker ids for retry
|
29
|
+
Job.where(running: true, worker_id: dead_worker_ids).update_all(running: false, worker_id: nil)
|
30
|
+
Worker.where(id: dead_worker_ids).delete_all
|
31
|
+
end
|
17
32
|
# reset retries schedules on startup
|
18
33
|
Job.where('scheduled_at > NOW() AND executions IS NOT NULL AND expired_at IS NULL AND finished_at IS NULL').update_all(scheduled_at: nil, updated_at: Time.now)
|
19
34
|
Cron.setup
|
@@ -25,7 +40,7 @@ module Skiplock
|
|
25
40
|
unless connection.active?
|
26
41
|
connection.reconnect!
|
27
42
|
sleep(0.5)
|
28
|
-
connection.exec_query('LISTEN skiplock')
|
43
|
+
connection.exec_query('LISTEN "skiplock::jobs"')
|
29
44
|
@next_schedule_at = Time.now
|
30
45
|
end
|
31
46
|
error = false
|
@@ -42,10 +57,11 @@ module Skiplock
|
|
42
57
|
notifications << payload[:extra]
|
43
58
|
end
|
44
59
|
notifications.each do |n|
|
45
|
-
op, id,
|
46
|
-
if
|
60
|
+
op, id, worker_id, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
|
61
|
+
next if op == 'DELETE' || running == 'true' || expired_at || finished_at
|
62
|
+
if scheduled_at.to_f <= Time.now.to_f
|
47
63
|
@next_schedule_at = Time.now.to_f
|
48
|
-
elsif
|
64
|
+
elsif scheduled_at.to_f < @next_schedule_at
|
49
65
|
@next_schedule_at = time.to_f
|
50
66
|
end
|
51
67
|
end
|
@@ -53,10 +69,10 @@ module Skiplock
|
|
53
69
|
rescue Exception => ex
|
54
70
|
# TODO: Report exception
|
55
71
|
error = true
|
56
|
-
|
72
|
+
t = Time.now
|
57
73
|
while @running
|
58
74
|
sleep(0.5)
|
59
|
-
break if Time.now -
|
75
|
+
break if Time.now - t > 5
|
60
76
|
end
|
61
77
|
end
|
62
78
|
sleep(0.1)
|
@@ -71,22 +87,21 @@ module Skiplock
|
|
71
87
|
@running = false
|
72
88
|
@executor.shutdown
|
73
89
|
@executor.wait_for_termination if wait
|
90
|
+
@worker.delete if @worker
|
74
91
|
end
|
75
92
|
|
76
93
|
private
|
77
94
|
|
78
95
|
def do_work
|
79
|
-
connection = ActiveRecord::Base.connection_pool.checkout
|
80
96
|
while @running
|
81
|
-
result = Job.dispatch(
|
97
|
+
result = Job.dispatch(worker_id: @worker.id)
|
82
98
|
next if result.is_a?(Hash)
|
83
99
|
@next_schedule_at = result if result.is_a?(Float)
|
84
100
|
break
|
85
101
|
end
|
86
102
|
rescue Exception => e
|
103
|
+
puts "Exception => #{e.message} #{e.backtrace}"
|
87
104
|
# TODO: Report exception
|
88
|
-
ensure
|
89
|
-
ActiveRecord::Base.connection_pool.checkin(connection)
|
90
105
|
end
|
91
106
|
end
|
92
107
|
end
|
data/lib/skiplock/job.rb
CHANGED
@@ -2,70 +2,70 @@ module Skiplock
|
|
2
2
|
class Job < ActiveRecord::Base
|
3
3
|
self.table_name = 'skiplock.jobs'
|
4
4
|
|
5
|
-
# Accept: An active ActiveRecord database connection (eg. ActiveRecord::Base.connection)
|
6
|
-
# The connection should be checked out using ActiveRecord::Base.connection_pool.checkout, and be checked
|
7
|
-
# in using ActiveRecord::Base.conection_pool.checkin once all of the job dispatches have been completed.
|
8
|
-
# *** IMPORTANT: This connection cannot be shared with the job's execution
|
9
|
-
#
|
10
5
|
# Return: Attributes hash of the Job if it was executed; otherwise returns the next Job's schedule time in FLOAT
|
11
|
-
def self.dispatch(
|
12
|
-
connection.exec_query('BEGIN')
|
13
|
-
job =
|
14
|
-
if job
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
6
|
+
def self.dispatch(worker_id: nil)
|
7
|
+
self.connection.exec_query('BEGIN')
|
8
|
+
job = self.find_by_sql("SELECT id, scheduled_at FROM #{self.table_name} WHERE running = FALSE AND expired_at IS NULL AND finished_at IS NULL ORDER BY scheduled_at ASC NULLS FIRST, priority ASC NULLS LAST, created_at ASC FOR UPDATE SKIP LOCKED LIMIT 1").first
|
9
|
+
if job.nil? || job.scheduled_at.to_f > Time.now.to_f
|
10
|
+
self.connection.exec_query('END')
|
11
|
+
return (job ? job.scheduled_at.to_f : Float::INFINITY)
|
12
|
+
end
|
13
|
+
job = Skiplock::Job.find_by_sql("UPDATE #{self.table_name} SET running = TRUE, worker_id = #{self.connection.quote(worker_id)} WHERE id = '#{job.id}' RETURNING *").first
|
14
|
+
self.connection.exec_query('END')
|
15
|
+
job.data ||= {}
|
16
|
+
job.exception_executions ||= {}
|
17
|
+
job_data = job.attributes.slice('job_class', 'queue_name', 'locale', 'timezone', 'priority', 'executions', 'exception_executions').merge('job_id' => job.id, 'enqueued_at' => job.updated_at, 'arguments' => (job.data['arguments'] || []))
|
18
|
+
job.executions = (job.executions || 0) + 1
|
19
|
+
Thread.current[:skiplock_dispatch_job] = job
|
20
|
+
begin
|
21
|
+
ActiveJob::Base.execute(job_data)
|
22
|
+
rescue Exception => ex
|
23
|
+
end
|
24
|
+
job.running = false
|
25
|
+
if ex
|
26
|
+
# TODO: report exception
|
27
|
+
job.exception_executions["[#{ex.class.name}]"] = (job.exception_executions["[#{ex.class.name}]"] || 0) + 1 unless job.exception_executions.key?('activejob_retry')
|
28
|
+
if job.executions >= Settings['max_retries'] || job.exception_executions.key?('activejob_retry')
|
29
|
+
job.expired_at = Time.now
|
30
|
+
job.save!
|
31
|
+
else
|
32
|
+
job.scheduled_at = Time.now + (5 * 2**job.executions)
|
33
|
+
job.save!
|
26
34
|
end
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
connection.exec_query("UPDATE #{self.table_name} SET running = 'f', executions = #{job_data['executions']}, exception_executions = '#{connection.quote_string(job_data['exception_executions'].to_json.to_s)}', scheduled_at = TO_TIMESTAMP(#{job_data['scheduled_at'].to_f}), updated_at = NOW() WHERE id = '#{job['id']}' RETURNING *").first
|
38
|
-
elsif job['cron']
|
39
|
-
data['last_cron_run'] = Time.now.utc.to_s
|
40
|
-
next_cron_at = Cron.next_schedule_at(job['cron'])
|
41
|
-
if next_cron_at
|
42
|
-
connection.exec_query("UPDATE #{self.table_name} SET running = 'f', scheduled_at = TO_TIMESTAMP(#{next_cron_at}), executions = 1, exception_executions = NULL, data = '#{connection.quote_string(data.to_json.to_s)}', updated_at = NOW() WHERE id = '#{job['id']}' RETURNING *").first
|
43
|
-
else
|
44
|
-
connection.exec_query("DELETE FROM #{self.table_name} WHERE id = '#{job['id']}' RETURNING *").first
|
45
|
-
end
|
46
|
-
elsif Settings['purge_completion']
|
47
|
-
connection.exec_query("DELETE FROM #{self.table_name} WHERE id = '#{job['id']}' RETURNING *").first
|
35
|
+
elsif job.exception_executions.key?('activejob_retry')
|
36
|
+
job.save!
|
37
|
+
elsif job['cron']
|
38
|
+
job.data['last_cron_run'] = Time.now.utc.to_s
|
39
|
+
next_cron_at = Cron.next_schedule_at(job['cron'])
|
40
|
+
if next_cron_at
|
41
|
+
job.executions = 1
|
42
|
+
job.exception_executions = nil
|
43
|
+
job.scheduled_at = Time.at(next_cron_at)
|
44
|
+
job.save!
|
48
45
|
else
|
49
|
-
|
46
|
+
job.delete
|
50
47
|
end
|
48
|
+
elsif Settings['purge_completion']
|
49
|
+
job.delete
|
51
50
|
else
|
52
|
-
|
53
|
-
job
|
51
|
+
job.finished_at = Time.now
|
52
|
+
job.exception_executions = nil
|
53
|
+
job.save!
|
54
54
|
end
|
55
|
+
job
|
55
56
|
ensure
|
56
57
|
Thread.current[:skiplock_dispatch_data] = nil
|
57
58
|
end
|
58
59
|
|
59
|
-
def self.enqueue_at(
|
60
|
-
if
|
61
|
-
|
62
|
-
Thread.current[:
|
63
|
-
Thread.current[:
|
64
|
-
Thread.current[:
|
65
|
-
|
60
|
+
def self.enqueue_at(activejob, timestamp)
|
61
|
+
timestamp = Time.at(timestamp) if timestamp
|
62
|
+
if Thread.current[:skiplock_dispatch_job].try(:id) == activejob.job_id
|
63
|
+
Thread.current[:skiplock_dispatch_job].exception_executions = activejob.exception_executions.merge('activejob_retry' => true)
|
64
|
+
Thread.current[:skiplock_dispatch_job].executions = activejob.executions
|
65
|
+
Thread.current[:skiplock_dispatch_job].scheduled_at = timestamp
|
66
|
+
Thread.current[:skiplock_dispatch_job]
|
66
67
|
else
|
67
|
-
|
68
|
-
Job.create!(id: job.job_id, job_class: job.class.name, queue_name: job.queue_name, locale: job.locale, timezone: job.timezone, priority: job.priority, executions: job.executions, data: { 'arguments' => job.arguments }, scheduled_at: timestamp)
|
68
|
+
Job.create!(id: activejob.job_id, job_class: activejob.class.name, queue_name: activejob.queue_name, locale: activejob.locale, timezone: activejob.timezone, priority: activejob.priority, executions: activejob.executions, data: { 'arguments' => activejob.serialize['arguments'] }, scheduled_at: timestamp)
|
69
69
|
end
|
70
70
|
end
|
71
71
|
end
|
data/lib/skiplock/manager.rb
CHANGED
@@ -65,10 +65,10 @@ module Skiplock
|
|
65
65
|
shutdown = false
|
66
66
|
Signal.trap("INT") { shutdown = true }
|
67
67
|
Signal.trap("TERM") { shutdown = true }
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
dispatcher = Dispatcher.new(master: false)
|
68
|
+
worker_pids = []
|
69
|
+
Settings['workers'].times do |n|
|
70
|
+
worker_pids << fork do
|
71
|
+
dispatcher = Dispatcher.new(master: false, worker_num: n)
|
72
72
|
thread = dispatcher.run
|
73
73
|
loop do
|
74
74
|
sleep 0.5
|
@@ -80,8 +80,7 @@ module Skiplock
|
|
80
80
|
end
|
81
81
|
end
|
82
82
|
sleep 0.1
|
83
|
-
|
84
|
-
dispatcher = Dispatcher.new
|
83
|
+
dispatcher = Dispatcher.new(worker_pids: worker_pids)
|
85
84
|
thread = dispatcher.run
|
86
85
|
loop do
|
87
86
|
sleep 0.5
|
data/lib/skiplock/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: skiplock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tin Vo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-03-
|
11
|
+
date: 2021-03-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -88,6 +88,7 @@ files:
|
|
88
88
|
- lib/skiplock/manager.rb
|
89
89
|
- lib/skiplock/notification.rb
|
90
90
|
- lib/skiplock/version.rb
|
91
|
+
- lib/skiplock/worker.rb
|
91
92
|
homepage: https://github.com/vtt/skiplock
|
92
93
|
licenses:
|
93
94
|
- MIT
|
@@ -107,7 +108,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
108
|
- !ruby/object:Gem::Version
|
108
109
|
version: '0'
|
109
110
|
requirements: []
|
110
|
-
|
111
|
+
rubyforge_project:
|
112
|
+
rubygems_version: 2.7.6
|
111
113
|
signing_key:
|
112
114
|
specification_version: 4
|
113
115
|
summary: ActiveJob Queue Adapter for PostgreSQL
|