jober 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|