worker-army 0.2.1 → 0.3.0
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 +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
|