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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3333bafc54ba37dcac68ad1e8ea338ee18e26512
4
- data.tar.gz: 273ea0c00e78379fa78d6b7808c34d08898d20c5
3
+ metadata.gz: 8091229fb2c866bd2995c9a60ec791e43b4668fb
4
+ data.tar.gz: 9ff89f626a9d0f0ec49f1ff026037c0b474dc23d
5
5
  SHA512:
6
- metadata.gz: 62bd6433faf7aa220080946c80092e2e1f3abac80f9e716830ca43aeaada2cfca07611881b98e3ea3f577a751e4c277cd8142991faf8d2e2d84c50c184bff151
7
- data.tar.gz: 46a3af7c5a75245bb6ebbd9b281317655bb012e589b15d9fae33fd99e1c229bd33b7471e06cf8f27a536b3bd10e1c9dd2119984a08e135117951dc945187c66d
6
+ metadata.gz: d6f235480c8cb018de1759ee8f4da0b0d01a274ada3146892f4a27193e17105dadc40b7d15b33beaa175514820aebe35d92ee775477ef0b810f098db4c3b9dd8
7
+ data.tar.gz: 3dd6d94b5f2080d6187e649c8c27aa37201584ea7ea584db38338ab37af5daf31e921ce4ddd616d17f5e7a191feeb2cadd65ef159f32eb9a89419cc1e3284a32
data/Gemfile CHANGED
@@ -11,8 +11,6 @@ gem 'unicorn'
11
11
  gem "foreman"
12
12
  gem "jeweler", "~> 2.0.1"
13
13
 
14
- # Add dependencies to develop your gem here.
15
- # Include everything needed to run rake, tests, features, etc.
16
14
  group :development do
17
15
  gem "bundler", "~> 1.6.2"
18
16
  end
data/README.rdoc CHANGED
@@ -1,6 +1,6 @@
1
1
  = worker-army
2
2
 
3
- Description goes here.
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.2.1
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'
@@ -4,32 +4,59 @@ require "multi_json"
4
4
 
5
5
  module WorkerArmy
6
6
  class Client
7
- def self.push_job(job_class, data = {}, callback_url = nil, queue_name = 'queue')
8
- raise "No data" unless data
9
- raise "No job class provided" unless job_class
10
-
11
- if ENV['worker_army_endpoint']
12
- puts "Using environment variables for config..."
13
- @config = { endpoint: ENV['worker_army_endpoint'] }
14
- else
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
- puts "Using config in your home directory"
17
- @config = YAML.load(File.read("#{ENV['HOME']}/.worker_army.yml"))
18
- rescue Errno::ENOENT
19
- raise "worker_army.yml expected in ~/.worker_army.yml"
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
- worker_army_base_url = @config['endpoint']
24
- callback_url = "#{worker_army_base_url}/generic_callback" unless callback_url
25
- response = RestClient.post "#{worker_army_base_url}/jobs",
26
- data.merge(
27
- job_class: job_class,
28
- callback_url: "#{worker_army_base_url}/callback?callback_url=#{callback_url}",
29
- queue_name: queue_name
30
- ).to_json,
31
- :content_type => :json, :accept => :json
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
@@ -1,7 +1,9 @@
1
1
  class ExampleJob
2
+ attr_accessor :log
3
+
2
4
  def perform(data = {})
3
5
  response_data = {foo: 'bar'}
4
- puts "in example worker with data: #{data}"
6
+ log.debug("in example worker with data: #{data}")
5
7
  sleep 2
6
8
  response_data
7
9
  end
@@ -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
- def self.config
18
- if ENV['worker_army_redis_host'] and ENV['worker_army_redis_port']
19
- config = { 'redis_host' => ENV['worker_army_redis_host'], 'redis_port' => ENV['worker_army_redis_port'] }
20
- if ENV['worker_army_redis_auth']
21
- config['redis_auth'] = ENV['worker_army_redis_auth']
22
- end
23
- else
24
- begin
25
- # puts "Using config in your home directory"
26
- config = YAML.load(File.read("#{ENV['HOME']}/.worker_army.yml"))
27
- rescue Errno::ENOENT
28
- raise "worker_army.yml expected in ~/.worker_army.yml"
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
- def self.redis_instance
35
- $config = Queue.config unless $config
36
- unless $redis
37
- $redis = Redis.new(host: $config['redis_host'], port: $config['redis_port'])
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
- def self.close_redis_connection
44
- $redis.quit if $redis
45
- $redis = nil
46
+ def close_redis_connection
47
+ $redis.quit if $redis
48
+ $redis = nil
49
+ end
46
50
  end
47
51
 
48
- def push(data, queue_name = "queue")
52
+ def push(data, queue_prefix = "queue")
49
53
  if Queue.redis_instance and data
50
- job_count = Queue.redis_instance.incr("#{queue_name}_counter")
51
- queue_name = data['queue_name'] if data['queue_name']
52
- queue_name = "#{queue_name}_#{data['job_class']}"
53
- Queue.redis_instance.rpush queue_name, data.merge(job_count: job_count).to_json
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, queue_name = "queue")
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("#{queue_name}_#{job_class_name}")
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
- job_count = data['job_count']
77
+ job_id = data['job_id']
67
78
  callback_url = data['callback_url']
68
- Queue.redis_instance["job_#{job_count}"] = data
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
- puts e
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
- worker_pings ? worker_pings.collect {|json| JSON.parse(json)} : []
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(queue_name = "queue")
91
- Queue.redis_instance["#{queue_name}_counter"]
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
@@ -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
- data = { job_count: job_count, workers: workers }
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 data
25
+ queue_job = queue.push data if data
26
+ json queue_job
20
27
  end
21
28
 
22
29
  post '/callback' do
@@ -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
- puts "Worker ready!"
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
- puts "List: #{list} => #{element}"
23
- response_data = {}
24
- job_count = 0
25
- begin
26
- data = JSON.parse(element)
27
- job_count = data['job_count']
28
- callback_url = data['callback_url']
29
- if @job and @job.class.name == data['job_class']
30
- response_data = @job.perform(data)
31
- response_data.merge!(job_count: job_count, callback_url: callback_url,
32
- finished_at: Time.now.utc.to_i, host_name: @host_name)
33
- if @worker_name
34
- response_data.merge!(worker_name: @worker_name)
35
- end
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
- if response_data
41
- begin
42
- response = RestClient.post data['callback_url'],
43
- response_data.to_json, :content_type => :json, :accept => :json
44
- rescue => e
45
- puts e
46
- end
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
- self.process_queue
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.2.1 ruby lib
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.2.1"
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-23"
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"]
@@ -1,4 +1,6 @@
1
1
  endpoint: "http://localhost:9292"
2
2
  redis_host: "localhost"
3
3
  redis_port: 6379
4
- redis_auth: "YOUR_PASS"
4
+ redis_auth: "YOUR_PASS"
5
+ client_retry_count: 10
6
+ worker_retry_count: 10
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.2.1
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-23 00:00:00.000000000 Z
11
+ date: 2014-05-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis