skiplock 1.0.7 → 1.0.8

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: 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