delayed_job_prevent_duplicate 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +44 -0
- data/README.md +11 -9
- data/delayed_job_prevent_duplicate.gemspec +1 -1
- data/lib/delayed_job_prevent_duplicate/duplicate_checker.rb +21 -0
- data/lib/delayed_job_prevent_duplicate/signature_concern.rb +76 -0
- data/lib/delayed_job_prevent_duplicate/version.rb +1 -1
- data/lib/delayed_job_prevent_duplicate.rb +4 -4
- data/lib/generators/delayed_job_prevent_duplicate/install_generator.rb +16 -0
- data/lib/generators/templates/migration.rb.tt +7 -0
- metadata +10 -7
- data/lib/.DS_Store +0 -0
- data/lib/delayed_duplicate_prevention_plugin.rb +0 -106
- data/lib/generators/delayed_job_prevent_duplicate_generator.rb +0 -29
- data/lib/generators/templates/migration.rb +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: de83fe452a89cce8af86bad6d543b0b564a48a8c097e34283f9b13bac1c22b0d
|
4
|
+
data.tar.gz: 3d4419ae6d770c137d146914e51e09d0ba88d7f38bbda751036763a64dc470d5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ab4227f7e5ec6c6be4a0012fdea1aa1cd864022f7798c81a0c97d8f7131c0643c84fa4b80bd295414fd9e08e76c1b0b6ad95d23eb7e57c67334c657ae5dc569f
|
7
|
+
data.tar.gz: 951f714521b654d1f09b3e024d27f2d50f5e3fb83e6f3ff4d642c2da16968c0c0f198d55ec361ec46f56103c0a7199f868cae99b87f45d6d66387cb2ed3f6b06
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
delayed_job_prevent_duplicate (0.2.0)
|
5
|
+
delayed_job (>= 3.0, < 5)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activesupport (7.1.3.2)
|
11
|
+
base64
|
12
|
+
bigdecimal
|
13
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
14
|
+
connection_pool (>= 2.2.5)
|
15
|
+
drb
|
16
|
+
i18n (>= 1.6, < 2)
|
17
|
+
minitest (>= 5.1)
|
18
|
+
mutex_m
|
19
|
+
tzinfo (~> 2.0)
|
20
|
+
base64 (0.2.0)
|
21
|
+
bigdecimal (3.1.7)
|
22
|
+
concurrent-ruby (1.2.3)
|
23
|
+
connection_pool (2.4.1)
|
24
|
+
delayed_job (4.1.11)
|
25
|
+
activesupport (>= 3.0, < 8.0)
|
26
|
+
drb (2.2.1)
|
27
|
+
i18n (1.14.4)
|
28
|
+
concurrent-ruby (~> 1.0)
|
29
|
+
minitest (5.22.3)
|
30
|
+
mutex_m (0.2.0)
|
31
|
+
rake (13.2.1)
|
32
|
+
tzinfo (2.0.6)
|
33
|
+
concurrent-ruby (~> 1.0)
|
34
|
+
|
35
|
+
PLATFORMS
|
36
|
+
arm64-darwin-23
|
37
|
+
ruby
|
38
|
+
|
39
|
+
DEPENDENCIES
|
40
|
+
delayed_job_prevent_duplicate!
|
41
|
+
rake (~> 13.0)
|
42
|
+
|
43
|
+
BUNDLED WITH
|
44
|
+
2.5.6
|
data/README.md
CHANGED
@@ -1,12 +1,14 @@
|
|
1
|
-
#
|
1
|
+
# Delayed Job Prevent Duplicate
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/a7d6801105cc0df16e37/maintainability)](https://codeclimate.com/github/noesya/delayed_job_prevent_duplicate/maintainability)
|
4
|
+
|
5
|
+
The purpose of this gem is to prevent to re-enqueue on DelayedJob a task already enqueued.
|
6
|
+
So we set a "signature" attached to every task enqueued which is a composite from the class and the id of the object, and the method called.
|
7
|
+
And then when creating a new job we look in the "pending" jobs if there is another one with the same signature (not in the "working" one because a task can be executed and yet you want to re-excute it because of any change).
|
6
8
|
|
7
9
|
## Note
|
8
10
|
|
9
|
-
This gem is based on the [synth](https://github.com/synth)
|
11
|
+
This gem is based on the work of [synth](https://github.com/synth): https://gist.github.com/synth/fba7baeffd083a931184
|
10
12
|
|
11
13
|
## Installation
|
12
14
|
|
@@ -22,13 +24,13 @@ And then execute:
|
|
22
24
|
|
23
25
|
$ bundle install
|
24
26
|
|
25
|
-
Next, you need to run the generator:
|
27
|
+
Next, you need to run the generator:
|
26
28
|
|
27
29
|
```ruby
|
28
|
-
rails g delayed_job_prevent_duplicate
|
30
|
+
rails g delayed_job_prevent_duplicate:install
|
29
31
|
```
|
30
|
-
|
31
|
-
All should be fine now!
|
32
|
+
|
33
|
+
All should be fine now!
|
32
34
|
|
33
35
|
|
34
36
|
## Contributing
|
@@ -5,7 +5,7 @@ require_relative "lib/delayed_job_prevent_duplicate/version"
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
6
|
spec.name = "delayed_job_prevent_duplicate"
|
7
7
|
spec.version = DelayedJobPreventDuplicate::VERSION
|
8
|
-
spec.authors = ["pabois"]
|
8
|
+
spec.authors = ["pabois", "arnaudlevy", "SebouChu"]
|
9
9
|
spec.email = ["pierreandre.boissinot@noesya.coop"]
|
10
10
|
|
11
11
|
spec.summary = "Prevent delayed_job to enqueue a task already enqueued"
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module DelayedJobPreventDuplicate
|
2
|
+
class DuplicateChecker
|
3
|
+
attr_reader :job
|
4
|
+
|
5
|
+
def self.duplicate?(job)
|
6
|
+
new(job).duplicate?
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(job)
|
10
|
+
@job = job
|
11
|
+
end
|
12
|
+
|
13
|
+
def duplicate?
|
14
|
+
# Looking for jobs with the same signature.
|
15
|
+
# Only jobs not started, otherwise it would never compute a real change if the job is currently running
|
16
|
+
duplicates = Delayed::Job.where(locked_at: nil, signature: job.signature)
|
17
|
+
duplicates = duplicates.where.not(id: job.id) if job.id.present?
|
18
|
+
duplicates.exists?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require_relative "duplicate_checker"
|
2
|
+
|
3
|
+
module DelayedJobPreventDuplicate
|
4
|
+
module SignatureConcern
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
before_validation :generate_signature
|
9
|
+
validate :prevent_duplicate
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def generate_signature
|
15
|
+
self.signature = signature_from_denormalized_data || random_signature
|
16
|
+
end
|
17
|
+
|
18
|
+
def signature_from_denormalized_data
|
19
|
+
begin
|
20
|
+
Digest::MD5.hexdigest(denormalized_data.to_json)
|
21
|
+
rescue
|
22
|
+
puts "DelayedJobPreventDuplicate could not generate the signature correctly."
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def random_signature
|
28
|
+
SecureRandom.uuid
|
29
|
+
end
|
30
|
+
|
31
|
+
def denormalized_data
|
32
|
+
@denormalize_data ||= begin
|
33
|
+
if payload_object.is_a?(Delayed::PerformableMethod)
|
34
|
+
denormalized_data_for_performable_method
|
35
|
+
else
|
36
|
+
denormalized_data_for_job_wrapper
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Methods tagged with handle_asynchronously
|
42
|
+
def denormalized_data_for_performable_method
|
43
|
+
{
|
44
|
+
object: payload_object.object.to_global_id.to_s,
|
45
|
+
method_name: payload_object.method_name,
|
46
|
+
args: stringify_arguments(payload_object.args)
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
# Regular Job
|
51
|
+
def denormalized_data_for_job_wrapper
|
52
|
+
{
|
53
|
+
job_class: payload_object.job_data["job_class"],
|
54
|
+
args: stringify_arguments(payload_object.job_data["arguments"])
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def stringify_arguments(arguments)
|
59
|
+
serialize_arguments(arguments).join('|')
|
60
|
+
end
|
61
|
+
|
62
|
+
def serialize_arguments(arguments)
|
63
|
+
arguments.map { |argument|
|
64
|
+
argument.is_a?(ActiveRecord::Base) ? argument.to_global_id.to_s
|
65
|
+
: argument.to_json
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def prevent_duplicate
|
70
|
+
if DuplicateChecker.duplicate?(self)
|
71
|
+
Rails.logger.warn "Found duplicate job(#{self.signature}), ignoring..."
|
72
|
+
errors.add(:base, "This is a duplicate")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "delayed_job"
|
4
|
+
|
5
|
+
require_relative "delayed_job_prevent_duplicate/signature_concern"
|
3
6
|
require_relative "delayed_job_prevent_duplicate/version"
|
4
7
|
|
5
8
|
module DelayedJobPreventDuplicate
|
6
9
|
class Error < StandardError; end
|
7
|
-
|
8
|
-
require 'delayed_duplicate_prevention_plugin'
|
9
10
|
|
10
|
-
Delayed::Backend::ActiveRecord::Job.send(:include,
|
11
|
-
Delayed::Worker.plugins << DelayedDuplicatePreventionPlugin
|
11
|
+
Delayed::Backend::ActiveRecord::Job.send(:include, SignatureConcern)
|
12
12
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/base"
|
4
|
+
require "rails/generators/active_record/migration"
|
5
|
+
|
6
|
+
module DelayedJobPreventDuplicate
|
7
|
+
class InstallGenerator < ::Rails::Generators::Base
|
8
|
+
include ActiveRecord::Generators::Migration
|
9
|
+
source_root File.expand_path("../../templates", __FILE__)
|
10
|
+
|
11
|
+
def copy_migration
|
12
|
+
migration_template "migration.rb", File.join(db_migrate_path, "add_signature_to_delayed_job.rb")
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: delayed_job_prevent_duplicate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- pabois
|
8
|
+
- arnaudlevy
|
9
|
+
- SebouChu
|
8
10
|
autorequire:
|
9
11
|
bindir: exe
|
10
12
|
cert_chain: []
|
11
|
-
date:
|
13
|
+
date: 2024-04-29 00:00:00.000000000 Z
|
12
14
|
dependencies:
|
13
15
|
- !ruby/object:Gem::Dependency
|
14
16
|
name: delayed_job
|
@@ -40,18 +42,19 @@ extra_rdoc_files: []
|
|
40
42
|
files:
|
41
43
|
- CHANGELOG.md
|
42
44
|
- Gemfile
|
45
|
+
- Gemfile.lock
|
43
46
|
- LICENSE.txt
|
44
47
|
- README.md
|
45
48
|
- Rakefile
|
46
49
|
- bin/console
|
47
50
|
- bin/setup
|
48
51
|
- delayed_job_prevent_duplicate.gemspec
|
49
|
-
- lib/.DS_Store
|
50
|
-
- lib/delayed_duplicate_prevention_plugin.rb
|
51
52
|
- lib/delayed_job_prevent_duplicate.rb
|
53
|
+
- lib/delayed_job_prevent_duplicate/duplicate_checker.rb
|
54
|
+
- lib/delayed_job_prevent_duplicate/signature_concern.rb
|
52
55
|
- lib/delayed_job_prevent_duplicate/version.rb
|
53
|
-
- lib/generators/
|
54
|
-
- lib/generators/templates/migration.rb
|
56
|
+
- lib/generators/delayed_job_prevent_duplicate/install_generator.rb
|
57
|
+
- lib/generators/templates/migration.rb.tt
|
55
58
|
homepage: https://github.com/noesya/delayed_job_prevent_duplicate
|
56
59
|
licenses:
|
57
60
|
- MIT
|
@@ -74,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
77
|
- !ruby/object:Gem::Version
|
75
78
|
version: '0'
|
76
79
|
requirements: []
|
77
|
-
rubygems_version: 3.
|
80
|
+
rubygems_version: 3.5.6
|
78
81
|
signing_key:
|
79
82
|
specification_version: 4
|
80
83
|
summary: Prevent delayed_job to enqueue a task already enqueued
|
data/lib/.DS_Store
DELETED
Binary file
|
@@ -1,106 +0,0 @@
|
|
1
|
-
# based on https://gist.github.com/synth/fba7baeffd083a931184
|
2
|
-
|
3
|
-
require 'delayed_job'
|
4
|
-
|
5
|
-
class DelayedDuplicatePreventionPlugin < Delayed::Plugin
|
6
|
-
|
7
|
-
module SignatureConcern
|
8
|
-
extend ActiveSupport::Concern
|
9
|
-
|
10
|
-
included do
|
11
|
-
before_validation :add_signature
|
12
|
-
validate :prevent_duplicate
|
13
|
-
end
|
14
|
-
|
15
|
-
private
|
16
|
-
|
17
|
-
def add_signature
|
18
|
-
# If signature fails, id will keep everything working (though deduplication will not work)
|
19
|
-
self.signature = generate_signature || generate_signature_random
|
20
|
-
self.args = get_args
|
21
|
-
end
|
22
|
-
|
23
|
-
def generate_signature
|
24
|
-
begin
|
25
|
-
if payload_object.is_a?(Delayed::PerformableMethod)
|
26
|
-
generate_signature_for_performable_method
|
27
|
-
else
|
28
|
-
generate_signature_random
|
29
|
-
end
|
30
|
-
rescue
|
31
|
-
generate_signature_failed
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
# Methods tagged with handle_asynchronously
|
36
|
-
def generate_signature_for_performable_method
|
37
|
-
if payload_object.object.respond_to?(:id) and payload_object.object.id.present?
|
38
|
-
sig = "#{payload_object.object.class}:#{payload_object.object.id}"
|
39
|
-
else
|
40
|
-
sig = "#{payload_object.object}"
|
41
|
-
end
|
42
|
-
sig += "##{payload_object.method_name}"
|
43
|
-
sig
|
44
|
-
end
|
45
|
-
|
46
|
-
# # Regular Job
|
47
|
-
# def generate_signature_for_job_wrapper
|
48
|
-
# sig = "#{payload_object.job_data["job_class"]}"
|
49
|
-
# payload_object.job_data["arguments"].each do |job_arg|
|
50
|
-
# string_job_arg = job_arg.is_a?(String) ? job_arg : job_arg.to_json
|
51
|
-
# end
|
52
|
-
# sig += "#{payload_object.job_data["job_class"]}"
|
53
|
-
# sig
|
54
|
-
# end
|
55
|
-
|
56
|
-
def generate_signature_random
|
57
|
-
SecureRandom.uuid
|
58
|
-
end
|
59
|
-
|
60
|
-
def generate_signature_failed
|
61
|
-
puts "DelayedDuplicatePreventionPlugin could not generate the signature correctly."
|
62
|
-
end
|
63
|
-
|
64
|
-
def get_args
|
65
|
-
self.payload_object.respond_to?(:args) ? self.payload_object.args : []
|
66
|
-
end
|
67
|
-
|
68
|
-
def prevent_duplicate
|
69
|
-
if DuplicateChecker.duplicate?(self)
|
70
|
-
Rails.logger.warn "Found duplicate job(#{self.signature}), ignoring..."
|
71
|
-
errors.add(:base, "This is a duplicate")
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
class DuplicateChecker
|
77
|
-
attr_reader :job
|
78
|
-
|
79
|
-
def self.duplicate?(job)
|
80
|
-
new(job).duplicate?
|
81
|
-
end
|
82
|
-
|
83
|
-
def initialize(job)
|
84
|
-
@job = job
|
85
|
-
end
|
86
|
-
|
87
|
-
def duplicate?
|
88
|
-
possible_dupes.any? { |possible_dupe| args_match?(possible_dupe, job) }
|
89
|
-
end
|
90
|
-
|
91
|
-
private
|
92
|
-
|
93
|
-
def possible_dupes
|
94
|
-
possible_dupes = Delayed::Job.where(attempts: 0, locked_at: nil) # Only jobs not started, otherwise it would never compute a real change if the job is currently running
|
95
|
-
.where(signature: job.signature) # Same signature
|
96
|
-
possible_dupes = possible_dupes.where.not(id: job.id) if job.id.present?
|
97
|
-
possible_dupes
|
98
|
-
end
|
99
|
-
|
100
|
-
def args_match?(job1, job2)
|
101
|
-
job1.payload_object.args == job2.payload_object.args
|
102
|
-
rescue
|
103
|
-
false
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
@@ -1,29 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rails'
|
4
|
-
|
5
|
-
module DelayedJobPreventDuplicate
|
6
|
-
class DelayedJobPreventDuplicateGenerator < ::Rails::Generators::Base
|
7
|
-
|
8
|
-
include Rails::Generators::Migration
|
9
|
-
source_root File.expand_path("../templates", __FILE__)
|
10
|
-
|
11
|
-
def self.next_migration_number(path)
|
12
|
-
unless @prev_migration_nr
|
13
|
-
@prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
|
14
|
-
else
|
15
|
-
@prev_migration_nr += 1
|
16
|
-
end
|
17
|
-
@prev_migration_nr.to_s
|
18
|
-
end
|
19
|
-
|
20
|
-
def copy_migration
|
21
|
-
migration_template "migration.rb", "db/migrate/add_signature_to_delayed_job.rb", migration_version: migration_version
|
22
|
-
end
|
23
|
-
|
24
|
-
def migration_version
|
25
|
-
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
26
|
-
end
|
27
|
-
|
28
|
-
end
|
29
|
-
end
|