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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9731224db727e3ebe4b32b94b4edff6bf7a53faca14be5a6f5d7cc20a02836e9
4
- data.tar.gz: 9c3cb7ccf0d98d7e8ec4bcd21e33269504d166804f0679fe7d286290527a8457
3
+ metadata.gz: c53cdb42608339ffd7ba7532393e933ce9128757ee35679a665fd2f1e1d0fba3
4
+ data.tar.gz: e7dfcc14eed82fef12747d9c0172de006cdb8823fdcaa1d7e944b8d0bde71be1
5
5
  SHA512:
6
- metadata.gz: 560b30ea96a8f255811b1c8a0beb7e161ba1edd2dde238e52cd31625e1a4e4d729ff24f3d4d37e4a29fbb2c3e2543e86b9cb1720553cacab346ac30db2970aea
7
- data.tar.gz: '093b4ee02119973dbd1c8c905d2ccbcc1d6d9efad440ba23ba8fc05a8a2a3580ec7a7d3c7c47d5f6077b789a440e4c46e455f81a5c10fd032b01ffefae906a0e'
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 `skiplock` to your application's Gemfile:
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 running = 'f' AND expired_at IS NULL AND finished_at IS NULL"
31
- execute "CREATE INDEX jobs_retry_index ON skiplock.jobs(scheduled_at) WHERE running = 'f' AND executions IS NOT NULL AND expired_at IS NULL AND finished_at IS NULL"
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
@@ -6,6 +6,7 @@ require 'skiplock/dispatcher'
6
6
  require 'skiplock/manager'
7
7
  require 'skiplock/notification'
8
8
  require 'skiplock/job'
9
+ require 'skiplock/worker'
9
10
  require 'skiplock/version'
10
11
 
11
12
  module Skiplock
@@ -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, queue, priority, time = n.split(',')
46
- if time.to_f <= Time.now.to_f
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 time.to_f < @next_schedule_at
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
- timestamp = Time.now
72
+ t = Time.now
57
73
  while @running
58
74
  sleep(0.5)
59
- break if Time.now - timestamp > 10
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(connection: connection)
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(connection: ActiveRecord::Base.connection)
12
- connection.exec_query('BEGIN')
13
- job = connection.exec_query("SELECT * FROM #{self.table_name} WHERE running = 'f' 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
14
- if job && job['scheduled_at'].to_f <= Time.now.to_f # found job ready to perform
15
- # update the job to mark it in progress in case database server goes down during job execution
16
- connection.exec_query("UPDATE #{self.table_name} SET running = 't' WHERE id = '#{job['id']}'")
17
- connection.exec_query('END') # close the transaction commit the state of job in progress
18
- executions = (job['executions'] || 0) + 1
19
- exceptions = job['exception_executions'] ? JSON.parse(job['exception_executions']) : {}
20
- data = job['data'] ? JSON.parse(job['data']) : {}
21
- job_data = job.slice('job_class', 'queue_name', 'locale', 'timezone', 'priority', 'executions').merge('job_id' => job['id'], 'exception_executions' => exceptions, 'enqueued_at' => job['updated_at']).merge(data)
22
- Thread.current[:skiplock_dispatch_data] = job_data
23
- begin
24
- ActiveJob::Base.execute(job_data)
25
- rescue Exception => ex
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
- if ex
28
- # TODO: report exception
29
- exceptions["[#{ex.class.name}]"] = (exceptions["[#{ex.class.name}]"] || 0) + 1 unless exceptions.key?('activejob_retry')
30
- if executions >= Settings['max_retries'] || exceptions.key?('activejob_retry')
31
- connection.exec_query("UPDATE #{self.table_name} SET running = 'f', executions = #{executions}, exception_executions = '#{connection.quote_string(exceptions.to_json.to_s)}', expired_at = NOW(), updated_at = NOW() WHERE id = '#{job['id']}' RETURNING *").first
32
- else
33
- timestamp = Time.now + (5 * 2**executions)
34
- connection.exec_query("UPDATE #{self.table_name} SET running = 'f', executions = #{executions}, exception_executions = '#{connection.quote_string(exceptions.to_json.to_s)}', scheduled_at = TO_TIMESTAMP(#{timestamp.to_f}), updated_at = NOW() WHERE id = '#{job['id']}' RETURNING *").first
35
- end
36
- elsif exceptions.key?('activejob_retry')
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
- connection.exec_query("UPDATE #{self.table_name} SET running = 'f', executions = #{executions}, exception_executions = NULL, finished_at = NOW(), updated_at = NOW() WHERE id = '#{job['id']}' RETURNING *").first
46
+ job.delete
50
47
  end
48
+ elsif Settings['purge_completion']
49
+ job.delete
51
50
  else
52
- connection.exec_query('END')
53
- job ? job['scheduled_at'].to_f : Float::INFINITY
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(job, timestamp)
60
- if Thread.current[:skiplock_dispatch_data]
61
- job.exception_executions['activejob_retry'] = true
62
- Thread.current[:skiplock_dispatch_data]['executions'] = job.executions
63
- Thread.current[:skiplock_dispatch_data]['exception_executions'] = job.exception_executions
64
- Thread.current[:skiplock_dispatch_data]['scheduled_at'] = Time.at(timestamp)
65
- self.new(Thread.current[:skiplock_dispatch_data].slice(*self.column_names).merge(id: job.job_id))
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
- timestamp = Time.at(timestamp) if timestamp
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
@@ -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
- Settings['workers'].times do |w|
69
- fork do
70
- Process.setproctitle("skiplock-worker[#{w+1}]")
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
- Process.setproctitle("skiplock-master")
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
@@ -1,4 +1,4 @@
1
1
  module Skiplock
2
- VERSION = Version = '1.0.3'
2
+ VERSION = Version = '1.0.4'
3
3
  end
4
4
 
@@ -0,0 +1,5 @@
1
+ module Skiplock
2
+ class Worker < ActiveRecord::Base
3
+ self.table_name = 'skiplock.workers'
4
+ end
5
+ end
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.3
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-17 00:00:00.000000000 Z
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
- rubygems_version: 3.0.3
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