resque_sliding_window 0.0.1
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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +70 -0
- data/Rakefile +1 -0
- data/lib/resque/plugins/sliding_window.rb +13 -0
- data/lib/resque_sliding_window.rb +4 -0
- data/lib/resque_sliding_window/resque_extensions.rb +35 -0
- data/lib/resque_sliding_window/version.rb +3 -0
- data/lib/resque_sliding_window/window_slider.rb +119 -0
- data/resque_sliding_window.gemspec +25 -0
- metadata +111 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Jon Phenow
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# ResqueSlidingWindow
|
2
|
+
|
3
|
+
Want a sliding window for debouncing bursty Resque events? Here it is.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'resque_sliding_window'
|
11
|
+
gem 'resque-scheduler', require: 'resque_scheduler', git: "https://github.com/jonhyman/resque-scheduler.git" # prefered repo
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
```bash
|
17
|
+
$ bundle
|
18
|
+
```
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
module Jobs
|
24
|
+
class Publish
|
25
|
+
extend Resque::Plugins::SlidingWindow
|
26
|
+
|
27
|
+
@queue = :low
|
28
|
+
|
29
|
+
def self.perform(id)
|
30
|
+
# Crazy stuff
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
Resque.enqueue_in(30, Jobs::Publish, id)
|
36
|
+
```
|
37
|
+
|
38
|
+
This will put a job in a delayed queue for 30 seconds. If a similar job is entered (same args, same klass),
|
39
|
+
it will be deleted in favor of the new 30-second job. However, it can only be
|
40
|
+
pushed off for a 1m 30s from its initial queue-time before it force runs one. When it forces
|
41
|
+
one through, others will not be run.
|
42
|
+
|
43
|
+
Default `max_time` is 1m and 30s. To change this:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
module Jobs
|
47
|
+
class Publish
|
48
|
+
extend Resque::Plugins::SlidingWindow
|
49
|
+
|
50
|
+
@queue = :low
|
51
|
+
|
52
|
+
def self.max_time
|
53
|
+
30.minutes
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.perform(id)
|
57
|
+
# Crazy stuff
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
|
64
|
+
## Contributing
|
65
|
+
|
66
|
+
1. Fork it
|
67
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
68
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
69
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
70
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Resque
|
2
|
+
module Plugins
|
3
|
+
module SlidingWindow
|
4
|
+
def before_schedule_sliding_window(*args)
|
5
|
+
ResqueSlidingWindow::WindowSlider.new(self, args).slide
|
6
|
+
end
|
7
|
+
|
8
|
+
def after_perform_sliding_window(*args)
|
9
|
+
ResqueSlidingWindow::WindowSlider.new(self, args).close
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'resque'
|
2
|
+
require 'resque_scheduler'
|
3
|
+
require 'resque/scheduler'
|
4
|
+
module ResqueSlidingWindow
|
5
|
+
module SchedulerPatches
|
6
|
+
def self.extended(base)
|
7
|
+
class << base
|
8
|
+
alias_method :delayed_timestamp_peek_without_rescue, :delayed_timestamp_peek
|
9
|
+
alias_method :delayed_timestamp_peek, :delayed_timestamp_peek_with_rescue
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def next_item_for_timestamp(timestamp)
|
14
|
+
key = "delayed:#{timestamp.to_i}"
|
15
|
+
|
16
|
+
item = patched_decode redis.lpop(key)
|
17
|
+
|
18
|
+
# If the list is empty, remove it.
|
19
|
+
clean_up_timestamp(key, timestamp)
|
20
|
+
item
|
21
|
+
end
|
22
|
+
|
23
|
+
def delayed_timestamp_peek_with_rescue(timestamp, start, count)
|
24
|
+
delayed_timestamp_peek_without_rescue timestamp, start, count
|
25
|
+
rescue Resque::Helpers::DecodeException => e
|
26
|
+
[]
|
27
|
+
end
|
28
|
+
|
29
|
+
def patched_decode(payload)
|
30
|
+
decode payload
|
31
|
+
rescue Exception => e
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
Resque.extend ResqueSlidingWindow::SchedulerPatches
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module ResqueSlidingWindow
|
2
|
+
class WindowSlider
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
attr_accessor :klass
|
6
|
+
attr_accessor :args
|
7
|
+
|
8
|
+
def_delegators :Resque, :job_to_hash, :encode, :redis, :remove_delayed_job_from_timestamp
|
9
|
+
|
10
|
+
def initialize(klass, args)
|
11
|
+
self.klass = klass
|
12
|
+
self.args = args
|
13
|
+
end
|
14
|
+
|
15
|
+
def slide
|
16
|
+
if first?
|
17
|
+
mark_first
|
18
|
+
elsif should_requeue?
|
19
|
+
requeue
|
20
|
+
else
|
21
|
+
false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def close
|
26
|
+
redis.del(slide_key)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def first?
|
32
|
+
!on_queue?
|
33
|
+
end
|
34
|
+
|
35
|
+
def mark_first
|
36
|
+
mark_time
|
37
|
+
end
|
38
|
+
|
39
|
+
def requeue
|
40
|
+
matching_delayed.each do |key, _|
|
41
|
+
remove_delayed_job_from_timestamp(key_to_timestamp(key), klass, *args)
|
42
|
+
end
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def should_requeue?
|
47
|
+
!maxed_out?
|
48
|
+
end
|
49
|
+
|
50
|
+
def matching_delayed
|
51
|
+
delayed_key_values.select { |_, value| value == search }
|
52
|
+
end
|
53
|
+
|
54
|
+
def search
|
55
|
+
encode(job_to_hash klass, args)
|
56
|
+
end
|
57
|
+
|
58
|
+
def on_queue?
|
59
|
+
matching_delayed.count > 0
|
60
|
+
end
|
61
|
+
|
62
|
+
def max_time
|
63
|
+
klass.respond_to?(:max_time) ? klass.public_send(:max_time) : 1.minute
|
64
|
+
end
|
65
|
+
|
66
|
+
def maxed_out?
|
67
|
+
(redis.get(slide_key).to_i + max_time) <= Time.now.utc.to_i
|
68
|
+
end
|
69
|
+
|
70
|
+
def slide_key
|
71
|
+
"sliding:#{klass.name}:#{keyify(args)}:count"
|
72
|
+
end
|
73
|
+
|
74
|
+
def mark_time
|
75
|
+
redis.set(slide_key, Time.now.utc.to_i)
|
76
|
+
end
|
77
|
+
|
78
|
+
def keyify(args)
|
79
|
+
"(#{args.map { |arg|
|
80
|
+
if arg.is_a?(Array)
|
81
|
+
keyify(arg)
|
82
|
+
elsif arg.respond_to?(:to_a) && !(arg.is_a?(String) || arg.is_a?(Symbol) || arg.is_a?(Numeric))
|
83
|
+
keyify(arg.to_a)
|
84
|
+
else
|
85
|
+
if arg.respond_to?(:to_key)
|
86
|
+
arg.to_key
|
87
|
+
else
|
88
|
+
arg.to_s
|
89
|
+
end
|
90
|
+
end
|
91
|
+
}.join(",").gsub(/\s/,"")})"
|
92
|
+
end
|
93
|
+
|
94
|
+
def key_to_timestamp(key)
|
95
|
+
key.to_s.gsub(/delayed:/, "").to_i
|
96
|
+
end
|
97
|
+
|
98
|
+
def timestamp_to_key(timestamp)
|
99
|
+
"delayed:#{timestamp}"
|
100
|
+
end
|
101
|
+
|
102
|
+
def delayed_timestamps
|
103
|
+
redis.zrange(:delayed_queue_schedule, 0,-1)
|
104
|
+
end
|
105
|
+
|
106
|
+
def get(key)
|
107
|
+
redis.rpop(key).tap { |val|
|
108
|
+
redis.rpush(key, val)
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
def delayed_key_values
|
113
|
+
delayed_timestamps.inject({}) do |hash, timestamp|
|
114
|
+
hash[timestamp_to_key(timestamp)] = get(timestamp_to_key(timestamp))
|
115
|
+
hash
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'resque_sliding_window/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "resque_sliding_window"
|
8
|
+
spec.version = ResqueSlidingWindow::VERSION
|
9
|
+
spec.authors = ["Jon Phenow"]
|
10
|
+
spec.email = ["j.phenow@gmail.com"]
|
11
|
+
spec.description = %q{Sliding Window unique-job workflow for Resque jobs}
|
12
|
+
spec.summary = %q{Sliding Window unique-job workflow for Resque jobs}
|
13
|
+
spec.homepage = "https://github.com/sportngin/resque_sliding_window"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
|
24
|
+
spec.add_dependency 'resque', '~> 1.21'
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: resque_sliding_window
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jon Phenow
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-06-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
type: :development
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ~>
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '1.3'
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '1.3'
|
29
|
+
prerelease: false
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
type: :development
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
prerelease: false
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: resque
|
48
|
+
type: :runtime
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.21'
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ~>
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '1.21'
|
61
|
+
prerelease: false
|
62
|
+
description: Sliding Window unique-job workflow for Resque jobs
|
63
|
+
email:
|
64
|
+
- j.phenow@gmail.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- Gemfile
|
71
|
+
- LICENSE.txt
|
72
|
+
- README.md
|
73
|
+
- Rakefile
|
74
|
+
- lib/resque/plugins/sliding_window.rb
|
75
|
+
- lib/resque_sliding_window.rb
|
76
|
+
- lib/resque_sliding_window/resque_extensions.rb
|
77
|
+
- lib/resque_sliding_window/version.rb
|
78
|
+
- lib/resque_sliding_window/window_slider.rb
|
79
|
+
- resque_sliding_window.gemspec
|
80
|
+
homepage: https://github.com/sportngin/resque_sliding_window
|
81
|
+
licenses:
|
82
|
+
- MIT
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options: []
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
segments:
|
93
|
+
- 0
|
94
|
+
version: '0'
|
95
|
+
hash: -1390516187519382713
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
segments:
|
102
|
+
- 0
|
103
|
+
version: '0'
|
104
|
+
hash: -1390516187519382713
|
105
|
+
requirements: []
|
106
|
+
rubyforge_project:
|
107
|
+
rubygems_version: 1.8.24
|
108
|
+
signing_key:
|
109
|
+
specification_version: 3
|
110
|
+
summary: Sliding Window unique-job workflow for Resque jobs
|
111
|
+
test_files: []
|