skiplock 1.0.23 → 1.0.24

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: 275e1ddd1a59648e86378889e2e2cc6a4e22943a3fb4211209f20a674744b1b3
4
- data.tar.gz: 7327f5893f36cc4b05163cfd0df47992344b2ca6b97c2ef10ca4f00b219a9ac9
3
+ metadata.gz: a43b01cedd94ea395b0b173e2da3c5c7ab0baf6ccbd35e599127544084ca5589
4
+ data.tar.gz: a908c4eea5b2b75727a70fe5efc248f0b0183e9fe97a37c068b185b9f259a4ba
5
5
  SHA512:
6
- metadata.gz: e9b8afd563ec4ff46838e73ae031a75a69142ee392a05d0e5b612008153b10a6a62a931e22643e817722d0017548573f9cafed6ba943f41da3cf8d0872d76481
7
- data.tar.gz: 8b04173649d9e4a21edb9d47e23d37d5c8d0b08ba8658f46c0fd8341270abf1148a2131e3f57251421caf024d5ce8d9bfb35a648ee9a904f999d6b21bf0fe5ec
6
+ metadata.gz: c5ae8b9bd79e37426710d707fadd2e4d658e541ffdd109a760687f2c14dacfa9d61614130d25be801264ecee86ea63d54ce4d6604c4cc80c7a47ac31752618f6
7
+ data.tar.gz: 42ba6d9047ac028c24389d1915f686f5d553751244bd557a98a8b28af311a53702d6c69078abafcb6696d5d00e441fd8eca43d2e18b11530945fa10a42d15cea
data/README.md CHANGED
@@ -50,12 +50,14 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
50
50
  ```yaml
51
51
  # config/skiplock.yml (default settings)
52
52
  ---
53
+ graceful_shutdown: 15
53
54
  min_threads: 1
54
55
  max_threads: 10
55
56
  max_retries: 20
56
57
  logfile: skiplock.log
57
58
  loglevel: info
58
59
  notification: custom
60
+ actioncable: false
59
61
  extensions: false
60
62
  purge_completion: true
61
63
  queues:
@@ -64,12 +66,14 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
64
66
  workers: 0
65
67
  ```
66
68
  Available configuration options are:
69
+ - **graceful_shutdown** (*integer*): sets the number of seconds to wait for jobs to finish before being killed during shutdown
67
70
  - **min_threads** (*integer*): sets minimum number of threads staying idle
68
71
  - **max_threads** (*integer*): sets the maximum number of threads allowed to run jobs
69
72
  - **max_retries** (*integer*): sets the maximum attempt a job will be retrying before it is marked expired. See `Retry system` for more details
70
73
  - **logfile** (*string*): filename for skiplock logs; empty logfile will disable logging
71
74
  - **loglevel** (*string*): sets logging level (`debug, info, warn, error, fatal, unknown`)
72
75
  - **notification** (*string*): sets the library to be used for notifying errors and exceptions (`auto, airbrake, bugsnag, exception_notification, custom`); using `auto` will detect library if available. See `Notification system` for more details
76
+ - **actioncable** (*boolean*): enable or disable usage of ActionCable notification
73
77
  - **extensions** (*multi*): enable or disable the class method extension. See `ClassMethod extension` for more details
74
78
  - **purge_completion** (*boolean*): when set to **true** will delete jobs after they were completed successfully; if set to **false** then the completed jobs should be purged periodically to maximize performance (eg. clean up old jobs after 3 months); queued jobs can manually override using `purge` option
75
79
  - **queues** (*hash*): defines the set of queues with priorities; lower priority takes precedence
@@ -83,6 +87,7 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
83
87
  ```
84
88
  $ bundle exec skiplock -h
85
89
  Usage: skiplock [options]
90
+ -a, --actioncable YESNO Actioncable notification
86
91
  -e, --environment STRING Rails environment
87
92
  -l, --logfile STRING Log filename
88
93
  -L, --loglevel STRING Log level (debug, info, warn, error, fatal, unknown)
@@ -238,6 +243,10 @@ Code examples of gathering counters information:
238
243
  ```ruby
239
244
  Skiplock::Counter.sum(:expiries)
240
245
  ```
246
+ - get all information in one query
247
+ ```ruby
248
+ Skiplock::Counter.pluck("sum(completions), sum(dispatches), sum(expiries), sum(failures), sum(retries)").first
249
+ ```
241
250
 
242
251
  ## Contributing
243
252
 
data/bin/skiplock CHANGED
@@ -5,6 +5,7 @@ options = {}
5
5
  begin
6
6
  op = OptionParser.new do |opts|
7
7
  opts.banner = "Usage: #{File.basename($0)} [options]"
8
+ opts.on('-a', '--actioncable YESNO', TrueClass, 'Actioncable notification')
8
9
  opts.on('-e', '--environment STRING', String, 'Rails environment')
9
10
  opts.on('-l', '--logfile STRING', String, 'Log filename')
10
11
  opts.on('-L', '--loglevel STRING', String, 'Log level (debug, info, warn, error, fatal, unknown)')
data/lib/skiplock/cron.rb CHANGED
@@ -19,6 +19,7 @@ module Skiplock
19
19
  query = Job.where('cron IS NOT NULL')
20
20
  query = query.where('job_class NOT IN (?)', cronjobs) if cronjobs.count > 0
21
21
  query.delete_all
22
+ rescue
22
23
  end
23
24
 
24
25
  def self.next_schedule_at(cron)
@@ -10,6 +10,7 @@ module Skiplock
10
10
  elsif @config[:extensions].is_a?(Array)
11
11
  @config[:extensions].each { |n| n.constantize.__send__(:extend, Skiplock::Extension) if n.safe_constantize }
12
12
  end
13
+ ActiveJob::Base.__send__(:include, Skiplock::Patch)
13
14
  (caller.any?{ |l| l =~ %r{/rack/} } && @config[:workers] == 0) ? async : Cron.setup
14
15
  end
15
16
 
@@ -17,7 +18,7 @@ module Skiplock
17
18
  setup_logger
18
19
  configure
19
20
  Worker.cleanup(@hostname)
20
- @worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname)
21
+ @worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname, actioncable: @config[:actioncable])
21
22
  Cron.setup if @worker.master
22
23
  @worker.start(**@config)
23
24
  at_exit { @worker.shutdown }
@@ -39,13 +40,13 @@ module Skiplock
39
40
  Signal.trap('TERM') { @shutdown = true }
40
41
  Signal.trap('HUP') { setup_logger }
41
42
  Worker.cleanup(@hostname)
42
- @worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname)
43
+ @worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname, actioncable: @config[:actioncable])
43
44
  ActiveRecord::Base.connection.disconnect! if @config[:workers] > 1
44
45
  (@config[:workers] - 1).times do |n|
45
46
  fork do
46
47
  sleep(0.25*n + 1)
47
48
  ActiveRecord::Base.establish_connection
48
- worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname, master: false)
49
+ worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname, master: false, actioncable: @config[:actioncable])
49
50
  worker.start(worker_num: n + 1, **@config)
50
51
  loop do
51
52
  sleep 0.5
@@ -75,24 +76,25 @@ module Skiplock
75
76
  @logger.info "-"*(title.length)
76
77
  @logger.info title
77
78
  @logger.info "-"*(title.length)
78
- @logger.info "ClassMethod extensions: #{@config[:extensions]}"
79
- @logger.info " Purge completion: #{@config[:purge_completion]}"
80
- @logger.info " Notification: #{@config[:notification]}"
81
- @logger.info " Max retries: #{@config[:max_retries]}"
82
- @logger.info " Min threads: #{@config[:min_threads]}"
83
- @logger.info " Max threads: #{@config[:max_threads]}"
84
- @logger.info " Environment: #{Rails.env}"
85
- @logger.info " Loglevel: #{@config[:loglevel]}"
86
- @logger.info " Logfile: #{@config[:logfile] || '(disabled)'}"
87
- @logger.info " Workers: #{@config[:workers]}"
88
- @logger.info " Queues: #{@config[:queues].map {|k,v| k + '(' + v.to_s + ')'}.join(', ')}" if @config[:queues].is_a?(Hash)
89
- @logger.info " PID: #{Process.pid}"
79
+ @logger.info "ActionCable notification: #{@config[:actioncable]}#{' (not available)' unless defined?(ActionCable)}"
80
+ @logger.info " ClassMethod extensions: #{@config[:extensions]}"
81
+ @logger.info " Purge completion: #{@config[:purge_completion]}"
82
+ @logger.info " Notification: #{@config[:notification]}"
83
+ @logger.info " Max retries: #{@config[:max_retries]}"
84
+ @logger.info " Min threads: #{@config[:min_threads]}"
85
+ @logger.info " Max threads: #{@config[:max_threads]}"
86
+ @logger.info " Environment: #{Rails.env}"
87
+ @logger.info " Loglevel: #{@config[:loglevel]}"
88
+ @logger.info " Logfile: #{@config[:logfile] || '(disabled)'}"
89
+ @logger.info " Workers: #{@config[:workers]}"
90
+ @logger.info " Queues: #{@config[:queues].map {|k,v| k + '(' + v.to_s + ')'}.join(', ')}" if @config[:queues].is_a?(Hash)
91
+ @logger.info " PID: #{Process.pid}"
90
92
  @logger.info "-"*(title.length)
91
93
  @logger.warn "[Skiplock] Custom notification has no registered 'on_error' callback" if Skiplock.on_errors.count == 0
92
94
  end
93
95
 
94
96
  def configure
95
- @hostname = Socket.ip_address_list.reject(&:ipv4_loopback?).reject(&:ipv6?).map(&:ip_address).join('|')
97
+ @hostname = "#{`hostname -f`.strip}|#{Socket.ip_address_list.reject(&:ipv4_loopback?).reject(&:ipv6?).map(&:ip_address).join('|')}"
96
98
  @config.transform_values! {|v| v.is_a?(String) ? v.downcase : v}
97
99
  @config[:graceful_shutdown] = 300 if @config[:graceful_shutdown] > 300
98
100
  @config[:graceful_shutdown] = nil if @config[:graceful_shutdown] < 0
@@ -133,6 +135,7 @@ module Skiplock
133
135
  else
134
136
  @config[:notification] = 'custom'
135
137
  end
138
+ @logger.error 'ActionCable is not found!' if @config[:actioncable] && !defined?(ActionCable)
136
139
  Skiplock.on_errors.freeze
137
140
  end
138
141
 
@@ -1,4 +1,4 @@
1
1
  module Skiplock
2
- VERSION = Version = '1.0.23'
2
+ VERSION = Version = '1.0.24'
3
3
  end
4
4
 
@@ -12,16 +12,19 @@ module Skiplock
12
12
  self.where(id: delete_ids).delete_all if delete_ids.count > 0
13
13
  end
14
14
 
15
- def self.generate(capacity:, hostname:, master: true)
16
- self.create!(pid: Process.pid, sid: Process.getsid(), master: master, hostname: hostname, capacity: capacity)
15
+ def self.generate(capacity:, hostname:, master: true, actioncable: false)
16
+ worker = self.create!(pid: Process.pid, sid: Process.getsid(), master: master, hostname: hostname, capacity: capacity)
17
17
  rescue
18
- self.create!(pid: Process.pid, sid: Process.getsid(), master: false, hostname: hostname, capacity: capacity)
18
+ worker = self.create!(pid: Process.pid, sid: Process.getsid(), master: false, hostname: hostname, capacity: capacity)
19
+ ensure
20
+ ActionCable.server.broadcast('skiplock', { worker: { op: 'CREATE', id: worker.id, hostname: worker.hostname, master: worker.master, capacity: worker.capacity, pid: worker.pid, sid: worker.sid, created_at: worker.created_at.to_f, updated_at: worker.updated_at.to_f } }) if actioncable && defined?(ActionCable)
19
21
  end
20
22
 
21
23
  def shutdown
22
24
  @running = false
23
25
  @executor.shutdown
24
26
  @executor.kill unless @executor.wait_for_termination(@config[:graceful_shutdown])
27
+ ActionCable.server.broadcast('skiplock', { worker: { op: 'DELETE', id: self.id, hostname: self.hostname, master: self.master, capacity: self.capacity, pid: self.pid, sid: self.sid, created_at: self.created_at.to_f, updated_at: self.updated_at.to_f } }) if @config[:actioncable] && defined?(ActionCable)
25
28
  self.delete
26
29
  Skiplock.logger.info "[Skiplock] Shutdown of #{self.master ? 'master' : 'cluster'} worker#{(' ' + @num.to_s) if @num > 0 && @config[:workers] > 2} (PID: #{self.pid}) was completed."
27
30
  end
@@ -32,7 +35,15 @@ module Skiplock
32
35
  @pg_config = ActiveRecord::Base.connection.raw_connection.conninfo_hash.compact
33
36
  @queues_order_query = @config[:queues].map { |q,v| "WHEN queue_name = '#{q}' THEN #{v}" }.join(' ') if @config[:queues].is_a?(Hash) && @config[:queues].count > 0
34
37
  @running = true
35
- @executor = Concurrent::ThreadPoolExecutor.new(min_threads: @config[:min_threads] + 1, max_threads: @config[:max_threads] + 1, max_queue: @config[:max_threads] + 1, idletime: 60, auto_terminate: true, fallback_policy: :abort)
38
+ @map = ::PG::TypeMapByOid.new
39
+ @map.add_coder(::PG::TextDecoder::Boolean.new(oid: 16, name: 'bool'))
40
+ @map.add_coder(::PG::TextDecoder::Integer.new(oid: 20, name: 'int8'))
41
+ @map.add_coder(::PG::TextDecoder::Integer.new(oid: 21, name: 'int2'))
42
+ @map.add_coder(::PG::TextDecoder::Integer.new(oid: 23, name: 'int4'))
43
+ @map.add_coder(::PG::TextDecoder::TimestampUtc.new(oid: 1114, name: 'timestamp'))
44
+ @map.add_coder(::PG::TextDecoder::String.new(oid: 2950, name: 'uuid'))
45
+ @map.add_coder(::PG::TextDecoder::JSON.new(oid: 3802, name: 'jsonb'))
46
+ @executor = Concurrent::ThreadPoolExecutor.new(min_threads: @config[:min_threads] + 1, max_threads: @config[:max_threads] + 1, max_queue: @config[:max_threads] + 1, idletime: 60, auto_terminate: false, fallback_policy: :abort)
36
47
  @executor.post { run }
37
48
  if @config[:standalone]
38
49
  Process.setproctitle("skiplock: #{self.master ? 'master' : 'cluster'} worker#{(' ' + @num.to_s) if @num > 0 && @config[:workers] > 2} [#{Rails.application.class.name.deconstantize.downcase}:#{Rails.env}]")
@@ -43,17 +54,10 @@ module Skiplock
43
54
  private
44
55
 
45
56
  def establish_connection
46
- map = ::PG::TypeMapByOid.new
47
- map.add_coder(::PG::TextDecoder::Boolean.new(oid: 16, name: 'bool'))
48
- map.add_coder(::PG::TextDecoder::Integer.new(oid: 20, name: 'int8'))
49
- map.add_coder(::PG::TextDecoder::Integer.new(oid: 21, name: 'int2'))
50
- map.add_coder(::PG::TextDecoder::Integer.new(oid: 23, name: 'int4'))
51
- map.add_coder(::PG::TextDecoder::TimestampUtc.new(oid: 1114, name: 'timestamp'))
52
- map.add_coder(::PG::TextDecoder::String.new(oid: 2950, name: 'uuid'))
53
- map.add_coder(::PG::TextDecoder::JSON.new(oid: 3802, name: 'jsonb'))
54
57
  @connection = ::PG.connect(@pg_config)
55
- @connection.type_map_for_results = map
58
+ @connection.type_map_for_results = @map
56
59
  @connection.exec('LISTEN "skiplock::jobs"').clear
60
+ @connection.exec('LISTEN "skiplock::workers"').clear
57
61
  end
58
62
 
59
63
  def run
@@ -85,19 +89,26 @@ module Skiplock
85
89
  next_schedule_at = (result ? result['scheduled_at'].to_f : Float::INFINITY)
86
90
  end
87
91
  end
88
- job_notifications = []
92
+ notifications = { 'skiplock::jobs' => [], 'skiplock::workers' => [] }
89
93
  @connection.wait_for_notify(0.2) do |channel, pid, payload|
90
- job_notifications << payload if payload
94
+ notifications[channel] << payload if payload
91
95
  loop do
92
96
  payload = @connection.notifies
93
97
  break unless @running && payload
94
- job_notifications << payload[:extra]
98
+ notifications[payload[:relname]] << payload[:extra]
95
99
  end
96
- job_notifications.each do |n|
100
+ notifications['skiplock::jobs'].each do |n|
97
101
  op, id, worker_id, job_class, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
102
+ ActionCable.server.broadcast('skiplock', { job: { op: op, id: id, worker_id: worker_id, job_class: job_class, queue_name: queue_name, running: (running == 'true'), expired_at: expired_at.to_f, finished_at: finished_at.to_f, scheduled_at: scheduled_at.to_f } }) if self.master && @config[:actioncable] && defined?(ActionCable)
98
103
  next if op == 'DELETE' || running == 'true' || expired_at.to_f > 0 || finished_at.to_f > 0
99
104
  next_schedule_at = scheduled_at.to_f if scheduled_at.to_f < next_schedule_at
100
105
  end
106
+ if self.master && @config[:actioncable] && defined?(ActionCable)
107
+ notifications['skiplock::workers'].each do |w|
108
+ op, id, hostname, master, capacity, pid, sid, created_at, updated_at = w.split(',')
109
+ ActionCable.server.broadcast('skiplock', { worker: { op: op, id: id, hostname: hostname, master: (master == 'true'), capacity: capacity.to_i, pid: pid.to_i, sid: sid.to_i, created_at: created_at.to_f, updated_at: updated_at.to_f } })
110
+ end
111
+ end
101
112
  end
102
113
  if Process.clock_gettime(Process::CLOCK_MONOTONIC) - timestamp > 60
103
114
  @connection.exec("UPDATE skiplock.workers SET updated_at = NOW() WHERE id = '#{self.id}'").clear
data/lib/skiplock.rb CHANGED
@@ -11,7 +11,7 @@ require 'skiplock/worker'
11
11
  require 'skiplock/version'
12
12
 
13
13
  module Skiplock
14
- DEFAULT_CONFIG = { 'extensions' => false, 'logfile' => 'skiplock.log', 'loglevel' => 'info', 'graceful_shutdown' => 15, 'min_threads' => 1, 'max_threads' => 10, 'max_retries' => 20, 'notification' => 'custom', 'purge_completion' => true, 'queues' => { 'default' => 100, 'mailers' => 999 }, 'workers' => 0 }.freeze
14
+ DEFAULT_CONFIG = { 'graceful_shutdown' => 15, 'min_threads' => 1, 'max_threads' => 10, 'max_retries' => 20, 'logfile' => 'skiplock.log', 'loglevel' => 'info', 'notification' => 'custom', 'actioncable' => false, 'extensions' => false, 'purge_completion' => true, 'queues' => { 'default' => 100, 'mailers' => 999 }, 'workers' => 0 }.freeze
15
15
 
16
16
  def self.logger=(l)
17
17
  @logger = l
@@ -34,5 +34,4 @@ module Skiplock
34
34
  def self.table_name_prefix
35
35
  'skiplock.'
36
36
  end
37
- end
38
- ActiveJob::Base.__send__(:include, Skiplock::Patch)
37
+ 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.23
4
+ version: 1.0.24
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-09-18 00:00:00.000000000 Z
11
+ date: 2021-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob