jober 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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/Rakefile +2 -0
- data/bin/jober +10 -0
- data/bin/manager +10 -0
- data/examples/classes.rb +72 -0
- data/examples/man.rb +7 -0
- data/jober.gemspec +26 -0
- data/lib/jober.rb +91 -0
- data/lib/jober/abstract_task.rb +46 -0
- data/lib/jober/logger.rb +18 -0
- data/lib/jober/manager.rb +136 -0
- data/lib/jober/queue.rb +47 -0
- data/lib/jober/queue_batch.rb +47 -0
- data/lib/jober/shared_object.rb +48 -0
- data/lib/jober/task.rb +20 -0
- data/lib/jober/unique_queue.rb +16 -0
- data/lib/jober/version.rb +3 -0
- data/spec/chain_spec.rb +18 -0
- data/spec/integration_spec.rb +37 -0
- data/spec/kill_task_spec.rb +39 -0
- data/spec/pids_spec.rb +39 -0
- data/spec/queue_batch_spec.rb +28 -0
- data/spec/queue_parallel_spec.rb +24 -0
- data/spec/queue_spec.rb +45 -0
- data/spec/shared_object_spec.rb +25 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/stats_spec.rb +33 -0
- data/spec/task_spec.rb +44 -0
- data/spec/unique_queue_spec.rb +34 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 658e5f757cdbd193692c730c37853625a6ece891
|
4
|
+
data.tar.gz: 949f6f7ee9ede001aa2ebb14693ee4e537b94737
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4c5152af3de453ca2f1dd7d0be59f44c52bff556b23dee3204140f18708133223da49f744f2f83acde333ee113c18f99190c65649829a3deee00a5c624a865b6
|
7
|
+
data.tar.gz: 47fa1ca5099ad4294d673cd2ce54b7347432ca75a3ed5e15e9c968b9208e093b5250ebefd0f1b2a80ce949b89b0261094d06a27ad32a31bf51731539b7797472
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 'Konstantin Makarchev'
|
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/Rakefile
ADDED
data/bin/jober
ADDED
data/bin/manager
ADDED
data/examples/classes.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
Bundler.require :default
|
3
|
+
|
4
|
+
class A < Jober::Task
|
5
|
+
every 10
|
6
|
+
def perform
|
7
|
+
10.times do |i|
|
8
|
+
info "enqueue to b #{i}"
|
9
|
+
B.enqueue(i)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class B < Jober::Queue
|
15
|
+
every 10
|
16
|
+
def perform(x)
|
17
|
+
10.times do |i|
|
18
|
+
info "enqueue to c #{x} #{i}"
|
19
|
+
C.enqueue(x, i)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class C < Jober::Queue
|
25
|
+
every 10
|
26
|
+
def perform(x, i)
|
27
|
+
10.times do |j|
|
28
|
+
info "enqueue to d #{x} #{i} #{j}"
|
29
|
+
D.enqueue(x, i, j)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class D < Jober::QueueBatch
|
35
|
+
every 10
|
36
|
+
batch_size 200
|
37
|
+
|
38
|
+
def perform(*batch)
|
39
|
+
info "got batch: #{batch.inspect}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class E < Jober::Task
|
44
|
+
5.times { every 5 }
|
45
|
+
|
46
|
+
def perform
|
47
|
+
puts "start e :)"
|
48
|
+
sleep 3
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class G < Jober::Task
|
53
|
+
every 10
|
54
|
+
|
55
|
+
def perform
|
56
|
+
"asdfsdf" + 1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class F < Jober::Task
|
61
|
+
every 5
|
62
|
+
def perform
|
63
|
+
sleep 100
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
if $0 == __FILE__
|
68
|
+
require 'irb'
|
69
|
+
require 'irb/completion'
|
70
|
+
|
71
|
+
IRB.start
|
72
|
+
end
|
data/examples/man.rb
ADDED
data/jober.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'jober/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "jober"
|
8
|
+
spec.version = Jober::VERSION
|
9
|
+
spec.authors = ["'Konstantin Makarchev'"]
|
10
|
+
spec.email = ["'kostya27@gmail.com'"]
|
11
|
+
spec.summary = %q{Simple background jobs, queues.}
|
12
|
+
spec.description = %q{Simple background jobs, queues.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
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_dependency 'redis'
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "rspec"
|
26
|
+
end
|
data/lib/jober.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require "Jober/version"
|
2
|
+
|
3
|
+
require 'redis'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
module Jober
|
7
|
+
autoload :Manager, 'Jober/manager'
|
8
|
+
autoload :AbstractTask, 'Jober/abstract_task'
|
9
|
+
autoload :Task, 'Jober/task'
|
10
|
+
autoload :Queue, 'Jober/queue'
|
11
|
+
autoload :QueueBatch, 'Jober/queue_batch'
|
12
|
+
autoload :UniqueQueue, 'Jober/unique_queue'
|
13
|
+
autoload :Logger, 'Jober/logger'
|
14
|
+
autoload :SharedObject, 'Jober/shared_object'
|
15
|
+
|
16
|
+
class << self
|
17
|
+
attr_accessor :redis_config
|
18
|
+
attr_reader :classes
|
19
|
+
|
20
|
+
def logger
|
21
|
+
::Logger.new(STDOUT)
|
22
|
+
end
|
23
|
+
|
24
|
+
def redis
|
25
|
+
Thread.current[:__redis__] ||= Redis.new(redis_config || {})
|
26
|
+
end
|
27
|
+
|
28
|
+
def reset_redis
|
29
|
+
Thread.current[:__redis__] = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_class(klass)
|
33
|
+
unless klass.to_s.start_with?('Jober::')
|
34
|
+
@classes ||= []
|
35
|
+
@classes << klass
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def after_fork(&block)
|
40
|
+
@after_fork = block
|
41
|
+
end
|
42
|
+
|
43
|
+
def call_after_fork
|
44
|
+
@after_fork.call if @after_fork
|
45
|
+
end
|
46
|
+
|
47
|
+
def dump(obj)
|
48
|
+
Marshal.dump(obj)
|
49
|
+
end
|
50
|
+
|
51
|
+
def load(obj)
|
52
|
+
Marshal.load(obj)
|
53
|
+
end
|
54
|
+
|
55
|
+
def exception(ex)
|
56
|
+
# redefine me
|
57
|
+
end
|
58
|
+
|
59
|
+
def underscore(str)
|
60
|
+
word = str.dup
|
61
|
+
word.gsub!('::', '/')
|
62
|
+
word.gsub!(/(?:([A-Za-z\d])|^)((?=a)b)(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" }
|
63
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
|
64
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
65
|
+
word.tr!("-", "_")
|
66
|
+
word.downcase!
|
67
|
+
word
|
68
|
+
end
|
69
|
+
|
70
|
+
def llens
|
71
|
+
h = {}
|
72
|
+
@classes.each do |klass|
|
73
|
+
next unless klass.ancestors.include?(Jober::Queue)
|
74
|
+
h[klass.short_name] = klass.len
|
75
|
+
end
|
76
|
+
h
|
77
|
+
end
|
78
|
+
|
79
|
+
def stats
|
80
|
+
h = {}
|
81
|
+
@classes.each do |klass|
|
82
|
+
start = Jober.redis.get("Jober:stats:#{klass.short_name}:start")
|
83
|
+
start = Time.at(start.to_i) if start
|
84
|
+
_end = Jober.redis.get("Jober:stats:#{klass.short_name}:end")
|
85
|
+
_end = Time.at(_end.to_i) if _end
|
86
|
+
h[klass.short_name] = {:start => start, :end => _end, :duration => (_end && start && _end >= start) ? (_end - start) : nil }
|
87
|
+
end
|
88
|
+
h
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Jober::AbstractTask
|
2
|
+
include Jober::Logger
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def every(interval, method = :perform)
|
6
|
+
workers << [interval, method]
|
7
|
+
end
|
8
|
+
|
9
|
+
def workers
|
10
|
+
@workers ||= []
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :short_name
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :stopped
|
17
|
+
|
18
|
+
def self.inherited(base)
|
19
|
+
Jober.add_class(base)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@stopped = false
|
24
|
+
trap("QUIT") { @stopped = true }
|
25
|
+
trap("INT") { @stopped = true }
|
26
|
+
end
|
27
|
+
|
28
|
+
def execute(method = :perform)
|
29
|
+
info "=> starting"
|
30
|
+
@start_at = Time.now
|
31
|
+
write_timestamp(:start)
|
32
|
+
run(method)
|
33
|
+
write_timestamp(:end)
|
34
|
+
info "<= end of #{method} in #{Time.now - @start_at}"
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def write_timestamp(type)
|
41
|
+
Jober.redis.set("Jober:stats:#{self.class.short_name}:#{type}", Time.now.to_i.to_s)
|
42
|
+
rescue Object => ex
|
43
|
+
error "#{ex.inspect} #{ex.backtrace}"
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
data/lib/jober/logger.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Jober::Logger
|
4
|
+
def logger
|
5
|
+
@logger ||= Jober.logger
|
6
|
+
end
|
7
|
+
|
8
|
+
def logger=(logger)
|
9
|
+
@logger = logger
|
10
|
+
end
|
11
|
+
|
12
|
+
Logger::Severity.constants.each do |level|
|
13
|
+
method_name = level.to_s.downcase
|
14
|
+
define_method method_name do |msg = nil, &block|
|
15
|
+
logger.send(method_name, msg, &block)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
class Jober::Manager
|
4
|
+
include Jober::Logger
|
5
|
+
|
6
|
+
attr_accessor :logger_path, :stopped
|
7
|
+
|
8
|
+
def initialize(name, allowed_classes = nil)
|
9
|
+
@name = name
|
10
|
+
@stopped = false
|
11
|
+
@mutex = Mutex.new
|
12
|
+
@pids = []
|
13
|
+
@allowed_classes = allowed_classes ? (Jober.classes & allowed_classes) : Jober.classes
|
14
|
+
|
15
|
+
$0 = "#{@name} manager"
|
16
|
+
if @logger_path
|
17
|
+
self.logger = ::Logger.new(File.join(@logger_path, "manager.log"))
|
18
|
+
end
|
19
|
+
info "starting manager #{@name}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def run!
|
23
|
+
@allowed_classes.each do |klass|
|
24
|
+
workers = klass.workers
|
25
|
+
workers = [[5 * 60, :perform]] if workers.empty?
|
26
|
+
|
27
|
+
workers.each do |interval, method|
|
28
|
+
Thread.new { start_worker(klass, interval, method) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def run
|
34
|
+
run!
|
35
|
+
|
36
|
+
trap("TERM") { stop }
|
37
|
+
|
38
|
+
loop do
|
39
|
+
sleep 1
|
40
|
+
break if @stopped
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop!
|
45
|
+
@stopped = true
|
46
|
+
@pids.each { |pid| ::Process.kill("QUIT", pid) }
|
47
|
+
info "stopping manager..."
|
48
|
+
end
|
49
|
+
|
50
|
+
def stop(timeout = 2.5)
|
51
|
+
stop!
|
52
|
+
return if @pids.empty?
|
53
|
+
|
54
|
+
sum = 0
|
55
|
+
while true
|
56
|
+
sleep(0.1)
|
57
|
+
sum += 0.1
|
58
|
+
break if sum >= timeout
|
59
|
+
break if @pids.empty?
|
60
|
+
end
|
61
|
+
|
62
|
+
return if @pids.empty?
|
63
|
+
|
64
|
+
info { "still alive pids: #{@pids}, killing" }
|
65
|
+
@pids.each { |pid| ::Process.kill("KILL", pid) }
|
66
|
+
|
67
|
+
@pids = []
|
68
|
+
end
|
69
|
+
|
70
|
+
def catch
|
71
|
+
yield
|
72
|
+
true
|
73
|
+
rescue Object => ex
|
74
|
+
p "exception #{ex.inspect} #{ex.backtrace}"
|
75
|
+
Jober.exception(ex)
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def start_worker(klass, interval, method)
|
80
|
+
debug { "start worker for #{klass.to_s} #{method}" }
|
81
|
+
loop do
|
82
|
+
pid = nil
|
83
|
+
res = catch do
|
84
|
+
pid = run_task_fork(klass, method)
|
85
|
+
add_pid(pid)
|
86
|
+
Process.wait(pid)
|
87
|
+
del_pid(pid)
|
88
|
+
sleep interval unless stopped
|
89
|
+
end
|
90
|
+
del_pid(pid)
|
91
|
+
break if stopped
|
92
|
+
sleep 0.5 unless res
|
93
|
+
break if stopped
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def run_task_fork(klass, method)
|
98
|
+
info "invoke #{klass} #{method}"
|
99
|
+
fork do
|
100
|
+
$0 = "#{@name} manager #{klass}"
|
101
|
+
#$0 += " #{index}" if index > 0
|
102
|
+
Jober.call_after_fork
|
103
|
+
Jober.reset_redis
|
104
|
+
|
105
|
+
inst = klass.new # class_name parent of Jober::Task
|
106
|
+
|
107
|
+
if @logger_path
|
108
|
+
logger_path = File.join(@logger_path, "#{klass.short_name}.log")
|
109
|
+
|
110
|
+
STDOUT.reopen(File.open(logger_path, 'a'))
|
111
|
+
STDERR.reopen(File.open(logger_path, 'a'))
|
112
|
+
inst.logger = ::Logger.new(logger_path)
|
113
|
+
end
|
114
|
+
|
115
|
+
inst.execute(method)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def pids
|
120
|
+
@mutex.synchronize { @pids }
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def add_pid(pid)
|
126
|
+
@mutex.synchronize { @pids << pid }
|
127
|
+
end
|
128
|
+
|
129
|
+
def del_pid(pid)
|
130
|
+
@mutex.synchronize { @pids -= [pid] }
|
131
|
+
end
|
132
|
+
|
133
|
+
def clear_pids
|
134
|
+
@mutex.synchronize { @pids = [] }
|
135
|
+
end
|
136
|
+
end
|
data/lib/jober/queue.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
class Jober::Queue < Jober::Task
|
2
|
+
|
3
|
+
def self.inherited(base)
|
4
|
+
super
|
5
|
+
base.set_queue_name(base.short_name)
|
6
|
+
end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :queue_name
|
10
|
+
|
11
|
+
def set_queue_name(q)
|
12
|
+
@queue_name = "Jober:queue:#{q}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def queue_name
|
17
|
+
self.class.queue_name
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.enqueue(*args)
|
21
|
+
Jober.redis.rpush(queue_name, Jober.dump(args))
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.len
|
25
|
+
Jober.redis.llen(self.queue_name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def pop
|
29
|
+
res = Jober.redis.lpop(queue_name)
|
30
|
+
Jober.load(res) if res
|
31
|
+
end
|
32
|
+
|
33
|
+
def run(method)
|
34
|
+
cnt = 0
|
35
|
+
while args = pop
|
36
|
+
send(method, *args)
|
37
|
+
cnt += 1
|
38
|
+
|
39
|
+
if stopped
|
40
|
+
break
|
41
|
+
end
|
42
|
+
|
43
|
+
info { "processed #{cnt}" } if cnt % 1000 == 0
|
44
|
+
end
|
45
|
+
info { "processed #{cnt}" }
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class Jober::QueueBatch < Jober::Queue
|
2
|
+
class << self
|
3
|
+
def batch_size(bs)
|
4
|
+
@batch_size = bs
|
5
|
+
end
|
6
|
+
|
7
|
+
def get_batch_size
|
8
|
+
@batch_size ||= 100
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute(method = :perform)
|
13
|
+
cnt = 0
|
14
|
+
batch = []
|
15
|
+
while args = pop
|
16
|
+
batch << args
|
17
|
+
if batch.length >= self.class.get_batch_size
|
18
|
+
execute_batch(method, batch)
|
19
|
+
batch = []
|
20
|
+
end
|
21
|
+
break if stopped
|
22
|
+
cnt += 1
|
23
|
+
end
|
24
|
+
if batch.length > 0
|
25
|
+
if stopped
|
26
|
+
reschedule_batch(batch)
|
27
|
+
else
|
28
|
+
execute_batch(method, batch)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def execute_batch(method, batch)
|
35
|
+
send(method, batch)
|
36
|
+
rescue Object => ex
|
37
|
+
reschedule_batch(batch)
|
38
|
+
Jober.exception(ex)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def reschedule_batch(batch)
|
44
|
+
batch.reverse_each { |ev| Jober.redis.lpush(queue_name, Jober.dump(ev)) }
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class Jober::SharedObject
|
2
|
+
class << self
|
3
|
+
def set(name, obj, conn = Jober.redis)
|
4
|
+
conn.set(key(name), Marshal.dump(obj))
|
5
|
+
end
|
6
|
+
|
7
|
+
def get(name, conn = Jober.redis)
|
8
|
+
r = conn.get(key(name))
|
9
|
+
Marshal.load(r) if r
|
10
|
+
end
|
11
|
+
|
12
|
+
def raw_get(name)
|
13
|
+
Jober.redis.get(key(name))
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](name)
|
17
|
+
get(name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def []=(name, obj)
|
21
|
+
set name, obj
|
22
|
+
end
|
23
|
+
|
24
|
+
def inc(name, by = 1)
|
25
|
+
Jober.redis.incrby(key(name), by)
|
26
|
+
end
|
27
|
+
|
28
|
+
def del(name)
|
29
|
+
Jober.redis.del key(name)
|
30
|
+
end
|
31
|
+
|
32
|
+
def clear
|
33
|
+
Jober.redis.keys(key('*')).each { |k| del(k) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def values(mask)
|
37
|
+
Jober.redis.keys(key(mask)).map { |key| get(key) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def key(name)
|
41
|
+
if name.start_with?('shared:')
|
42
|
+
name
|
43
|
+
else
|
44
|
+
"shared:#{name}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/jober/task.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
class Jober::Task < Jober::AbstractTask
|
2
|
+
|
3
|
+
def self.extract_name(name)
|
4
|
+
Jober.underscore(name).gsub(/_?queue_?/, '').gsub(/_?fk_?/, '').gsub(/_?task_?/, '').split('::').last.gsub('/', '-')
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.inherited(base)
|
8
|
+
super
|
9
|
+
base.short_name = extract_name(base.name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform
|
13
|
+
raise "implement me"
|
14
|
+
end
|
15
|
+
|
16
|
+
def run(method)
|
17
|
+
send(method)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Jober::UniqueQueue < Jober::Queue
|
2
|
+
|
3
|
+
def self.len
|
4
|
+
Jober.redis.scard(queue_name)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.enqueue(*args)
|
8
|
+
Jober.redis.sadd(queue_name, Jober.dump(args))
|
9
|
+
end
|
10
|
+
|
11
|
+
def pop
|
12
|
+
res = Jober.redis.spop(queue_name)
|
13
|
+
Jober.load(res) if res
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
data/spec/chain_spec.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
class MyChain < Jober::Task
|
4
|
+
every 3
|
5
|
+
|
6
|
+
def perform
|
7
|
+
sleep 5
|
8
|
+
SO["chain"] += 1
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "Queue" do
|
13
|
+
it "should work" do
|
14
|
+
SO["chain"] = 0
|
15
|
+
run_manager_for(7, [MyChain])
|
16
|
+
SO["chain"].should == 1
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
class A < Jober::Task
|
4
|
+
every 3000
|
5
|
+
|
6
|
+
def perform
|
7
|
+
10.times do |i|
|
8
|
+
B.enqueue(i)
|
9
|
+
C.enqueue(i)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class B < Jober::Queue
|
15
|
+
every 3
|
16
|
+
|
17
|
+
def perform(x)
|
18
|
+
SO["b"] += x
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class C < Jober::QueueBatch
|
23
|
+
every 3
|
24
|
+
|
25
|
+
def perform(batch)
|
26
|
+
SO["c"] = batch.flatten
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "integration" do
|
31
|
+
it "should work" do
|
32
|
+
SO["b"] = 0
|
33
|
+
run_manager_for(5, [A, B, C])
|
34
|
+
SO["b"].should == 45
|
35
|
+
SO["c"].should == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
class LongTask1 < Jober::Task
|
4
|
+
def perform
|
5
|
+
sleep 100
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class LongTask11 < Jober::Task
|
10
|
+
def perform
|
11
|
+
while true
|
12
|
+
sleep 1
|
13
|
+
break if stopped
|
14
|
+
end
|
15
|
+
SO["11"] = true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class LongTask2 < Jober::Queue
|
20
|
+
def perform
|
21
|
+
sleep 1
|
22
|
+
SO["a"] += 1
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "should kill long tasks" do
|
27
|
+
it "kill" do
|
28
|
+
t = Time.now
|
29
|
+
SO["a"] = 0
|
30
|
+
100.times { LongTask2.enqueue }
|
31
|
+
|
32
|
+
m = run_manager_for(3, [LongTask1, LongTask11, LongTask2])
|
33
|
+
|
34
|
+
SO["a"].should == 3
|
35
|
+
m.pids.should == []
|
36
|
+
(Time.now - t).should < (3.2 + 2.5)
|
37
|
+
SO["11"].should == true
|
38
|
+
end
|
39
|
+
end
|
data/spec/pids_spec.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
class Man1 < Jober::Task
|
4
|
+
every 2
|
5
|
+
|
6
|
+
def perform
|
7
|
+
sleep 3
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Man2 < Jober::Task
|
12
|
+
every 2
|
13
|
+
|
14
|
+
def perform
|
15
|
+
sleep 2
|
16
|
+
raise "jopa"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "manage pids" do
|
21
|
+
it "should work" do
|
22
|
+
run_manager_for(nil, [Man1, Man2]) do |m|
|
23
|
+
sleep 1
|
24
|
+
m.pids.size.should == 2
|
25
|
+
|
26
|
+
sleep 1.5
|
27
|
+
m.pids.size.should == 1
|
28
|
+
|
29
|
+
sleep 1
|
30
|
+
m.pids.size.should == 0
|
31
|
+
|
32
|
+
sleep 1
|
33
|
+
m.pids.size.should == 1
|
34
|
+
|
35
|
+
sleep 1.1
|
36
|
+
m.pids.size.should == 2
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
class MyBatchQueue < Jober::QueueBatch
|
4
|
+
batch_size 6
|
5
|
+
|
6
|
+
attr_reader :res
|
7
|
+
|
8
|
+
def perform(batch)
|
9
|
+
@res ||= []
|
10
|
+
@res << batch
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "QueueBatch" do
|
15
|
+
it "should set internals" do
|
16
|
+
w = MyBatchQueue.new
|
17
|
+
w.queue_name.should == 'Jober:queue:my_batch'
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should execute" do
|
21
|
+
10.times { |i| MyBatchQueue.enqueue(i) }
|
22
|
+
MyBatchQueue.new.execute.res.should == [[[0], [1], [2], [3], [4], [5]], [[6], [7], [8], [9]]]
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should register class" do
|
26
|
+
Jober.classes.should include(MyBatchQueue)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
class FkMyParallel < Jober::Queue
|
4
|
+
5.times { |i| every 3 }
|
5
|
+
|
6
|
+
def perform(arg)
|
7
|
+
SO["fork:#{$$}"] ||= 0
|
8
|
+
SO["fork:#{$$}"] += 1
|
9
|
+
SO.inc("count", arg)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "Queue" do
|
14
|
+
it "should work" do
|
15
|
+
1000.times { |i| FkMyParallel.enqueue(i) }
|
16
|
+
run_manager_for(2, [FkMyParallel])
|
17
|
+
SO.raw_get("count").to_i.should == 499500
|
18
|
+
|
19
|
+
vals = SO.values("fork*")
|
20
|
+
vals.size.should == 5
|
21
|
+
vals.all? { |el| el > 20 }.should == true
|
22
|
+
vals.inject(0, :+).should == 1000
|
23
|
+
end
|
24
|
+
end
|
data/spec/queue_spec.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
class MyQueue < Jober::Queue
|
4
|
+
def initialize(*args)
|
5
|
+
super
|
6
|
+
@counter = 0
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :counter
|
10
|
+
|
11
|
+
def perform(x)
|
12
|
+
@counter += x
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Jasdfoadsfjaf < Jober::Queue
|
17
|
+
set_queue_name 'human_name'
|
18
|
+
|
19
|
+
def perform
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "Queue" do
|
24
|
+
it "should set internals" do
|
25
|
+
w = MyQueue.new
|
26
|
+
w.queue_name.should == 'Jober:queue:my'
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should execute" do
|
30
|
+
MyQueue.enqueue(1)
|
31
|
+
MyQueue.enqueue(2)
|
32
|
+
MyQueue.enqueue(3)
|
33
|
+
MyQueue.new.execute.counter.should == 6
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should register class" do
|
37
|
+
Jober.classes.should include(MyQueue)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "custom queue name" do
|
41
|
+
10.times { Jasdfoadsfjaf.enqueue }
|
42
|
+
p Jober.llens
|
43
|
+
Jober.llens['human_name'].should == 10
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
describe "SO" do
|
4
|
+
it "should work" do
|
5
|
+
SO["a"].should == nil
|
6
|
+
SO["a"] = 0
|
7
|
+
SO["a"].should == 0
|
8
|
+
SO["a"] += 1
|
9
|
+
SO["a"].should == 1
|
10
|
+
|
11
|
+
SO["b"].should == nil
|
12
|
+
SO["b"] = [1, 2, 3, :bla, {"a" => 2.5}]
|
13
|
+
SO["b"].should == [1, 2, 3, :bla, {"a" => 2.5}]
|
14
|
+
|
15
|
+
SO.del("a")
|
16
|
+
SO["a"].should == nil
|
17
|
+
|
18
|
+
SO.clear
|
19
|
+
SO["b"].should == nil
|
20
|
+
|
21
|
+
SO["c"] ||= 0
|
22
|
+
SO["c"] += 1
|
23
|
+
SO["c"].should == 1
|
24
|
+
end
|
25
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
Bundler.require :default
|
3
|
+
|
4
|
+
Jober::SharedObject
|
5
|
+
SO = Jober::SharedObject
|
6
|
+
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.expect_with(:rspec) { |c| c.syntax = :should }
|
9
|
+
|
10
|
+
config.before(:each) do
|
11
|
+
SO.clear
|
12
|
+
Jober.redis.keys("Jober*").each { |k| Jober.redis.del(k) }
|
13
|
+
end
|
14
|
+
|
15
|
+
config.after(:all) do
|
16
|
+
SO.clear
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def run_manager_for(timeout, classes, &block)
|
21
|
+
m = Jober::Manager.new "test", classes
|
22
|
+
m.run!
|
23
|
+
|
24
|
+
if block
|
25
|
+
block.call(m)
|
26
|
+
else
|
27
|
+
sleep timeout
|
28
|
+
end
|
29
|
+
|
30
|
+
m.stop
|
31
|
+
sleep 0.1
|
32
|
+
m
|
33
|
+
end
|
data/spec/stats_spec.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
class MyQueue1 < Jober::Queue
|
4
|
+
def perform
|
5
|
+
sleep 2
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class MyQueue2 < Jober::QueueBatch
|
10
|
+
def perform
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "Stats" do
|
15
|
+
it "llens" do
|
16
|
+
10.times { MyQueue1.enqueue }
|
17
|
+
99.times { MyQueue2.enqueue }
|
18
|
+
|
19
|
+
h = Jober.llens
|
20
|
+
h['my1'].should == 10
|
21
|
+
h['my2'].should == 99
|
22
|
+
end
|
23
|
+
|
24
|
+
it "stats" do
|
25
|
+
MyQueue1.enqueue
|
26
|
+
run_manager_for(3, [MyQueue1, MyQueue2])
|
27
|
+
h = Jober.stats
|
28
|
+
h['my1'][:start].should be
|
29
|
+
h['my1'][:end].should be
|
30
|
+
h['my1'][:duration].should be_within(0.1).of(2.0)
|
31
|
+
h['my2'].should == {:start=>nil, :end=>nil, :duration=>nil}
|
32
|
+
end
|
33
|
+
end
|
data/spec/task_spec.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
class WorkerTask < Jober::Task
|
4
|
+
def initialize(*args)
|
5
|
+
super
|
6
|
+
@counter = 0
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :counter
|
10
|
+
|
11
|
+
def perform
|
12
|
+
10.times { @counter += 1 }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Task2 < Jober::Task
|
17
|
+
every 3, :bla
|
18
|
+
every 5
|
19
|
+
|
20
|
+
def bla
|
21
|
+
end
|
22
|
+
|
23
|
+
def perform
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "Task" do
|
28
|
+
it "should execute" do
|
29
|
+
w = WorkerTask.new
|
30
|
+
w.execute.counter.should == 10
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should set short_name" do
|
34
|
+
WorkerTask.short_name.should == 'worker'
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should register class" do
|
38
|
+
Jober.classes.should include(WorkerTask)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "set some schedulers" do
|
42
|
+
Task2.workers.should == [[3, :bla], [5, :perform]]
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
class MyUniqueQueue < Jober::UniqueQueue
|
4
|
+
def initialize(*args)
|
5
|
+
super
|
6
|
+
@counter = 0
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :counter
|
10
|
+
|
11
|
+
def perform(x)
|
12
|
+
@counter += x
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "Queue" do
|
17
|
+
it "should set internals" do
|
18
|
+
w = MyUniqueQueue.new
|
19
|
+
w.queue_name.should == 'Jober:queue:my_unique'
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should execute Only for unique values" do
|
23
|
+
MyUniqueQueue.enqueue(1)
|
24
|
+
MyUniqueQueue.enqueue(2)
|
25
|
+
MyUniqueQueue.enqueue(2)
|
26
|
+
MyUniqueQueue.enqueue(2)
|
27
|
+
MyUniqueQueue.enqueue(3)
|
28
|
+
MyUniqueQueue.enqueue(3)
|
29
|
+
MyUniqueQueue.enqueue(3)
|
30
|
+
MyUniqueQueue.len.should == 3
|
31
|
+
MyUniqueQueue.new.execute.counter.should == 6
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jober
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- "'Konstantin Makarchev'"
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-04-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
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: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Simple background jobs, queues.
|
70
|
+
email:
|
71
|
+
- "'kostya27@gmail.com'"
|
72
|
+
executables:
|
73
|
+
- jober
|
74
|
+
- manager
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- ".gitignore"
|
79
|
+
- Gemfile
|
80
|
+
- LICENSE.txt
|
81
|
+
- Rakefile
|
82
|
+
- bin/jober
|
83
|
+
- bin/manager
|
84
|
+
- examples/classes.rb
|
85
|
+
- examples/man.rb
|
86
|
+
- jober.gemspec
|
87
|
+
- lib/jober.rb
|
88
|
+
- lib/jober/abstract_task.rb
|
89
|
+
- lib/jober/logger.rb
|
90
|
+
- lib/jober/manager.rb
|
91
|
+
- lib/jober/queue.rb
|
92
|
+
- lib/jober/queue_batch.rb
|
93
|
+
- lib/jober/shared_object.rb
|
94
|
+
- lib/jober/task.rb
|
95
|
+
- lib/jober/unique_queue.rb
|
96
|
+
- lib/jober/version.rb
|
97
|
+
- spec/chain_spec.rb
|
98
|
+
- spec/integration_spec.rb
|
99
|
+
- spec/kill_task_spec.rb
|
100
|
+
- spec/pids_spec.rb
|
101
|
+
- spec/queue_batch_spec.rb
|
102
|
+
- spec/queue_parallel_spec.rb
|
103
|
+
- spec/queue_spec.rb
|
104
|
+
- spec/shared_object_spec.rb
|
105
|
+
- spec/spec_helper.rb
|
106
|
+
- spec/stats_spec.rb
|
107
|
+
- spec/task_spec.rb
|
108
|
+
- spec/unique_queue_spec.rb
|
109
|
+
homepage: ''
|
110
|
+
licenses:
|
111
|
+
- MIT
|
112
|
+
metadata: {}
|
113
|
+
post_install_message:
|
114
|
+
rdoc_options: []
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
requirements: []
|
128
|
+
rubyforge_project:
|
129
|
+
rubygems_version: 2.2.2
|
130
|
+
signing_key:
|
131
|
+
specification_version: 4
|
132
|
+
summary: Simple background jobs, queues.
|
133
|
+
test_files:
|
134
|
+
- spec/chain_spec.rb
|
135
|
+
- spec/integration_spec.rb
|
136
|
+
- spec/kill_task_spec.rb
|
137
|
+
- spec/pids_spec.rb
|
138
|
+
- spec/queue_batch_spec.rb
|
139
|
+
- spec/queue_parallel_spec.rb
|
140
|
+
- spec/queue_spec.rb
|
141
|
+
- spec/shared_object_spec.rb
|
142
|
+
- spec/spec_helper.rb
|
143
|
+
- spec/stats_spec.rb
|
144
|
+
- spec/task_spec.rb
|
145
|
+
- spec/unique_queue_spec.rb
|