skiplock 1.0.22 → 1.1.0

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: 3b99466d7f1848f9dcd750a4dceb6a39d7124707e941c74b6c75f0b3a57944ff
4
- data.tar.gz: 3f3ed1c9327d41e675d5c170590b52fe32f1facc5b57667dba513e5324e6ee20
3
+ metadata.gz: 71cf3158d71dfe424c218da13a55d785365bee9027f827bb1498ba5e0110fda9
4
+ data.tar.gz: 1512863ab863b392f84d67a137959227a823ac33dd9ca8e1ab8e87c63fe04adf
5
5
  SHA512:
6
- metadata.gz: '0129296507b0479ba28a6e4e0bce810340f0ff2559004da1385cf02b6d6023f7be5cb1543f3fb24cebdeb579d6be6ed1e5d7d72d43245de5374926607b23d7df'
7
- data.tar.gz: 163faf212d06b6d7f8045eb347ccab77de5b16bfe77fe8780a5d73176ce3c5804cc25cbca8f962d92bc5dfccc1ada223d03fc0b443d0df48125a12bf4ec7ddeb
6
+ metadata.gz: 79ef005a284b8aabfff1e1435bba2019a3b3698f714803f6521562f143b3d5f4f6536991a040bbd44574a52fa3be2df7ac565c37cccf4da40da95fe4b150b1c6
7
+ data.tar.gz: 1f7fd443df703c98133b66838c6d6d49a9cfa3b54d33cafaff15d59fd3a538591400fdb917ccfb5ec4b8a2831d88b6a1f85ef622ecb7566842f2f761e97bba9f
data/README.md CHANGED
@@ -4,7 +4,7 @@
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
 
7
- The library is quite small compared to other PostgreSQL job queues (eg. *delay_job*, *queue_classic*, *que*, *good_job*) with less than 500 lines of codes; and it still provides similar set of features and more...
7
+ The library is quite small compared to other PostgreSQL job queues (eg. *delay_job*, *queue_classic*, *que*, *good_job*) with less than 600 lines of codes; and it still provides similar set of features and more...
8
8
 
9
9
  #### Compatibility:
10
10
 
@@ -50,11 +50,13 @@ 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
59
+ namespace:
58
60
  notification: custom
59
61
  extensions: false
60
62
  purge_completion: true
@@ -64,11 +66,13 @@ 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`)
75
+ - **namespace** (*string*): sets namespace for jobs (workers will only process jobs of specified namespace)
72
76
  - **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
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
@@ -85,6 +89,8 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
85
89
  Usage: skiplock [options]
86
90
  -e, --environment STRING Rails environment
87
91
  -l, --logfile STRING Log filename
92
+ -L, --loglevel STRING Log level (debug, info, warn, error, fatal, unknown)
93
+ -n, --namespace STRING Job namespace
88
94
  -s, --graceful-shutdown NUM Number of seconds to wait for graceful shutdown
89
95
  -r, --max-retries NUM Number of maxixum retries
90
96
  -t, --max-threads NUM Number of maximum threads
@@ -237,6 +243,10 @@ Code examples of gathering counters information:
237
243
  ```ruby
238
244
  Skiplock::Counter.sum(:expiries)
239
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
+ ```
240
250
 
241
251
  ## Contributing
242
252
 
data/bin/skiplock CHANGED
@@ -8,6 +8,7 @@ begin
8
8
  opts.on('-e', '--environment STRING', String, 'Rails environment')
9
9
  opts.on('-l', '--logfile STRING', String, 'Log filename')
10
10
  opts.on('-L', '--loglevel STRING', String, 'Log level (debug, info, warn, error, fatal, unknown)')
11
+ opts.on('-n', '--namespace STRING', String, 'Job namespace')
11
12
  opts.on('-s', '--graceful-shutdown NUM', Integer, 'Number of seconds to wait for graceful shutdown')
12
13
  opts.on('-r', '--max-retries NUM', Integer, 'Number of maxixum retries')
13
14
  opts.on('-t', '--max-threads NUM', Integer, 'Number of maximum threads')
@@ -7,11 +7,14 @@ class CreateSkiplockSchema < ActiveRecord::Migration<%= "[#{ActiveRecord::VERSIO
7
7
  t.integer :expiries, null: false, default: 0
8
8
  t.integer :failures, null: false, default: 0
9
9
  t.integer :retries, null: false, default: 0
10
- t.date :day, null: false, index: { unique: true }
10
+ t.string :namespace, null: false, default: '', index: true
11
+ t.date :day, null: false
12
+ t.index [ :namespace, :day ], unique: true
11
13
  end
12
14
  create_table 'skiplock.jobs', id: :uuid do |t|
13
15
  t.uuid :worker_id, index: true
14
16
  t.string :job_class, null: false
17
+ t.string :namespace, null: false, default: '', index: true
15
18
  t.string :queue_name, index: true
16
19
  t.string :locale
17
20
  t.string :timezone
@@ -31,6 +34,7 @@ class CreateSkiplockSchema < ActiveRecord::Migration<%= "[#{ActiveRecord::VERSIO
31
34
  t.integer :sid, null: false
32
35
  t.integer :capacity, null: false
33
36
  t.string :hostname, null: false, index: true
37
+ t.string :namespace, null: false, default: '', index: true
34
38
  t.boolean :master, null: false, default: false, index: true
35
39
  t.jsonb :data
36
40
  t.timestamps null: false, index: true, default: -> { 'now()' }
@@ -41,27 +45,29 @@ class CreateSkiplockSchema < ActiveRecord::Migration<%= "[#{ActiveRecord::VERSIO
41
45
  record RECORD;
42
46
  BEGIN
43
47
  record = NEW;
44
- IF (TG_OP = 'DELETE') THEN
48
+ IF TG_OP = 'DELETE' THEN
45
49
  record = OLD;
46
- IF (record.running = TRUE) THEN
47
- INSERT INTO skiplock.counters (day,completions) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET completions = skiplock.counters.completions + 1;
50
+ IF record.running IS TRUE THEN
51
+ INSERT INTO skiplock.counters (namespace,day,completions) VALUES (record.namespace,NOW(),1) ON CONFLICT (namespace,day) DO UPDATE SET completions = skiplock.counters.completions + 1;
48
52
  END IF;
49
- ELSIF (TG_OP = 'UPDATE') THEN
50
- IF (OLD.running = FALSE AND record.running = TRUE) THEN
51
- IF (record.executions > 0) THEN
52
- INSERT INTO skiplock.counters (day,retries) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET retries = skiplock.counters.retries + 1;
53
+ ELSIF TG_OP = 'UPDATE' THEN
54
+ IF OLD.running IS FALSE AND record.running IS TRUE THEN
55
+ IF record.executions > 0 THEN
56
+ INSERT INTO skiplock.counters (namespace,day,retries) VALUES (record.namespace,NOW(),1) ON CONFLICT (namespace,day) DO UPDATE SET retries = skiplock.counters.retries + 1;
53
57
  ELSE
54
- INSERT INTO skiplock.counters (day,dispatches) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET dispatches = skiplock.counters.dispatches + 1;
58
+ INSERT INTO skiplock.counters (namespace,day,dispatches) VALUES (record.namespace,NOW(),1) ON CONFLICT (namespace,day) DO UPDATE SET dispatches = skiplock.counters.dispatches + 1;
59
+ END IF;
60
+ ELSIF OLD.finished_at IS NULL AND record.finished_at IS NOT NULL THEN
61
+ INSERT INTO skiplock.counters (namespace,day,completions) VALUES (record.namespace,NOW(),1) ON CONFLICT (namespace,day) DO UPDATE SET completions = skiplock.counters.completions + 1;
62
+ ELSIF OLD.running IS TRUE AND record.running IS FALSE THEN
63
+ IF record.expired_at IS NOT NULL THEN
64
+ INSERT INTO skiplock.counters (namespace,day,expiries) VALUES (record.namespace,NOW(),1) ON CONFLICT (namespace,day) DO UPDATE SET expiries = skiplock.counters.expiries + 1;
65
+ ELSE
66
+ INSERT INTO skiplock.counters (namespace,day,failures) VALUES (record.namespace,NOW(),1) ON CONFLICT (namespace,day) DO UPDATE SET failures = skiplock.counters.failures + 1;
55
67
  END IF;
56
- ELSIF (OLD.finished_at IS NULL AND record.finished_at IS NOT NULL) THEN
57
- INSERT INTO skiplock.counters (day,completions) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET completions = skiplock.counters.completions + 1;
58
- ELSIF (OLD.running = TRUE AND record.running = FALSE AND record.expired_at IS NOT NULL) THEN
59
- INSERT INTO skiplock.counters (day,expiries) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET expiries = skiplock.counters.expiries + 1;
60
- ELSIF (OLD.running = TRUE AND record.running = FALSE AND record.expired_at IS NULL AND record.finished_at IS NULL) THEN
61
- INSERT INTO skiplock.counters (day,failures) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET failures = skiplock.counters.failures + 1;
62
68
  END IF;
63
69
  END IF;
64
- PERFORM pg_notify('skiplock::jobs', CONCAT(TG_OP,',',record.id::TEXT,',',record.worker_id::TEXT,',',record.job_class,',',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));
70
+ PERFORM pg_notify('skiplock::jobs', CONCAT(TG_OP,',',record.id::TEXT,',',record.worker_id::TEXT,',',record.namespace,',',record.job_class,',',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));
65
71
  RETURN NULL;
66
72
  END;
67
73
  $$ LANGUAGE plpgsql
@@ -71,12 +77,12 @@ class CreateSkiplockSchema < ActiveRecord::Migration<%= "[#{ActiveRecord::VERSIO
71
77
  DECLARE
72
78
  record RECORD;
73
79
  BEGIN
74
- IF (TG_OP = 'DELETE') THEN
80
+ IF TG_OP = 'DELETE' THEN
75
81
  record = OLD;
76
82
  ELSE
77
83
  record = NEW;
78
84
  END IF;
79
- PERFORM pg_notify('skiplock::workers', CONCAT(TG_OP,',',record.id::TEXT,',',record.hostname,',',record.master::TEXT,',',record.capacity,',',record.pid,',',record.sid,',',CAST(EXTRACT(EPOCH FROM record.created_at) AS FLOAT)::TEXT,',',CAST(EXTRACT(EPOCH FROM record.updated_at) AS FLOAT)::TEXT));
85
+ PERFORM pg_notify('skiplock::workers', CONCAT(TG_OP,',',record.id::TEXT,',',record.namespace,',',record.hostname,',',record.master::TEXT,',',record.capacity,',',record.pid,',',record.sid,',',CAST(EXTRACT(EPOCH FROM record.created_at) AS FLOAT)::TEXT,',',CAST(EXTRACT(EPOCH FROM record.updated_at) AS FLOAT)::TEXT));
80
86
  RETURN NULL;
81
87
  END;
82
88
  $$ LANGUAGE plpgsql;
@@ -87,7 +93,7 @@ class CreateSkiplockSchema < ActiveRecord::Migration<%= "[#{ActiveRecord::VERSIO
87
93
  execute "CREATE INDEX jobs_retry_index ON skiplock.jobs(scheduled_at) WHERE running = FALSE AND executions IS NOT NULL AND expired_at IS NULL AND finished_at IS NULL"
88
94
  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"
89
95
  execute "CREATE UNIQUE INDEX jobs_unique_cron_index ON skiplock.jobs (job_class) WHERE cron IS NOT NULL"
90
- execute "CREATE UNIQUE INDEX workers_unique_master_index ON skiplock.workers(hostname) WHERE master = TRUE"
96
+ execute "CREATE UNIQUE INDEX workers_unique_master_index ON skiplock.workers(hostname,namespace) WHERE master = TRUE"
91
97
  end
92
98
 
93
99
  def down
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)
data/lib/skiplock/job.rb CHANGED
@@ -3,14 +3,15 @@ module Skiplock
3
3
  self.implicit_order_column = 'updated_at'
4
4
  attribute :activejob_error
5
5
  attribute :exception
6
- attribute :max_retries, :integer
7
- attribute :purge, :boolean
6
+ attribute :max_retries
7
+ attribute :purge
8
8
  belongs_to :worker, inverse_of: :jobs, required: false
9
9
 
10
10
  def self.dispatch(purge_completion: true, max_retries: 20)
11
+ namespace_query = Skiplock.namespace.nil? ? "namespace IS NULL" : "namespace = '#{Skiplock.namespace}'"
11
12
  job = nil
12
13
  self.connection.transaction do
13
- job = self.find_by_sql("SELECT id, scheduled_at FROM skiplock.jobs 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
14
+ job = self.find_by_sql("SELECT id, scheduled_at FROM skiplock.jobs WHERE running = FALSE AND expired_at IS NULL AND finished_at IS NULL AND #{namespace_query} ORDER BY scheduled_at ASC NULLS FIRST, priority ASC NULLS LAST, created_at ASC FOR UPDATE SKIP LOCKED LIMIT 1").first
14
15
  return if job.nil? || job.scheduled_at.to_f > Time.now.to_f
15
16
  job = self.find_by_sql("UPDATE skiplock.jobs SET running = TRUE, updated_at = NOW() WHERE id = '#{job.id}' RETURNING *").first
16
17
  end
@@ -33,7 +34,7 @@ module Skiplock
33
34
  Thread.current[:skiplock_job]
34
35
  else
35
36
  serialize = activejob.serialize
36
- self.create!(serialize.slice(*self.column_names).merge('id' => serialize['job_id'], 'data' => { 'arguments' => serialize['arguments'], 'options' => options }, 'scheduled_at' => timestamp))
37
+ self.create!(serialize.slice(*self.column_names).merge('id' => serialize['job_id'], 'data' => { 'arguments' => serialize['arguments'], 'options' => options }, 'namespace' => Skiplock.namespace, 'scheduled_at' => timestamp))
37
38
  end
38
39
  end
39
40
 
@@ -10,7 +10,8 @@ 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
- async if (caller.any?{ |l| l =~ %r{/rack/} } && @config[:workers] == 0)
13
+ ActiveJob::Base.__send__(:include, Skiplock::Patch)
14
+ (caller.any?{ |l| l =~ %r{/rack/} } && @config[:workers] == 0) ? async : Cron.setup
14
15
  end
15
16
 
16
17
  def async
@@ -18,6 +19,7 @@ module Skiplock
18
19
  configure
19
20
  Worker.cleanup(@hostname)
20
21
  @worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname)
22
+ Cron.setup if @worker.master
21
23
  @worker.start(**@config)
22
24
  at_exit { @worker.shutdown }
23
25
  rescue Exception => ex
@@ -42,7 +44,8 @@ module Skiplock
42
44
  ActiveRecord::Base.connection.disconnect! if @config[:workers] > 1
43
45
  (@config[:workers] - 1).times do |n|
44
46
  fork do
45
- sleep 1
47
+ sleep(0.25*n + 1)
48
+ ActiveRecord::Base.establish_connection
46
49
  worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname, master: false)
47
50
  worker.start(worker_num: n + 1, **@config)
48
51
  loop do
@@ -73,24 +76,25 @@ module Skiplock
73
76
  @logger.info "-"*(title.length)
74
77
  @logger.info title
75
78
  @logger.info "-"*(title.length)
76
- @logger.info "ClassMethod extensions: #{@config[:extensions]}"
77
- @logger.info " Purge completion: #{@config[:purge_completion]}"
78
- @logger.info " Notification: #{@config[:notification]}"
79
- @logger.info " Max retries: #{@config[:max_retries]}"
80
- @logger.info " Min threads: #{@config[:min_threads]}"
81
- @logger.info " Max threads: #{@config[:max_threads]}"
82
- @logger.info " Environment: #{Rails.env}"
83
- @logger.info " Loglevel: #{@config[:loglevel]}"
84
- @logger.info " Logfile: #{@config[:logfile] || '(disabled)'}"
85
- @logger.info " Workers: #{@config[:workers]}"
86
- @logger.info " Queues: #{@config[:queues].map {|k,v| k + '(' + v.to_s + ')'}.join(', ')}" if @config[:queues].is_a?(Hash)
87
- @logger.info " PID: #{Process.pid}"
79
+ @logger.info " ClassMethod extensions: #{@config[:extensions]}"
80
+ @logger.info " Purge completion: #{@config[:purge_completion]}"
81
+ @logger.info " Notification: #{@config[:notification]}"
82
+ @logger.info " Max retries: #{@config[:max_retries]}"
83
+ @logger.info " Min threads: #{@config[:min_threads]}"
84
+ @logger.info " Max threads: #{@config[:max_threads]}"
85
+ @logger.info " Environment: #{Rails.env}"
86
+ @logger.info " Namespace: #{@config[:namespace] || '(nil)'}"
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}"
88
92
  @logger.info "-"*(title.length)
89
93
  @logger.warn "[Skiplock] Custom notification has no registered 'on_error' callback" if Skiplock.on_errors.count == 0
90
94
  end
91
95
 
92
96
  def configure
93
- @hostname = Socket.gethostname
97
+ @hostname = "#{`hostname -f`.strip}|#{Socket.ip_address_list.reject(&:ipv4_loopback?).reject(&:ipv6?).map(&:ip_address).join('|')}"
94
98
  @config.transform_values! {|v| v.is_a?(String) ? v.downcase : v}
95
99
  @config[:graceful_shutdown] = 300 if @config[:graceful_shutdown] > 300
96
100
  @config[:graceful_shutdown] = nil if @config[:graceful_shutdown] < 0
@@ -131,6 +135,7 @@ module Skiplock
131
135
  else
132
136
  @config[:notification] = 'custom'
133
137
  end
138
+ Skiplock.namespace = @config[:namespace]
134
139
  Skiplock.on_errors.freeze
135
140
  end
136
141
 
@@ -145,6 +150,7 @@ module Skiplock
145
150
  end
146
151
  if @config[:standalone]
147
152
  Rails.logger.reopen('/dev/null') rescue Rails.logger.reopen('NUL') # supports Windows NUL device
153
+ Rails.logger.level = @logger.level
148
154
  Rails.logger.extend(ActiveSupport::Logger.broadcast(@logger))
149
155
  end
150
156
  rescue Exception => ex
@@ -153,4 +159,4 @@ module Skiplock
153
159
  Skiplock.on_errors.each { |p| p.call(ex) }
154
160
  end
155
161
  end
156
- end
162
+ end
@@ -1,4 +1,4 @@
1
1
  module Skiplock
2
- VERSION = Version = '1.0.22'
2
+ VERSION = Version = '1.1.0'
3
3
  end
4
4
 
@@ -5,7 +5,7 @@ module Skiplock
5
5
 
6
6
  def self.cleanup(hostname = nil)
7
7
  delete_ids = []
8
- self.where(hostname: hostname || Socket.gethostname).each do |worker|
8
+ self.where(namespace: Skiplock.namespace, hostname: hostname || Socket.gethostname).each do |worker|
9
9
  sid = Process.getsid(worker.pid) rescue nil
10
10
  delete_ids << worker.id if worker.sid != sid || worker.updated_at < 10.minutes.ago
11
11
  end
@@ -13,115 +13,119 @@ module Skiplock
13
13
  end
14
14
 
15
15
  def self.generate(capacity:, hostname:, master: true)
16
- self.create!(pid: Process.pid, sid: Process.getsid(), master: master, hostname: hostname, capacity: capacity)
16
+ worker = self.create!(pid: Process.pid, sid: Process.getsid(), master: master, hostname: hostname, capacity: capacity, namespace: Skiplock.namespace)
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, namespace: Skiplock.namespace)
19
19
  end
20
20
 
21
21
  def shutdown
22
22
  @running = false
23
23
  @executor.shutdown
24
24
  @executor.kill unless @executor.wait_for_termination(@config[:graceful_shutdown])
25
- self.delete
26
25
  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
26
  end
28
27
 
29
28
  def start(worker_num: 0, **config)
30
29
  @num = worker_num
31
30
  @config = config
31
+ @pg_config = ActiveRecord::Base.connection.raw_connection.conninfo_hash.compact
32
+ @namespace_query = Skiplock.namespace.nil? ? "namespace IS NULL" : "namespace = '#{Skiplock.namespace}'"
32
33
  @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
33
34
  @running = true
34
- @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: :discard)
35
+ @map = ::PG::TypeMapByOid.new
36
+ @map.add_coder(::PG::TextDecoder::Boolean.new(oid: 16, name: 'bool'))
37
+ @map.add_coder(::PG::TextDecoder::Integer.new(oid: 20, name: 'int8'))
38
+ @map.add_coder(::PG::TextDecoder::Integer.new(oid: 21, name: 'int2'))
39
+ @map.add_coder(::PG::TextDecoder::Integer.new(oid: 23, name: 'int4'))
40
+ @map.add_coder(::PG::TextDecoder::TimestampUtc.new(oid: 1114, name: 'timestamp'))
41
+ @map.add_coder(::PG::TextDecoder::String.new(oid: 2950, name: 'uuid'))
42
+ @map.add_coder(::PG::TextDecoder::JSON.new(oid: 3802, name: 'jsonb'))
43
+ @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)
35
44
  @executor.post { run }
36
- 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}]") if @config[:standalone]
45
+ if @config[:standalone]
46
+ 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}]")
47
+ ActiveRecord::Base.connection.throw_away!
48
+ end
37
49
  end
38
50
 
39
51
  private
40
52
 
53
+ def establish_connection
54
+ @connection = ::PG.connect(@pg_config)
55
+ @connection.type_map_for_results = @map
56
+ @connection.exec('LISTEN "skiplock::jobs"').clear
57
+ @connection.exec('LISTEN "skiplock::workers"').clear
58
+ end
59
+
41
60
  def run
42
61
  sleep 3
43
62
  Skiplock.logger.info "[Skiplock] Starting in #{@config[:standalone] ? 'standalone' : 'async'} mode (PID: #{self.pid}) with #{@config[:max_threads]} max threads as #{self.master ? 'master' : 'cluster'} worker#{(' ' + @num.to_s) if @num > 0 && @config[:workers] > 2}..."
44
- connection = nil
45
- error = false
46
- listen = false
47
63
  next_schedule_at = Time.now.to_f
48
64
  pg_exception_timestamp = nil
49
65
  timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
50
66
  while @running
51
67
  Rails.application.reloader.wrap do
52
68
  begin
53
- unless listen
54
- connection = self.class.connection
55
- connection.exec_query('LISTEN "skiplock::jobs"')
56
- if self.master
57
- Job.flush
58
- Cron.setup
59
- end
60
- listen = true
61
- end
62
- if error
63
- unless connection.active?
64
- connection.reconnect!
65
- sleep(0.5)
66
- connection.exec_query('LISTEN "skiplock::jobs"')
67
- Job.flush if self.master
68
- pg_exception_timestamp = nil
69
- next_schedule_at = Time.now.to_f
70
- end
71
- error = false
69
+ if @connection.nil? || @connection.status != ::PG::CONNECTION_OK
70
+ establish_connection
71
+ @executor.post { Rails.application.executor.wrap { Job.flush } } if self.master
72
+ pg_exception_timestamp = nil
73
+ next_schedule_at = Time.now.to_f
72
74
  end
73
75
  if Time.now.to_f >= next_schedule_at && @executor.remaining_capacity > 1 # reserves 1 slot in queue for Job.flush in case of pg_connection error
74
76
  result = nil
75
- connection.transaction do
76
- result = connection.select_all("SELECT id, running, scheduled_at FROM skiplock.jobs WHERE running = FALSE AND expired_at IS NULL AND finished_at IS NULL ORDER BY scheduled_at ASC NULLS FIRST,#{@queues_order_query ? ' CASE ' + @queues_order_query + ' ELSE NULL END ASC NULLS LAST,' : ''} priority ASC NULLS LAST, created_at ASC FOR UPDATE SKIP LOCKED LIMIT 1").first
77
- result = connection.select_all("UPDATE skiplock.jobs SET running = TRUE, worker_id = '#{self.id}', updated_at = NOW() WHERE id = '#{result['id']}' RETURNING *").first if result && result['scheduled_at'].to_f <= Time.now.to_f
77
+ @connection.transaction do |conn|
78
+ conn.exec("SELECT id, running, scheduled_at FROM skiplock.jobs WHERE running = FALSE AND expired_at IS NULL AND finished_at IS NULL AND #{@namespace_query} ORDER BY scheduled_at ASC NULLS FIRST,#{@queues_order_query ? ' CASE ' + @queues_order_query + ' ELSE NULL END ASC NULLS LAST,' : ''} priority ASC NULLS LAST, created_at ASC FOR UPDATE SKIP LOCKED LIMIT 1") do |r|
79
+ result = r.first
80
+ conn.exec("UPDATE skiplock.jobs SET running = TRUE, worker_id = '#{self.id}', updated_at = NOW() WHERE id = '#{result['id']}' RETURNING *") { |r| result = r.first } if result && result['scheduled_at'].to_f <= Time.now.to_f
81
+ end
78
82
  end
79
83
  if result && result['running']
80
- @executor.post do
81
- Rails.application.executor.wrap { Job.instantiate(result).execute(purge_completion: @config[:purge_completion], max_retries: @config[:max_retries]) }
82
- end
84
+ @executor.post { Rails.application.executor.wrap { Job.instantiate(result).execute(purge_completion: @config[:purge_completion], max_retries: @config[:max_retries]) } }
83
85
  else
84
86
  next_schedule_at = (result ? result['scheduled_at'].to_f : Float::INFINITY)
85
87
  end
86
88
  end
87
- job_notifications = []
88
- connection.raw_connection.wait_for_notify(0.2) do |channel, pid, payload|
89
- job_notifications << payload if payload
89
+ notifications = { 'skiplock::jobs' => [], 'skiplock::workers' => [] }
90
+ @connection.wait_for_notify(0.2) do |channel, pid, payload|
91
+ notifications[channel] << payload if payload
90
92
  loop do
91
- payload = connection.raw_connection.notifies
93
+ payload = @connection.notifies
92
94
  break unless @running && payload
93
- job_notifications << payload[:extra]
95
+ notifications[payload[:relname]] << payload[:extra]
94
96
  end
95
- job_notifications.each do |n|
96
- op, id, worker_id, job_class, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
97
- next if op == 'DELETE' || running == 'true' || expired_at.to_f > 0 || finished_at.to_f > 0
97
+ notifications['skiplock::jobs'].each do |n|
98
+ op, id, worker_id, namespace, job_class, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
99
+ next if op == 'DELETE' || running == 'true' || namespace != Skiplock.namespace || expired_at.to_f > 0 || finished_at.to_f > 0
98
100
  next_schedule_at = scheduled_at.to_f if scheduled_at.to_f < next_schedule_at
99
101
  end
100
102
  end
101
103
  if Process.clock_gettime(Process::CLOCK_MONOTONIC) - timestamp > 60
102
- connection.exec_query("UPDATE skiplock.workers SET updated_at = NOW() WHERE id = '#{self.id}'")
104
+ @connection.exec("UPDATE skiplock.workers SET updated_at = NOW() WHERE id = '#{self.id}'").clear
103
105
  timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
104
106
  end
105
107
  rescue Exception => ex
106
- Skiplock.logger.error(ex.to_s)
107
- Skiplock.logger.error(ex.backtrace.join("\n"))
108
108
  report_exception = true
109
109
  # if error is with database connection then only report if it persists longer than 1 minute
110
- if ex.is_a?(::PG::ConnectionBad) ||ex.is_a?(::PG::UnableToSend) || ex.message.include?('Bad file descriptor')
110
+ if @connection.nil? || @connection.status != ::PG::CONNECTION_OK
111
111
  report_exception = false if pg_exception_timestamp.nil? || Process.clock_gettime(Process::CLOCK_MONOTONIC) - pg_exception_timestamp <= 60
112
112
  pg_exception_timestamp ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
113
113
  end
114
- Skiplock.on_errors.each { |p| p.call(ex) } if report_exception
115
- error = true
114
+ if report_exception
115
+ Skiplock.logger.error(ex.to_s)
116
+ Skiplock.logger.error(ex.backtrace.join("\n"))
117
+ Skiplock.on_errors.each { |p| p.call(ex) }
118
+ end
116
119
  wait(5)
117
120
  end
118
121
  sleep(0.3)
119
122
  end
120
123
  end
121
- connection.exec_query('UNLISTEN *')
124
+ ensure
125
+ @connection.close if @connection && !@connection.finished?
122
126
  end
123
127
 
124
- def wait(timeout)
128
+ def wait(timeout = 1)
125
129
  t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
126
130
  while @running
127
131
  sleep(0.5)
@@ -129,4 +133,4 @@ module Skiplock
129
133
  end
130
134
  end
131
135
  end
132
- end
136
+ end
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', 'namespace' => nil, 'notification' => 'custom', '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
@@ -21,6 +21,14 @@ module Skiplock
21
21
  @logger
22
22
  end
23
23
 
24
+ def self.namespace=(n)
25
+ @namespace = n
26
+ end
27
+
28
+ def self.namespace
29
+ @namespace || ''
30
+ end
31
+
24
32
  def self.on_error(&block)
25
33
  @on_errors ||= []
26
34
  @on_errors << block
@@ -34,5 +42,4 @@ module Skiplock
34
42
  def self.table_name_prefix
35
43
  'skiplock.'
36
44
  end
37
- end
38
- ActiveJob::Base.__send__(:include, Skiplock::Patch)
45
+ 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.22
4
+ version: 1.1.0
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: 2022-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob