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 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