que-locks 0.3.0 → 0.4.0

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: 809491912a7146d6ac1d4df762d88efb1e4bc9a1ae037769b937275fea35b502
4
- data.tar.gz: b562eb878454d0a03939cbbcd3b81371478c7f36aea12c428a568da6ea23dc19
3
+ metadata.gz: 9d0ffcedfe00f4e3e4e6b609c345bfb8f5af2f13c31c7f21394e1e43c71458c1
4
+ data.tar.gz: 80c89326dfbab470400c543be8760e4966103d1d781602db4135e903f1a38234
5
5
  SHA512:
6
- metadata.gz: 12a5d34c46c3eadd293885796eedc264b77c751cc5f31ec063e75a6a878f5ec7bec42c1716e51798db3006fa6be44419129e9421da1d012cd8aa1432e668fbb7
7
- data.tar.gz: 63185315f4926d0d2226c0231e207c9c4417c443f56deac59dd771711a64c142cb1906cce1a8b898b8bc31902279d8f5cc34d4d213b592f5993f1ebc46d44ba7
6
+ metadata.gz: 4eedc15d8acf9d9a37cb0ccda407b26c2d7505f259ccabac9d469c9862070a90bd592dd2f6b9dca16fa51bc967077ce12319cf82924a713d2c700d253c8ddf5a
7
+ data.tar.gz: c4f436197a6546866b8b3d25441f5a548d007c1ae64c7adb218c9b95ea5792be4c68b88f4692cc862c4b32a2e8f56e30d26a4b87d23415b84c4e1755ac297367
data/Gemfile CHANGED
@@ -8,6 +8,7 @@ gemspec
8
8
  group :development do
9
9
  gem "activerecord"
10
10
  gem "activejob"
11
+ gem "railties"
11
12
  gem "pg"
12
13
  gem "database_cleaner"
13
14
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- que-locks (0.3.0)
4
+ que-locks (0.4.0)
5
5
  neatjson (~> 0.9)
6
6
  que (~> 1.0)
7
7
  xxhash (~> 0.4)
@@ -9,42 +9,81 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- activejob (6.1.6.1)
13
- activesupport (= 6.1.6.1)
12
+ actionpack (6.1.4.7)
13
+ actionview (= 6.1.4.7)
14
+ activesupport (= 6.1.4.7)
15
+ rack (~> 2.0, >= 2.0.9)
16
+ rack-test (>= 0.6.3)
17
+ rails-dom-testing (~> 2.0)
18
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
19
+ actionview (6.1.4.7)
20
+ activesupport (= 6.1.4.7)
21
+ builder (~> 3.1)
22
+ erubi (~> 1.4)
23
+ rails-dom-testing (~> 2.0)
24
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
25
+ activejob (6.1.4.7)
26
+ activesupport (= 6.1.4.7)
14
27
  globalid (>= 0.3.6)
15
- activemodel (6.1.6.1)
16
- activesupport (= 6.1.6.1)
17
- activerecord (6.1.6.1)
18
- activemodel (= 6.1.6.1)
19
- activesupport (= 6.1.6.1)
20
- activesupport (6.1.6.1)
28
+ activemodel (6.1.4.7)
29
+ activesupport (= 6.1.4.7)
30
+ activerecord (6.1.4.7)
31
+ activemodel (= 6.1.4.7)
32
+ activesupport (= 6.1.4.7)
33
+ activesupport (6.1.4.7)
21
34
  concurrent-ruby (~> 1.0, >= 1.0.2)
22
35
  i18n (>= 1.6, < 2)
23
36
  minitest (>= 5.1)
24
37
  tzinfo (~> 2.0)
25
38
  zeitwerk (~> 2.3)
26
39
  ast (2.4.2)
40
+ builder (3.2.4)
27
41
  byebug (11.1.3)
28
42
  concurrent-ruby (1.1.10)
43
+ crass (1.0.6)
29
44
  database_cleaner (2.0.1)
30
45
  database_cleaner-active_record (~> 2.0.0)
31
46
  database_cleaner-active_record (2.0.1)
32
47
  activerecord (>= 5.a)
33
48
  database_cleaner-core (~> 2.0.0)
34
49
  database_cleaner-core (2.0.1)
50
+ erubi (1.10.0)
35
51
  globalid (1.0.0)
36
52
  activesupport (>= 5.0)
37
53
  i18n (1.12.0)
38
54
  concurrent-ruby (~> 1.0)
39
55
  json (2.6.2)
56
+ loofah (2.18.0)
57
+ crass (~> 1.0.2)
58
+ nokogiri (>= 1.5.9)
59
+ method_source (1.0.0)
60
+ mini_portile2 (2.8.0)
40
61
  minitest (5.16.2)
41
62
  mocha (1.14.0)
42
63
  neatjson (0.9)
64
+ nokogiri (1.13.8)
65
+ mini_portile2 (~> 2.8.0)
66
+ racc (~> 1.4)
43
67
  parallel (1.22.1)
44
68
  parser (3.1.2.0)
45
69
  ast (~> 2.4.1)
46
70
  pg (1.4.2)
47
71
  que (1.4.1)
72
+ racc (1.6.0)
73
+ rack (2.2.4)
74
+ rack-test (2.0.2)
75
+ rack (>= 1.3)
76
+ rails-dom-testing (2.0.3)
77
+ activesupport (>= 4.2.0)
78
+ nokogiri (>= 1.6)
79
+ rails-html-sanitizer (1.4.3)
80
+ loofah (~> 2.3)
81
+ railties (6.1.4.7)
82
+ actionpack (= 6.1.4.7)
83
+ activesupport (= 6.1.4.7)
84
+ method_source
85
+ rake (>= 0.13)
86
+ thor (~> 1.0)
48
87
  rainbow (3.1.1)
49
88
  rake (10.5.0)
50
89
  regexp_parser (2.5.0)
@@ -66,6 +105,7 @@ GEM
66
105
  rubocop-ast (>= 0.4.0)
67
106
  ruby-progressbar (1.11.0)
68
107
  rufo (0.13.0)
108
+ thor (1.2.1)
69
109
  tzinfo (2.0.5)
70
110
  concurrent-ruby (~> 1.0)
71
111
  unicode-display_width (2.2.0)
@@ -85,6 +125,7 @@ DEPENDENCIES
85
125
  mocha
86
126
  pg
87
127
  que-locks!
128
+ railties
88
129
  rake (~> 10.0)
89
130
  rubocop
90
131
  rubocop-performance
@@ -0,0 +1,29 @@
1
+ module Que::Locks
2
+ module ActiveJobExtensions
3
+ class ExclusiveJobWrapper < ::ActiveJob::QueueAdapters::QueAdapter::JobWrapper
4
+ # Opt into the locking functionality provided by que-locks
5
+ self.exclusive_execution_lock = true
6
+ end
7
+
8
+ def enqueue(job)
9
+ return super unless job.class.exclusive_execution_lock
10
+ do_enqueue job
11
+ end
12
+
13
+ def enqueue_at(job, timestamp)
14
+ return super unless job.class.exclusive_execution_lock
15
+ do_enqueue job, run_at: Time.at(timestamp)
16
+ end
17
+
18
+ def do_enqueue(job, **job_options)
19
+ job_options[:priority] = job.priority if job.respond_to? :priority
20
+ job_options[:queue] = job.queue_name if job.respond_to? :queue_name
21
+
22
+ que_job = ExclusiveJobWrapper.enqueue job.serialize, job_options: job_options
23
+ if que_job && job.respond_to?(:provider_job_id=)
24
+ job.provider_job_id = que_job.attrs["job_id"]
25
+ end
26
+ que_job
27
+ end
28
+ end
29
+ end
@@ -2,10 +2,14 @@ require "xxhash"
2
2
 
3
3
  module Que
4
4
  SQL[:args_already_enqueued] = %{
5
- SELECT COUNT(*) FROM public.que_jobs WHERE job_class = $1 AND args = $2 AND finished_at IS NULL AND expired_at IS NULL;
5
+ SELECT COUNT(*) FROM public.que_jobs WHERE job_class = $1 AND args = $2 AND finished_at IS NULL AND expired_at IS NULL LIMIT 1;
6
6
  }
7
7
 
8
- SQL[:try_aquire_execution_lock] = %{
8
+ SQL[:active_job_args_already_enqueued] = %{
9
+ SELECT COUNT(*) FROM public.que_jobs WHERE job_class = $1 AND args->0 @> $2 AND finished_at IS NULL AND expired_at IS NULL LIMIT 1;
10
+ }
11
+
12
+ SQL[:try_acquire_execution_lock] = %{
9
13
  SELECT pg_try_advisory_lock(42, $1) AS locked
10
14
  }
11
15
 
@@ -16,19 +20,25 @@ module Que
16
20
  module Locks::ExecutionLock
17
21
  class << self
18
22
  def already_enqueued_job_wanting_lock?(klass, args)
23
+ query = :args_already_enqueued
24
+ if active_job_class?(klass)
25
+ args = active_jobless_args(args)
26
+ query = :active_job_args_already_enqueued
27
+ end
28
+
19
29
  args_string = Que.serialize_json(args)
20
- values = Que.execute(:args_already_enqueued, [klass.name, args_string]).first
30
+ values = Que.execute(query, [klass.name, args_string]).first
21
31
  values[:count] != 0
22
32
  end
23
33
 
24
- def can_aquire?(klass, args)
25
- can_aquire_key?(lock_key(klass, args))
34
+ def can_acquire?(klass, args)
35
+ can_acquire_key?(lock_key(klass, args))
26
36
  end
27
37
 
28
- def can_aquire_key?(key)
38
+ def can_acquire_key?(key)
29
39
  result = false
30
40
  begin
31
- result = aquire!(key)
41
+ result = acquire!(key)
32
42
  ensure
33
43
  if result
34
44
  release!(key)
@@ -38,17 +48,43 @@ module Que
38
48
  end
39
49
 
40
50
  def lock_key(klass, args)
51
+ if active_job_class?(klass)
52
+ args = active_jobless_args(args)
53
+ end
41
54
  XXhash.xxh32(klass.name + ":" + Que.serialize_json(args), 42) / 2
42
55
  end
43
56
 
44
- def aquire!(key)
45
- result = Que.execute(:try_aquire_execution_lock, [key]).first
57
+ def acquire!(key)
58
+ result = Que.execute(:try_acquire_execution_lock, [key]).first
46
59
  result[:locked]
47
60
  end
48
61
 
49
62
  def release!(key)
50
63
  Que.execute(:release_execution_lock, [key])
51
64
  end
65
+
66
+ private
67
+
68
+ def active_job_class?(klass)
69
+ if Object.const_defined?("ActiveJob::QueueAdapters::QueAdapter::JobWrapper")
70
+ return klass.ancestors.include? ::ActiveJob::QueueAdapters::QueAdapter::JobWrapper
71
+ end
72
+ false
73
+ end
74
+
75
+ def active_jobless_args(args)
76
+ # ActiveJob handles its own arguments, and thus comes in as one argument:
77
+ # a single serialized hash representing the job. We want to use this hash
78
+ # to check and see if we already have an equivalent job queued up, but that
79
+ # requires us to toss out irrelevant ActiveJob-specific parameters that will
80
+ # throw our check off. There are enough of these
81
+ # (see ActiveJob::Core#serialize) that it's easier to maintain a whitelist;
82
+ # toss everything else.
83
+ hash = args.first
84
+ okay_keys = ["job_class", "arguments"]
85
+ # Careful, this is a shallow copy, don't actually modify that hash
86
+ hash.reject { |key| !okay_keys.include?(key) }
87
+ end
52
88
  end
53
89
  end
54
90
  end
@@ -6,7 +6,7 @@ module Que::Locks
6
6
  args << kwargs if kwargs.any?
7
7
  return true unless self.exclusive_execution_lock
8
8
  return false if Que::Locks::ExecutionLock.already_enqueued_job_wanting_lock?(self, args)
9
- return Que::Locks::ExecutionLock.can_aquire?(self, args)
9
+ return Que::Locks::ExecutionLock.can_acquire?(self, args)
10
10
  end
11
11
 
12
12
  def enqueue(*args, queue: nil, priority: nil, run_at: nil, job_class: nil, tags: nil, job_options: {}, **kwargs)
@@ -25,12 +25,16 @@ module Que::Locks
25
25
 
26
26
  if Que::Locks::ExecutionLock.already_enqueued_job_wanting_lock?(self, args_list)
27
27
  Que.log(level: :info, event: :skipped_enqueue_due_to_preemptive_lock_check, args: args_list)
28
- else
29
- super(*args, **forwardable_kwargs)
28
+ # This technically breaks API compatibility with que, which always
29
+ # returns a job. It could be argued that we should return the
30
+ # already-enqueued job, but then we'd lose the ability to signal to
31
+ # the caller that a job wasn't actually enqueued. Let's see how
32
+ # far we can get with this.
33
+ return
30
34
  end
31
- else
32
- super(*args, **forwardable_kwargs)
33
35
  end
36
+
37
+ super(*args, **forwardable_kwargs)
34
38
  end
35
39
  end
36
40
  end
@@ -3,7 +3,7 @@ module Que::Locks
3
3
  if job.class.exclusive_execution_lock
4
4
  args = job.que_attrs[:args]
5
5
  lock_key = ExecutionLock.lock_key(job.class, args)
6
- if ExecutionLock.aquire!(lock_key)
6
+ if ExecutionLock.acquire!(lock_key)
7
7
  begin
8
8
  block.call
9
9
  ensure
@@ -0,0 +1,13 @@
1
+ module Que::Locks
2
+ class Railtie < Rails::Railtie
3
+ initializer "que-locks.patch_active_job" do
4
+ ActiveSupport.on_load(:active_job) do
5
+ class_attribute :exclusive_execution_lock
6
+
7
+ require_relative "active_job_extensions"
8
+ ActiveJob::QueueAdapters::QueAdapter.prepend(Que::Locks::ActiveJobExtensions)
9
+ ActiveJob::QueueAdapters::QueAdapter.singleton_class.prepend(Que::Locks::ActiveJobExtensions) # for rails 4
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,5 +1,5 @@
1
1
  module Que
2
2
  module Locks
3
- VERSION = "0.3.0".freeze
3
+ VERSION = "0.4.0".freeze
4
4
  end
5
5
  end
data/lib/que/locks.rb CHANGED
@@ -1,8 +1,10 @@
1
+ require "que"
1
2
  require "que/locks/version"
2
3
  require "que/locks/execution_lock"
3
4
  require "que/locks/json_extensions"
4
5
  require "que/locks/job_extensions"
5
6
  require "que/locks/lock_middleware"
7
+ require "que/locks/railtie" if defined?(Rails::Railtie)
6
8
 
7
9
  module Que
8
10
  module Locks
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: que-locks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harry Brundage
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-07-29 00:00:00.000000000 Z
11
+ date: 2022-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -103,10 +103,12 @@ files:
103
103
  - bin/setup
104
104
  - docker-compose.yml
105
105
  - lib/que/locks.rb
106
+ - lib/que/locks/active_job_extensions.rb
106
107
  - lib/que/locks/execution_lock.rb
107
108
  - lib/que/locks/job_extensions.rb
108
109
  - lib/que/locks/json_extensions.rb
109
110
  - lib/que/locks/lock_middleware.rb
111
+ - lib/que/locks/railtie.rb
110
112
  - lib/que/locks/version.rb
111
113
  - que-locks.gemspec
112
114
  homepage: https://github.com/superpro-inc/que-locks