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 +4 -4
- data/README.md +5 -9
- data/lib/generators/skiplock/templates/migration.rb.erb +1 -1
- data/lib/skiplock/dispatcher.rb +89 -76
- data/lib/skiplock/manager.rb +5 -2
- data/lib/skiplock/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2ee6a8ff68af1a1029a26db98867457a798c4fb6aacf0a2a2816efd3ef2b977b
|
4
|
+
data.tar.gz: 2c75fb21c79346f4525b8634b28fb326505acb7ab61795fe7d291c3faa75244f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
84
|
-
#
|
85
|
-
Skiplock
|
86
|
-
|
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
|
data/lib/skiplock/dispatcher.rb
CHANGED
@@ -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.
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
37
|
-
|
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
|
40
|
-
# remove workers
|
41
|
-
Worker.where(
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
70
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
113
|
+
@last_exception = ex
|
103
114
|
end
|
104
|
-
|
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)
|
data/lib/skiplock/manager.rb
CHANGED
@@ -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
|
|
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.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-
|
11
|
+
date: 2021-04-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|