activejob-lockable 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +1 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +104 -0
- data/Rakefile +6 -0
- data/activejob-lockable.gemspec +31 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/activejob/lockable.rb +40 -0
- data/lib/activejob/lockable/lockable.rb +59 -0
- data/lib/activejob/lockable/redis_store.rb +19 -0
- data/lib/activejob/lockable/version.rb +5 -0
- metadata +156 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1b13ca8541fb112296178a39c09d9ef7fa8e3146
|
4
|
+
data.tar.gz: 0fd8547b9e050bdaf2dea768ff3e4ed29920537e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 52ff57a0d672cb58afb292fd22c4e482396797e7b241d88e908290be7535d4217e1a0e22e8616ff900060e8215af20656b2a0c157b369d83a6348a498a5ad855
|
7
|
+
data.tar.gz: 3237db29230609de7750f29926f40535722b51a38ffa6684fb247b163985a4ffb66aed0e4bebb9b6ebc74c43c761b839216ecf164c8613f7319250e05db02871
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Dmytro Zakharov
|
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,104 @@
|
|
1
|
+
# ActiveJob::Lockable
|
2
|
+
|
3
|
+
Gem to make to make jobs lockable. Useful when a job is called N times, but only a single execution is needed.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'activejob-lockable'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install activejob-lockable
|
20
|
+
|
21
|
+
## Configuration
|
22
|
+
|
23
|
+
Create an initializer with redis connection:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
ActiveJob::Lockable.redis = Redis.current # if you have a redis instance
|
27
|
+
|
28
|
+
# or
|
29
|
+
ActiveJob::Lockable.redis = 'redis://localhost:6379/0'
|
30
|
+
# or
|
31
|
+
ActiveJob::Lockable.redis = {
|
32
|
+
host: '10.0.1.1',
|
33
|
+
port: 6380,
|
34
|
+
db: 15
|
35
|
+
}
|
36
|
+
```
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
# job
|
42
|
+
# nothing to change!
|
43
|
+
```
|
44
|
+
```ruby
|
45
|
+
# code
|
46
|
+
MyJob.set(lock: 10.seconds).perform_later(id)
|
47
|
+
```
|
48
|
+
|
49
|
+
Now, after the first enqueue (perform_later), a lock will be created and the following enqueues within 10 seconds will be rejected.
|
50
|
+
|
51
|
+
### Lock key
|
52
|
+
|
53
|
+
A lock key by default:
|
54
|
+
|
55
|
+
`job_name_in_downcase:md5(arguments)`
|
56
|
+
|
57
|
+
To override the key you can override method `lock_key`:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
# job
|
61
|
+
def lock_key
|
62
|
+
'my-custom-key'
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
### Lock period
|
67
|
+
|
68
|
+
You can set a fixed `lock_period`, in that case `.set(lock: N)` will be ignored and the job will always be lockable:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
# job
|
72
|
+
def lock_period
|
73
|
+
1.day
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
### On lock action
|
78
|
+
|
79
|
+
When a job is locked and another one is enqueued, you can set up a custom callback that will be called. This is useful if you want to raise exception or be notified:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
# job
|
83
|
+
class MyJob < ApplicationJob
|
84
|
+
on_locked :raise_if_locked
|
85
|
+
|
86
|
+
def raise_if_locked
|
87
|
+
raise 'Job is locked'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
## Dependencies
|
93
|
+
|
94
|
+
* [ActiveSupport](https://github.com/rails/rails/tree/master/activesupport)
|
95
|
+
* [ActibeJob](https://github.com/rails/rails/tree/master/activejob)
|
96
|
+
* [Redis](https://redis.io/)
|
97
|
+
|
98
|
+
## Contributing
|
99
|
+
|
100
|
+
Bug reports and pull requests are welcome on GitHub at [qonto/activejob-lockable](https://github.com/qonto/activejob-lockable). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
101
|
+
|
102
|
+
## License
|
103
|
+
|
104
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'activejob/lockable/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'activejob-lockable'
|
8
|
+
spec.version = ActiveJob::Lockable::VERSION
|
9
|
+
spec.authors = ['Dmytro Zakharov']
|
10
|
+
spec.email = ['dmytro@qonto.eu']
|
11
|
+
|
12
|
+
spec.summary = %q{Prevents jobs from enqueuing with unique arguments for a certain period of time}
|
13
|
+
spec.homepage = 'https://github.com/qonto/activejob-lockable'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.bindir = 'exe'
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.add_dependency 'activejob'
|
24
|
+
spec.add_dependency 'activesupport'
|
25
|
+
|
26
|
+
spec.add_development_dependency 'bundler', '~> 1.15'
|
27
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
28
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
29
|
+
spec.add_development_dependency 'fakeredis'
|
30
|
+
spec.add_development_dependency 'pry'
|
31
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "activejob/lockable"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'activejob/lockable/version'
|
2
|
+
require 'active_support/lazy_load_hooks'
|
3
|
+
|
4
|
+
ActiveSupport.on_load :active_job do
|
5
|
+
require 'activejob/lockable/lockable'
|
6
|
+
ActiveJob::Base::ClassMethods.send(:include, ActiveJob::Lockable::ClassMethods)
|
7
|
+
ActiveJob::Base.send(:include, ActiveJob::Lockable)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ActiveJob
|
11
|
+
module Lockable
|
12
|
+
extend self
|
13
|
+
|
14
|
+
# Accepts:
|
15
|
+
# 1. A redis URL (valid for `Redis.new(url: url)`)
|
16
|
+
# 2. an options hash compatible with `Redis.new`
|
17
|
+
# 3. or a valid Redis instance (one that responds to `#smembers`). Likely,
|
18
|
+
# this will be an instance of either `Redis`, `Redis::Client`,
|
19
|
+
# `Redis::DistRedis`, or `Redis::Namespace`.
|
20
|
+
def redis=(server)
|
21
|
+
@redis = if server.is_a?(String)
|
22
|
+
Redis.new(:url => server, :thread_safe => true)
|
23
|
+
elsif server.is_a?(Hash)
|
24
|
+
Redis.new(server.merge(:thread_safe => true))
|
25
|
+
elsif server.respond_to?(:smembers)
|
26
|
+
server
|
27
|
+
else
|
28
|
+
raise ArgumentError,
|
29
|
+
'You must supply a url, options hash or valid Redis connection instance'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns the current Redis connection. If none has been created, will
|
34
|
+
# create a new one.
|
35
|
+
def redis
|
36
|
+
return @redis if @redis
|
37
|
+
raise 'Redis is not configured'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'activejob/lockable/redis_store'
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module Lockable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def on_locked(action)
|
9
|
+
self.on_locked_action = action
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
included do
|
14
|
+
attr_reader :options
|
15
|
+
class_attribute :on_locked_action
|
16
|
+
|
17
|
+
def enqueue(options = {})
|
18
|
+
@options = options
|
19
|
+
if locked?
|
20
|
+
logger.info "job is locked, expires in #{locked_ttl} second"
|
21
|
+
send(on_locked_action) if on_locked_action && respond_to?(on_locked_action)
|
22
|
+
else
|
23
|
+
lock!
|
24
|
+
super(options)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def lock!
|
29
|
+
return if lock_period.to_i <= 0
|
30
|
+
logger.info "locked with #{lock_key} for #{lock_period} seconds. Job_id: #{self.job_id} class_name: #{self.class}"
|
31
|
+
begin
|
32
|
+
ActiveJob::Lockable::RedisStore.setex(lock_key, lock_period, self.job_id)
|
33
|
+
rescue => e
|
34
|
+
logger.info "EXCEPTION: locked with #{lock_key} for #{lock_period} seconds. Job_id: #{self.job_id} class_name: #{self.class}"
|
35
|
+
raise e
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def lock_key
|
40
|
+
md5 = Digest::MD5.hexdigest(self.arguments.join)
|
41
|
+
"#{self.class.name.downcase}:#{md5}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def locked?
|
45
|
+
ActiveJob::Lockable::RedisStore.exists?(lock_key)
|
46
|
+
end
|
47
|
+
|
48
|
+
def locked_ttl
|
49
|
+
ActiveJob::Lockable::RedisStore.ttl(lock_key)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def lock_period
|
55
|
+
options[:lock]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ActiveJob
|
2
|
+
module Lockable
|
3
|
+
class RedisStore
|
4
|
+
class << self
|
5
|
+
def setex(cache_key, expiration, cache_value)
|
6
|
+
ActiveJob::Lockable.redis.setex(cache_key, expiration, cache_value)
|
7
|
+
end
|
8
|
+
|
9
|
+
def exists?(cache_key)
|
10
|
+
ActiveJob::Lockable.redis.exists(cache_key)
|
11
|
+
end
|
12
|
+
|
13
|
+
def ttl(cache_key)
|
14
|
+
ActiveJob::Lockable.redis.ttl(cache_key)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activejob-lockable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dmytro Zakharov
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-11-13 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: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.15'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.15'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: fakeredis
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pry
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description:
|
112
|
+
email:
|
113
|
+
- dmytro@qonto.eu
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- ".rspec"
|
120
|
+
- Gemfile
|
121
|
+
- LICENSE.txt
|
122
|
+
- README.md
|
123
|
+
- Rakefile
|
124
|
+
- activejob-lockable.gemspec
|
125
|
+
- bin/console
|
126
|
+
- bin/setup
|
127
|
+
- lib/activejob/lockable.rb
|
128
|
+
- lib/activejob/lockable/lockable.rb
|
129
|
+
- lib/activejob/lockable/redis_store.rb
|
130
|
+
- lib/activejob/lockable/version.rb
|
131
|
+
homepage: https://github.com/qonto/activejob-lockable
|
132
|
+
licenses:
|
133
|
+
- MIT
|
134
|
+
metadata: {}
|
135
|
+
post_install_message:
|
136
|
+
rdoc_options: []
|
137
|
+
require_paths:
|
138
|
+
- lib
|
139
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - ">="
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '0'
|
144
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
145
|
+
requirements:
|
146
|
+
- - ">="
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
version: '0'
|
149
|
+
requirements: []
|
150
|
+
rubyforge_project:
|
151
|
+
rubygems_version: 2.6.13
|
152
|
+
signing_key:
|
153
|
+
specification_version: 4
|
154
|
+
summary: Prevents jobs from enqueuing with unique arguments for a certain period of
|
155
|
+
time
|
156
|
+
test_files: []
|