placate 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1c781f759fd2282b4cbbf32039ef53e5eed2f1959bf46d670b13cc57139e7a91
4
+ data.tar.gz: e96b43f21704a5ae648a1acc2a3dfd8530f5669c301cfdf872d44d8bbbc73510
5
+ SHA512:
6
+ metadata.gz: 7d3005d9ff5ba7a53b501706066bb5d7657fa72e5e40b6846dab2e1b8282dcf973a5a932d4d1c932f02a2d2409851060b56c22d492b759d0915651dea7a60acf
7
+ data.tar.gz: e2be1bab70de91dd7e595c64c50367e3a2985551d69f2f587af4e0d3183c03c9ada2a3b99562497f2396f69609ee08d9e7f8922fcb7820f7663aa500dd2e6f7d
data/.rubocop.yml ADDED
@@ -0,0 +1,17 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.3
3
+
4
+ Style/StringLiterals:
5
+ Enabled: true
6
+ EnforcedStyle: double_quotes
7
+
8
+ Style/StringLiteralsInInterpolation:
9
+ Enabled: true
10
+ EnforcedStyle: double_quotes
11
+
12
+ Layout/LineLength:
13
+ Max: 120
14
+
15
+ Style/Documentation:
16
+ Description: Document classes and non-namespace modules.
17
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-11-28
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 TODO: Write your name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # Placate
2
+
3
+ Placate is a simple Redis-based solution for preventing duplicate job execution in Ruby background job processors. It's designed to work with ActiveJob and any Redis-backed job system.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'placate'
11
+ ```
12
+
13
+ And then execute:
14
+ ```bash
15
+ bundle install
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### Basic Setup
21
+
22
+ First, configure Placate with your Redis connection:
23
+
24
+ ```ruby
25
+ # config/initializers/placate.rb
26
+ Placate.configure do |config|
27
+ config.redis = Redis.new
28
+ config.default_lock_ttl = 30 # Default time-to-live in seconds
29
+ end
30
+ ```
31
+
32
+ ### In Your Jobs
33
+
34
+ Include the UniqueJob module in any job you want to prevent duplicates:
35
+
36
+ ```ruby
37
+ class ProcessOrderJob < ApplicationJob
38
+ include Placate::UniqueJob
39
+
40
+ def perform(order_id)
41
+ # Your job code here
42
+ end
43
+ end
44
+ ```
45
+
46
+ Now if you try to enqueue the same job with the same arguments within the TTL window, the duplicate will be prevented:
47
+
48
+ ```ruby
49
+ ProcessOrderJob.perform_later(order_id: 123) # First job enqueued
50
+ ProcessOrderJob.perform_later(order_id: 123) # Blocked as duplicate
51
+ ProcessOrderJob.perform_later(order_id: 456) # Different args, will be enqueued
52
+ ```
53
+
54
+ ### Customization
55
+
56
+ You can customize the TTL per job class:
57
+
58
+ ```ruby
59
+ class LongRunningJob < ApplicationJob
60
+ include Placate::UniqueJob
61
+
62
+ self.unique_lock_ttl = 120 # 2 minutes
63
+
64
+ def perform
65
+ # Your job code here
66
+ end
67
+ end
68
+ ```
69
+
70
+ Or use a custom key prefix:
71
+
72
+ ```ruby
73
+ class CustomPrefixJob < ApplicationJob
74
+ include Placate::UniqueJob
75
+
76
+ self.unique_lock_prefix = 'my_app_jobs'
77
+
78
+ def perform
79
+ # Your job code here
80
+ end
81
+ end
82
+ ```
83
+
84
+ ## How It Works
85
+
86
+ Placate uses Redis to maintain a lock based on the job class name and arguments. When a job is enqueued (`before_enqueue`):
87
+
88
+ 1. A unique key is generated based on the job class and arguments
89
+ 2. The current timestamp is stored in Redis with this key
90
+ 3. If a key exists and its timestamp is within the TTL window, the job is not enqueued
91
+ 4. If no key exists or the existing timestamp is older than the TTL, the job is enqueued
92
+
93
+ After the job is performed (`after_perform`):
94
+
95
+ 1. The unique key is deleted, to enable the next job.
96
+
97
+ ## Requirements
98
+
99
+ - Ruby 3.3+
100
+ - Rails 7.0+
101
+ - Redis
102
+
103
+ ## Development
104
+
105
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
106
+
107
+ ## Contributing
108
+
109
+ Bug reports and pull requests are welcome on GitHub. This project is intended to be a safe, welcoming space for collaboration.
110
+
111
+ ## License
112
+
113
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Placate
4
+ class Configuration
5
+ attr_accessor :redis, :default_lock_ttl
6
+
7
+ def initialize
8
+ @default_lock_ttl = 60 # 1 minute default
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Placate
6
+ module UniqueJob
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def unique_lock_ttl
11
+ @unique_lock_ttl || Placate.configuration.default_lock_ttl
12
+ end
13
+
14
+ def unique_lock_ttl=(seconds)
15
+ @unique_lock_ttl = seconds
16
+ end
17
+ end
18
+
19
+ included do
20
+ class_attribute :unique_lock_prefix, instance_writer: false, default: nil
21
+
22
+ before_enqueue do |job|
23
+ args_hash = Digest::MD5.hexdigest(job.arguments.to_s)
24
+ prefix = job.class.unique_lock_prefix || job.class.name
25
+ lock_key = "#{prefix}:#{args_hash}:lock"
26
+
27
+ current_time = Time.now.to_i
28
+ existing_lock = Placate.redis.get(lock_key)
29
+
30
+ if existing_lock
31
+ lock_time = existing_lock.to_i
32
+ throw :abort if current_time - lock_time < (job.class.unique_lock_ttl || 30)
33
+ end
34
+
35
+ Placate.redis.set(lock_key, current_time)
36
+ end
37
+
38
+ after_perform do |job|
39
+ args_hash = Digest::MD5.hexdigest(job.arguments.to_s)
40
+ prefix = job.class.unique_lock_prefix || job.class.name
41
+ lock_key = "#{prefix}:#{args_hash}:lock"
42
+
43
+ Placate.redis.del(lock_key) if lock_key
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Placate
4
+ VERSION = "0.1.0"
5
+ end
data/lib/placate.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+
5
+ require_relative "placate/version"
6
+ require_relative "placate/configuration"
7
+ require_relative "placate/unique_job"
8
+
9
+ module Placate
10
+ class Error < StandardError; end
11
+ # Your code goes here...
12
+
13
+ class << self
14
+ def configure
15
+ yield(configuration)
16
+ end
17
+
18
+ def configuration
19
+ @configuration ||= Configuration.new
20
+ end
21
+
22
+ def redis
23
+ configuration.redis
24
+ end
25
+ end
26
+ end
data/sig/placate.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Placate
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: placate
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Toby
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-11-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activejob
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.16'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.16'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.21'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.21'
69
+ description: Prevents duplicate job execution using Redis locks for any Redis-based
70
+ job processing system
71
+ email:
72
+ - toby@darkroom.tech
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".rubocop.yml"
78
+ - CHANGELOG.md
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - lib/placate.rb
83
+ - lib/placate/configuration.rb
84
+ - lib/placate/unique_job.rb
85
+ - lib/placate/version.rb
86
+ - sig/placate.rbs
87
+ homepage: https://github.com/tobyond/placate
88
+ licenses:
89
+ - MIT
90
+ metadata:
91
+ homepage_uri: https://github.com/tobyond/placate
92
+ source_code_uri: https://github.com/tobyond/placate
93
+ changelog_uri: https://github.com/tobyond/placate/blob/main/CHANGELOG.md
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 3.3.0
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubygems_version: 3.5.3
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Unique job handling for Redis-based job processors
113
+ test_files: []