skiplock 1.0.7 → 1.0.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b5f33c5c4f2f2223b68f6d025989cc3eb6568fb52a7e39ef3bf4c48a1807ec3
4
- data.tar.gz: 3839f01f8771b1ec06a0ab33c32555d4aa7ad0a48e142d0224fe46868692bfa5
3
+ metadata.gz: 2ee6a8ff68af1a1029a26db98867457a798c4fb6aacf0a2a2816efd3ef2b977b
4
+ data.tar.gz: 2c75fb21c79346f4525b8634b28fb326505acb7ab61795fe7d291c3faa75244f
5
5
  SHA512:
6
- metadata.gz: 7bf28c36dca7f41518c6d5be2651ff7b40e0f62c526248a478db4a88545b7663260225de5b2c6eacf89e17e34cf24ac2fd38c0bbf95c4cac54a8f57f68fd772a
7
- data.tar.gz: 3998b4377bbe2dd0b993ac9abcf848c6164fcb4378387cc8c7187d507c6fc6df1676fdba30ce98e26ac41730d0569a64197317c52447b1e8d6a06c93fb313fa7
6
+ metadata.gz: c34fa2c27f9fc8bcfbe6c54a30de8645418dfe7c13e8db9ea8c30352f75abd9ab9b04d1a294aea7f2a9f33772d9cc5ed96839bd63d1eacbda17749758b4755b2
7
+ data.tar.gz: 03cdbef1a70a64fb188258dc44049f9323f53cb9c2e64f8a79a9c8a4a17ca8aea2e7ca718ceb3bc78cc08fd61d6dbd5c3f104f653a9645132ef31f73d22883da
data/README.md CHANGED
@@ -72,7 +72,7 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
72
72
  - **workers** (*integer*) sets the maximum number of processes when running in standalone mode using the `skiplock` executable; setting this to **0** will enable **async mode**
73
73
 
74
74
  #### Async mode
75
- 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 web server like Puma, then it should be configured as below:
75
+ 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 it should be configured as below:
76
76
  ```ruby
77
77
  # config/puma.rb
78
78
  before_fork do
@@ -80,14 +80,10 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
80
80
  Skiplock::Manager.shutdown
81
81
  end
82
82
 
83
- on_worker_boot do
84
- # ...
85
- Skiplock::Manager.start
86
- end
87
-
88
- on_worker_shutdown do
89
- # ...
90
- Skiplock::Manager.shutdown
83
+ after_worker_fork do |worker_index|
84
+ # restarts skiplock after all Puma workers have been started
85
+ # Skiplock runs in Puma master's process only
86
+ Skiplock::Manager.start if (worker_index + 1) == @options[:workers]
91
87
  end
92
88
  ```
93
89
 
@@ -36,7 +36,7 @@ class CreateSkiplockSchema < ActiveRecord::Migration<%= "[#{ActiveRecord::VERSIO
36
36
  ELSE
37
37
  record = NEW;
38
38
  END IF;
39
- PERFORM pg_notify('skiplock::jobs', CONCAT(TG_OP,',',record.id::TEXT,',',record.worker_id::TEXT,',',record.queue_name,',',record.running::TEXT,',',CAST(EXTRACT(EPOCH FROM record.expired_at) AS FLOAT)::TEXT,',',CAST(EXTRACT(EPOCH FROM record.finished_at) AS FLOAT)::TEXT,',',CAST(EXTRACT(EPOCH FROM record.scheduled_at) AS FLOAT)::TEXT));
39
+ PERFORM pg_notify('skiplock::jobs', CONCAT(TG_OP,',',record.id::TEXT,',',record.worker_id::TEXT,',',record.queue_name,',',record.running::TEXT,',',CAST(EXTRACT(EPOCH FROM record.expired_at) AS FLOAT)::TEXT,',',CAST(EXTRACT(EPOCH FROM record.finished_at) AS FLOAT)::TEXT,',',CAST(EXTRACT(EPOCH FROM CASE WHEN record.scheduled_at IS NULL THEN record.updated_at ELSE record.scheduled_at END) AS FLOAT)::TEXT));
40
40
  RETURN NULL;
41
41
  END;
42
42
  $$ LANGUAGE plpgsql
@@ -9,100 +9,112 @@ module Skiplock
9
9
  else
10
10
  @worker_num = worker_num
11
11
  end
12
+ @last_dispatch_at = 0
12
13
  @next_schedule_at = Time.now.to_f
13
14
  @running = true
14
15
  end
15
16
 
16
17
  def run
17
18
  Thread.new do
18
- Rails.application.reloader.wrap do
19
- sleep(0.1) while @running && !Rails.application.initialized?
20
- Process.setproctitle("skiplock-#{@master ? 'master[0]' : 'worker[' + @worker_num.to_s + ']'}") if Settings['workers'] > 0
21
- ActiveRecord::Base.connection_pool.with_connection do |connection|
22
- connection.exec_query('LISTEN "skiplock::jobs"')
23
- hostname = `hostname -f`.strip
24
- @worker = Worker.create!(pid: Process.pid, ppid: (@master ? nil : Process.ppid), capacity: Settings['max_threads'], hostname: hostname)
25
- if @master
26
- if File.exists?('tmp/cache/skiplock')
27
- # get performed jobs that could not sync with database
28
- job_ids = File.read('tmp/cache/skiplock').split("\n")
29
- if Settings['purge_completion']
30
- Job.where(id: job_ids, running: true).delete_all
31
- else
32
- Job.where(id: job_ids, running: true).update_all(running: false, finished_at: File.mtime('tmp/cache/skiplock'))
33
- end
34
- File.delete('tmp/cache/skiplock')
19
+ sleep(1) while @running && !Rails.application.initialized?
20
+ Process.setproctitle("skiplock-#{@master ? 'master[0]' : 'worker[' + @worker_num.to_s + ']'}") if Settings['workers'] > 0
21
+ ActiveRecord::Base.connection_pool.with_connection do |connection|
22
+ connection.exec_query('LISTEN "skiplock::jobs"')
23
+ hostname = `hostname -f`.strip
24
+ @worker = Worker.create!(pid: Process.pid, ppid: (@master ? nil : Process.ppid), capacity: Settings['max_threads'], hostname: hostname)
25
+ if @master
26
+ connection.exec_query('LISTEN "skiplock::workers"')
27
+ if File.exists?('tmp/cache/skiplock')
28
+ # get performed jobs that could not sync with database
29
+ job_ids = File.read('tmp/cache/skiplock').split("\n")
30
+ if Settings['purge_completion']
31
+ Job.where(id: job_ids).delete_all
32
+ else
33
+ Job.where(id: job_ids).update_all(running: false, finished_at: File.mtime('tmp/cache/skiplock'), updated_at: Time.now)
35
34
  end
36
- # get current worker ids
37
- worker_ids = Worker.where(hostname: hostname, pid: @worker_pids).ids
35
+ File.delete('tmp/cache/skiplock')
36
+ end
37
+ # get dead worker ids
38
+ dead_worker_ids = Worker.where(hostname: hostname).where.not(pid: @worker_pids).ids
39
+ if dead_worker_ids.count > 0
38
40
  # reset orphaned jobs of the dead worker ids for retry
39
- Job.where(running: true).where.not(worker_id: worker_ids).update_all(running: false, worker_id: nil)
40
- # remove workers that were not shutdown properly on the host
41
- Worker.where(hostname: hostname).where.not(pid: @worker_pids).delete_all
42
- # reset retries schedules on startup
43
- 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)
44
- Cron.setup
41
+ Job.where(running: true).where(worker_id: dead_worker_ids).update_all(running: false, worker_id: nil)
42
+ # remove dead workers
43
+ Worker.where(id: dead_worker_ids).delete_all
45
44
  end
46
- error = false
47
- while @running
48
- begin
49
- if error
50
- unless connection.active?
51
- connection.reconnect!
52
- sleep(0.5)
53
- connection.exec_query('LISTEN "skiplock::jobs"')
54
- @next_schedule_at = Time.now
55
- end
56
- error = false
45
+ # reset retries schedules on startup
46
+ 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)
47
+ Cron.setup
48
+ end
49
+ error = false
50
+ while @running
51
+ begin
52
+ if error
53
+ unless connection.active?
54
+ connection.reconnect!
55
+ sleep(0.5)
56
+ connection.exec_query('LISTEN "skiplock::jobs"')
57
+ connection.exec_query('LISTEN "skiplock::workers"') if @master
58
+ @next_schedule_at = Time.now.to_f
57
59
  end
58
- if Job::Errors.keys.count > 0
59
- completed_ids = Job::Errors.keys.map { |k| k if Job::Errors[k] }.compact
60
- if Settings['purge_completion'] && completed_ids.count > 0
61
- Job.where(id: completed_ids, running: true).delete_all
62
- elsif completed_ids.count > 0
63
- Job.where(id: completed_ids, running: true).update_all(running: false, finished_at: Time.now)
64
- end
65
- orphaned_ids = Job::Errors.keys.map { |k| k unless Job::Errors[k] }.compact
66
- Job.where(id: orphaned_ids, running: true).update_all(running: false, worker_id: nil, scheduled_at: (Time.now + 10)) if orphaned_ids.count > 0
67
- Job::Errors.clear
60
+ error = false
61
+ end
62
+ if Job::Errors.keys.count > 0
63
+ completed_ids = Job::Errors.keys.map { |k| k if Job::Errors[k] }.compact
64
+ if Settings['purge_completion'] && completed_ids.count > 0
65
+ Job.where(id: completed_ids, running: true).delete_all
66
+ elsif completed_ids.count > 0
67
+ Job.where(id: completed_ids, running: true).update_all(running: false, finished_at: Time.now, updated_at: Time.now)
68
68
  end
69
- if Time.now.to_f >= @next_schedule_at && @executor.remaining_capacity > 0
70
- @executor.post { do_work }
69
+ orphaned_ids = Job::Errors.keys.map { |k| k unless Job::Errors[k] }.compact
70
+ Job.where(id: orphaned_ids, running: true).update_all(running: false, worker_id: nil, scheduled_at: (Time.now + 10), updated_at: Time.now) if orphaned_ids.count > 0
71
+ Job::Errors.clear
72
+ end
73
+ notifications = { 'skiplock::jobs' => [], 'skiplock::workers' => [] }
74
+ connection.raw_connection.wait_for_notify(0.1) do |channel, pid, payload|
75
+ notifications[channel] << payload if payload
76
+ loop do
77
+ payload = connection.raw_connection.notifies
78
+ break unless @running && payload
79
+ notifications[payload[:relname]] << payload[:extra]
71
80
  end
72
- notifications = []
73
- connection.raw_connection.wait_for_notify(0.1) do |channel, pid, payload|
74
- notifications << payload if payload
75
- loop do
76
- payload = connection.raw_connection.notifies
77
- break unless @running && payload
78
- notifications << payload[:extra]
81
+ notifications['skiplock::jobs'].each do |n|
82
+ op, id, worker_id, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
83
+ if @master
84
+ # TODO: report job status to action cable
79
85
  end
80
- notifications.each do |n|
81
- op, id, worker_id, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
82
- next if op == 'DELETE' || running == 'true' || expired_at.to_s.length > 0 || finished_at.to_s.length > 0
83
- if scheduled_at.to_f <= Time.now.to_f
84
- @next_schedule_at = Time.now.to_f
85
- elsif scheduled_at.to_f < @next_schedule_at
86
- @next_schedule_at = scheduled_at.to_f
87
- end
86
+ next if op == 'DELETE' || running == 'true' || expired_at.to_f > 0 || finished_at.to_f > 0 || scheduled_at.to_f < @last_dispatch_at
87
+ if scheduled_at.to_f < Time.now.to_f
88
+ @next_schedule_at = Time.now.to_f
89
+ elsif scheduled_at.to_f < @next_schedule_at
90
+ @next_schedule_at = scheduled_at.to_f
88
91
  end
89
92
  end
90
- rescue Exception => ex
91
- STDERR.puts ex.message
92
- STDERR.puts ex.backtrace
93
- Skiplock.on_error.call(ex, @last_exception) if Skiplock.on_error.is_a?(Proc)
94
- error = true
95
- t = Time.now
96
- while @running
97
- sleep(0.5)
98
- break if Time.now - t > 5
99
- end
100
- @last_exception = ex
93
+ if @master
94
+ # TODO: report worker status to action cable
95
+ notifications['skiplock::workers'].each do |n|
96
+ end
97
+ end
98
+ end
99
+ if Time.now.to_f >= @next_schedule_at && @executor.remaining_capacity > 0
100
+ @executor.post { do_work }
101
+ end
102
+ rescue Exception => ex
103
+ # most likely error with database connection
104
+ STDERR.puts ex.message
105
+ STDERR.puts ex.backtrace
106
+ Skiplock.on_error.call(ex, @last_exception) if Skiplock.on_error.is_a?(Proc)
107
+ error = true
108
+ t = Time.now
109
+ while @running
110
+ sleep(0.5)
111
+ break if Time.now - t > 5
101
112
  end
102
- sleep(0.2)
113
+ @last_exception = ex
103
114
  end
104
- connection.exec_query('UNLISTEN *')
115
+ sleep(0.2)
105
116
  end
117
+ connection.exec_query('UNLISTEN *')
106
118
  end
107
119
  end
108
120
  end
@@ -118,6 +130,7 @@ module Skiplock
118
130
 
119
131
  def do_work
120
132
  while @running
133
+ @last_dispatch_at = Time.now.to_f
121
134
  result = Job.dispatch(queues_order_query: @queues_order_query, worker_id: @worker.id)
122
135
  next if result.is_a?(Job)
123
136
  @next_schedule_at = result if result.is_a?(Float)
@@ -1,6 +1,6 @@
1
1
  module Skiplock
2
2
  class Manager
3
- def self.start(standalone: false, workers: nil, max_retries: nil, max_threads: nil, min_threads: nil, logging: nil)
3
+ def self.start(standalone: false, restart: false, workers: nil, max_retries: nil, max_threads: nil, min_threads: nil, logging: nil)
4
4
  unless Settings.frozen?
5
5
  load_settings
6
6
  Settings['logging'] = logging if logging
@@ -16,7 +16,7 @@ module Skiplock
16
16
  Settings['workers'] = 0 if Settings['workers'] < 0
17
17
  Settings.freeze
18
18
  end
19
- return unless standalone || (caller.any?{|l| l =~ %r{/rack/}} && (Settings['workers'] == 0 || Rails.env.development?))
19
+ return unless standalone || restart || (caller.any?{|l| l =~ %r{/rack/}} && (Settings['workers'] == 0 || Rails.env.development?))
20
20
  if standalone
21
21
  self.standalone
22
22
  else
@@ -24,12 +24,15 @@ module Skiplock
24
24
  @thread = @dispatcher.run
25
25
  at_exit { self.shutdown }
26
26
  end
27
+ ActiveJob::Base.logger = nil
27
28
  end
28
29
 
29
30
  def self.shutdown(wait: true)
30
31
  if @dispatcher && @thread
31
32
  @dispatcher.shutdown(wait: wait)
32
33
  @thread.join
34
+ @dispatcher = nil
35
+ @thread = nil
33
36
  end
34
37
  end
35
38
 
@@ -1,4 +1,4 @@
1
1
  module Skiplock
2
- VERSION = Version = '1.0.7'
2
+ VERSION = Version = '1.0.8'
3
3
  end
4
4
 
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.7
4
+ version: 1.0.8
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-23 00:00:00.000000000 Z
11
+ date: 2021-04-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob