worker-army 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +0 -2
- data/README.rdoc +2 -3
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/lib/worker-army.rb +14 -0
- data/lib/worker_army/client.rb +49 -22
- data/lib/worker_army/example_job.rb +3 -1
- data/lib/worker_army/queue.rb +83 -38
- data/lib/worker_army/web.rb +10 -3
- data/lib/worker_army/worker.rb +62 -26
- data/worker-army.gemspec +3 -3
- data/worker-army.yml.sample +3 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8091229fb2c866bd2995c9a60ec791e43b4668fb
|
4
|
+
data.tar.gz: 9ff89f626a9d0f0ec49f1ff026037c0b474dc23d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d6f235480c8cb018de1759ee8f4da0b0d01a274ada3146892f4a27193e17105dadc40b7d15b33beaa175514820aebe35d92ee775477ef0b810f098db4c3b9dd8
|
7
|
+
data.tar.gz: 3dd6d94b5f2080d6187e649c8c27aa37201584ea7ea584db38338ab37af5daf31e921ce4ddd616d17f5e7a191feeb2cadd65ef159f32eb9a89419cc1e3284a32
|
data/Gemfile
CHANGED
data/README.rdoc
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
= worker-army
|
2
2
|
|
3
|
-
|
3
|
+
Simple redis based worker queue with a HTTP/Rest interface.
|
4
4
|
|
5
5
|
== Contributing to worker-army
|
6
6
|
|
@@ -14,6 +14,5 @@ Description goes here.
|
|
14
14
|
|
15
15
|
== Copyright
|
16
16
|
|
17
|
-
Copyright (c) 2013 Oliver Kiessler. See LICENSE.txt for
|
17
|
+
Copyright (c) 2013-2014 Oliver Kiessler. See LICENSE.txt for
|
18
18
|
further details.
|
19
|
-
|
data/Rakefile
CHANGED
@@ -13,7 +13,6 @@ require 'rake'
|
|
13
13
|
|
14
14
|
require 'jeweler'
|
15
15
|
Jeweler::Tasks.new do |gem|
|
16
|
-
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
16
|
gem.name = "worker-army"
|
18
17
|
gem.homepage = "http://github.com/okiess/worker-army"
|
19
18
|
gem.license = "MIT"
|
@@ -49,6 +48,7 @@ task 'start_example_worker' do
|
|
49
48
|
worker.process_queue
|
50
49
|
end
|
51
50
|
|
51
|
+
desc "Start a worker-army worker to execute a job class"
|
52
52
|
task :start_worker, :job_class do |t, args|
|
53
53
|
if args[:job_class]
|
54
54
|
worker = WorkerArmy::Worker.new
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/lib/worker-army.rb
CHANGED
@@ -1,4 +1,18 @@
|
|
1
1
|
require "redis"
|
2
|
+
require "logger"
|
3
|
+
|
4
|
+
module WorkerArmy
|
5
|
+
class Log
|
6
|
+
attr_accessor :log
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
self.log = Logger.new('/tmp/worker-army.log')
|
10
|
+
self.log.level = Logger::DEBUG
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
$WORKER_ARMY_LOG = WorkerArmy::Log.new.log
|
2
16
|
|
3
17
|
require File.dirname(__FILE__) + '/worker_army/queue'
|
4
18
|
require File.dirname(__FILE__) + '/worker_army/worker'
|
data/lib/worker_army/client.rb
CHANGED
@@ -4,32 +4,59 @@ require "multi_json"
|
|
4
4
|
|
5
5
|
module WorkerArmy
|
6
6
|
class Client
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
7
|
+
class << self
|
8
|
+
def push_job(job_class, data = {}, callback_url = nil, queue_prefix = 'queue', retry_count = 0)
|
9
|
+
raise "No data" unless data
|
10
|
+
raise "No job class provided" unless job_class
|
11
|
+
|
12
|
+
if ENV['worker_army_endpoint']
|
13
|
+
# puts "Using environment variables for config..."
|
14
|
+
@config = { endpoint: ENV['worker_army_endpoint'] }
|
15
|
+
else
|
16
|
+
begin
|
17
|
+
# puts "Using config in your home directory"
|
18
|
+
@config = YAML.load(File.read("#{ENV['HOME']}/.worker_army.yml"))
|
19
|
+
rescue Errno::ENOENT
|
20
|
+
raise "worker_army.yml expected in ~/.worker_army.yml"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
worker_army_base_url = @config['endpoint']
|
25
|
+
callback_url = "#{worker_army_base_url}/generic_callback" unless callback_url
|
26
|
+
response = nil
|
15
27
|
begin
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
28
|
+
response = RestClient.post "#{worker_army_base_url}/jobs",
|
29
|
+
data.merge(
|
30
|
+
job_class: job_class,
|
31
|
+
callback_url: "#{worker_army_base_url}/callback?callback_url=#{callback_url}",
|
32
|
+
queue_prefix: queue_prefix
|
33
|
+
).to_json,
|
34
|
+
:content_type => :json, :accept => :json
|
35
|
+
rescue => e
|
36
|
+
puts "Failed! Retrying (#{retry_count})..."
|
37
|
+
retry_count += 1
|
38
|
+
if retry_count < client_retry_count(@config)
|
39
|
+
sleep (retry_count * 2)
|
40
|
+
push_job(job_class, data, callback_url, queue_prefix, retry_count)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
if response and response.body and response.code == 200
|
44
|
+
hash = JSON.parse(response.body)
|
45
|
+
hash.merge(success: true)
|
46
|
+
else
|
47
|
+
{ success: false }
|
20
48
|
end
|
21
49
|
end
|
22
50
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
response.code == 200
|
51
|
+
def client_retry_count(config)
|
52
|
+
if ENV['worker_army_client_retry_count']
|
53
|
+
return ENV['worker_army_client_retry_count'].to_i
|
54
|
+
elsif config and config['client_retry_count']
|
55
|
+
return config['client_retry_count'].to_i
|
56
|
+
else
|
57
|
+
return 10
|
58
|
+
end
|
59
|
+
end
|
33
60
|
end
|
34
61
|
end
|
35
62
|
end
|
data/lib/worker_army/queue.rb
CHANGED
@@ -3,6 +3,7 @@ require "rest-client"
|
|
3
3
|
require "json"
|
4
4
|
require "multi_json"
|
5
5
|
require "yaml"
|
6
|
+
require 'securerandom'
|
6
7
|
|
7
8
|
module WorkerArmy
|
8
9
|
class Queue
|
@@ -12,83 +13,127 @@ module WorkerArmy
|
|
12
13
|
@config = Queue.config
|
13
14
|
# puts "Config: #{@config}"
|
14
15
|
Queue.redis_instance
|
16
|
+
@log = $WORKER_ARMY_LOG
|
15
17
|
end
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def config
|
21
|
+
if ENV['worker_army_redis_host'] and ENV['worker_army_redis_port']
|
22
|
+
config = { 'redis_host' => ENV['worker_army_redis_host'], 'redis_port' => ENV['worker_army_redis_port'] }
|
23
|
+
if ENV['worker_army_redis_auth']
|
24
|
+
config['redis_auth'] = ENV['worker_army_redis_auth']
|
25
|
+
end
|
26
|
+
else
|
27
|
+
begin
|
28
|
+
# puts "Using config in your home directory"
|
29
|
+
config = YAML.load(File.read("#{ENV['HOME']}/.worker_army.yml"))
|
30
|
+
rescue Errno::ENOENT
|
31
|
+
raise "worker_army.yml expected in ~/.worker_army.yml"
|
32
|
+
end
|
29
33
|
end
|
34
|
+
config
|
30
35
|
end
|
31
|
-
config
|
32
|
-
end
|
33
36
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
37
|
+
def redis_instance
|
38
|
+
$config = Queue.config unless $config
|
39
|
+
unless $redis
|
40
|
+
$redis = Redis.new(host: $config['redis_host'], port: $config['redis_port'])
|
41
|
+
end
|
42
|
+
$redis.auth($config['redis_auth']) if $config['redis_auth']
|
43
|
+
$redis
|
38
44
|
end
|
39
|
-
$redis.auth($config['redis_auth']) if $config['redis_auth']
|
40
|
-
$redis
|
41
|
-
end
|
42
45
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
+
def close_redis_connection
|
47
|
+
$redis.quit if $redis
|
48
|
+
$redis = nil
|
49
|
+
end
|
46
50
|
end
|
47
51
|
|
48
|
-
def push(data,
|
52
|
+
def push(data, queue_prefix = "queue")
|
49
53
|
if Queue.redis_instance and data
|
50
|
-
job_count = Queue.redis_instance.incr("#{
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
+
job_count = Queue.redis_instance.incr("#{queue_prefix}_counter")
|
55
|
+
queue_prefix = queue_prefix if queue_prefix
|
56
|
+
queue_prefix = data['queue_prefix'] if data['queue_prefix']
|
57
|
+
queue_name = "#{queue_prefix}_#{data['job_class']}"
|
58
|
+
Queue.redis_instance.sadd 'known_queues', queue_name
|
59
|
+
queue_count = Queue.redis_instance.incr("#{queue_name}_counter")
|
60
|
+
job_id = SecureRandom.uuid
|
61
|
+
Queue.redis_instance.rpush queue_name, data.merge(job_count: job_count,
|
62
|
+
queue_count: queue_count, job_id: job_id, queue_name: queue_name).to_json
|
54
63
|
end
|
55
64
|
raise "No data" unless data
|
56
65
|
raise "No redis connection!" unless Queue.redis_instance
|
66
|
+
{ job_count: job_count, job_id: job_id, queue_count: queue_count,
|
67
|
+
queue_name: queue_name }
|
57
68
|
end
|
58
69
|
|
59
|
-
def pop(job_class_name,
|
70
|
+
def pop(job_class_name, queue_prefix = "queue")
|
60
71
|
raise "No redis connection!" unless Queue.redis_instance
|
61
|
-
return Queue.redis_instance.blpop("#{
|
72
|
+
return Queue.redis_instance.blpop("#{queue_prefix}_#{job_class_name}")
|
62
73
|
end
|
63
74
|
|
64
75
|
def save_result(data)
|
65
76
|
if data
|
66
|
-
|
77
|
+
job_id = data['job_id']
|
67
78
|
callback_url = data['callback_url']
|
68
|
-
Queue.redis_instance["job_#{
|
79
|
+
Queue.redis_instance["job_#{job_id}"] = data
|
80
|
+
Queue.redis_instance.lpush 'jobs', job_id
|
69
81
|
if callback_url
|
70
82
|
data.delete("callback_url")
|
71
83
|
begin
|
72
84
|
response = RestClient.post callback_url.split("?callback_url=").last,
|
73
85
|
data.to_json, :content_type => :json, :accept => :json
|
74
86
|
rescue => e
|
75
|
-
|
87
|
+
@log.error(e)
|
76
88
|
end
|
77
89
|
end
|
78
90
|
end
|
79
91
|
end
|
80
92
|
|
93
|
+
def add_failed_job(job_id)
|
94
|
+
Queue.redis_instance.lpush 'failed_jobs', job_id
|
95
|
+
end
|
96
|
+
|
97
|
+
def failed_jobs
|
98
|
+
Queue.redis_instance.llen 'failed_jobs'
|
99
|
+
end
|
100
|
+
|
81
101
|
def ping(data)
|
82
102
|
Queue.redis_instance.lpush 'workers', data.to_json
|
103
|
+
Queue.redis_instance.set 'last_ping', data[:timestamp].to_i
|
104
|
+
end
|
105
|
+
|
106
|
+
def last_ping
|
107
|
+
Queue.redis_instance.get 'last_ping'
|
83
108
|
end
|
84
109
|
|
85
110
|
def get_known_workers(recent_worker_pings = 1000)
|
86
111
|
worker_pings = Queue.redis_instance.lrange 'workers', 0, recent_worker_pings
|
87
|
-
|
112
|
+
return [] unless worker_pings
|
113
|
+
worker_pings = worker_pings.collect {|json| JSON.parse(json)}.sort_by {|h| h['timestamp'].to_i}.reverse
|
114
|
+
uniq_workers = worker_pings.collect {|h| [h['host_name'], h['worker_pid']]}.uniq
|
115
|
+
workers = []
|
116
|
+
uniq_workers.each do |worker_pair|
|
117
|
+
worker_pings.each do |hash|
|
118
|
+
if hash['host_name'] == worker_pair[0] and hash['worker_pid'] == worker_pair[1]
|
119
|
+
workers << hash
|
120
|
+
break
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
workers
|
125
|
+
end
|
126
|
+
|
127
|
+
def get_known_queues
|
128
|
+
Queue.redis_instance.smembers 'known_queues'
|
129
|
+
end
|
130
|
+
|
131
|
+
def finished_jobs
|
132
|
+
Queue.redis_instance.llen 'jobs'
|
88
133
|
end
|
89
134
|
|
90
|
-
def get_job_count(
|
91
|
-
Queue.redis_instance["#{
|
135
|
+
def get_job_count(queue_prefix = "queue")
|
136
|
+
Queue.redis_instance["#{queue_prefix}_counter"].to_i
|
92
137
|
end
|
93
138
|
end
|
94
139
|
end
|
data/lib/worker_army/web.rb
CHANGED
@@ -9,14 +9,21 @@ queue = WorkerArmy::Queue.new
|
|
9
9
|
get '/' do
|
10
10
|
job_count = queue.get_job_count || 0
|
11
11
|
workers = queue.get_known_workers
|
12
|
-
|
12
|
+
last_ping = queue.last_ping || 0
|
13
|
+
queues = queue.get_known_queues
|
14
|
+
finished_jobs = queue.finished_jobs
|
15
|
+
failed_jobs = queue.failed_jobs
|
16
|
+
data = { job_count: job_count, finished_jobs: finished_jobs,
|
17
|
+
failed_jobs: failed_jobs, workers: workers,
|
18
|
+
last_worker_ping: last_ping.to_i, queues: queues
|
19
|
+
}
|
13
20
|
json data
|
14
21
|
end
|
15
22
|
|
16
23
|
post '/jobs' do
|
17
24
|
data = JSON.parse(request.body.read)
|
18
|
-
queue.push data if data
|
19
|
-
json
|
25
|
+
queue_job = queue.push data if data
|
26
|
+
json queue_job
|
20
27
|
end
|
21
28
|
|
22
29
|
post '/callback' do
|
data/lib/worker_army/worker.rb
CHANGED
@@ -5,47 +5,83 @@ require 'socket'
|
|
5
5
|
|
6
6
|
module WorkerArmy
|
7
7
|
class Worker
|
8
|
-
attr_accessor :queue, :job, :worker_name
|
8
|
+
attr_accessor :queue, :job, :worker_name, :processed, :failed, :config
|
9
9
|
def initialize(worker_name = nil)
|
10
10
|
@queue = WorkerArmy::Queue.new
|
11
11
|
@worker_name = worker_name
|
12
12
|
@host_name = Socket.gethostname
|
13
|
+
@processed = 0
|
14
|
+
@failed = 0
|
15
|
+
begin
|
16
|
+
# puts "Using config in your home directory"
|
17
|
+
@config = YAML.load(File.read("#{ENV['HOME']}/.worker_army.yml"))
|
18
|
+
rescue Errno::ENOENT
|
19
|
+
# ignore
|
20
|
+
end
|
21
|
+
@log = $WORKER_ARMY_LOG
|
13
22
|
end
|
14
23
|
|
15
24
|
def process_queue
|
16
25
|
raise "No job class set!" unless @job
|
26
|
+
@job.log = @log if @job.respond_to?(:log)
|
17
27
|
@queue.ping(worker_pid: Process.pid, job_name: @job.class.name, host_name: @host_name,
|
18
28
|
timestamp: Time.now.utc.to_i)
|
19
|
-
|
29
|
+
@log.info("Worker ready! Waiting for jobs: #{@job.class.name}")
|
30
|
+
@log.info("Processed: #{@processed} - Failed: #{@failed}")
|
20
31
|
list, element = @queue.pop(@job.class.name)
|
21
32
|
if list and element
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
execute_job(list, element, 0)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def execute_job(list, element, retry_count = 0)
|
39
|
+
@log.debug("Queue: #{list} => #{element}") if retry_count == 0
|
40
|
+
response_data = {}
|
41
|
+
job_count = 0
|
42
|
+
begin
|
43
|
+
data = JSON.parse(element)
|
44
|
+
job_id = data['job_id']
|
45
|
+
callback_url = data['callback_url']
|
46
|
+
if @job and @job.class.name == data['job_class']
|
47
|
+
response_data = @job.perform(data)
|
48
|
+
response_data = {} unless response_data
|
49
|
+
response_data.merge!(job_id: job_id, callback_url: callback_url,
|
50
|
+
finished_at: Time.now.utc.to_i, host_name: @host_name)
|
51
|
+
@processed += 1
|
52
|
+
if @worker_name
|
53
|
+
response_data.merge!(worker_name: @worker_name)
|
36
54
|
end
|
37
|
-
rescue => e
|
38
|
-
puts e
|
39
55
|
end
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
56
|
+
response_data
|
57
|
+
rescue => e
|
58
|
+
@log.error(e)
|
59
|
+
retry_count += 1
|
60
|
+
if retry_count < worker_retry_count(@config)
|
61
|
+
@log.debug("Failed! Retrying (#{retry_count})...")
|
62
|
+
sleep (retry_count * 2)
|
63
|
+
execute_job(list, element, retry_count)
|
64
|
+
else
|
65
|
+
@failed += 1
|
66
|
+
@queue.add_failed_job(job_id)
|
47
67
|
end
|
48
|
-
|
68
|
+
end
|
69
|
+
begin
|
70
|
+
response = RestClient.post data['callback_url'],
|
71
|
+
response_data.to_json, :content_type => :json, :accept => :json
|
72
|
+
rescue => e
|
73
|
+
@logger.error(e)
|
74
|
+
end
|
75
|
+
self.process_queue
|
76
|
+
end
|
77
|
+
|
78
|
+
def worker_retry_count(config = nil)
|
79
|
+
if ENV['worker_army_worker_retry_count']
|
80
|
+
return ENV['worker_army_worker_retry_count'].to_i
|
81
|
+
elsif config and config['worker_retry_count']
|
82
|
+
return config['worker_retry_count'].to_i
|
83
|
+
else
|
84
|
+
return 10
|
49
85
|
end
|
50
86
|
end
|
51
87
|
end
|
data/worker-army.gemspec
CHANGED
@@ -2,16 +2,16 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: worker-army 0.
|
5
|
+
# stub: worker-army 0.3.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "worker-army"
|
9
|
-
s.version = "0.
|
9
|
+
s.version = "0.3.0"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib"]
|
13
13
|
s.authors = ["Oliver Kiessler"]
|
14
|
-
s.date = "2014-05-
|
14
|
+
s.date = "2014-05-27"
|
15
15
|
s.description = "Simple redis based worker queue with a HTTP/Rest interface"
|
16
16
|
s.email = "kiessler@inceedo.com"
|
17
17
|
s.executables = ["worker_army"]
|
data/worker-army.yml.sample
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: worker-army
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Oliver Kiessler
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-05-
|
11
|
+
date: 2014-05-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|