komrade-client 1.0.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.
- data/lib/komrade.rb +36 -0
- data/lib/komrade/http_helpers.rb +74 -0
- data/lib/komrade/queue.rb +25 -0
- data/lib/komrade/rate_limiter.rb +55 -0
- data/lib/komrade/tasks.rb +18 -0
- data/lib/komrade/worker.rb +63 -0
- data/readme.md +61 -0
- metadata +52 -0
data/lib/komrade.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
|
|
3
|
+
module Komrade
|
|
4
|
+
extend self
|
|
5
|
+
Error = Class.new(StandardError)
|
|
6
|
+
|
|
7
|
+
def env(key)
|
|
8
|
+
ENV[key]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def env!(key)
|
|
12
|
+
env(key) || raise(Error, "Komrade requires #{key} be set in the ENV.")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def url
|
|
16
|
+
URI.parse(env!("KOMRADE_URL"))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def log(data)
|
|
20
|
+
result = nil
|
|
21
|
+
if data.key?(:measure)
|
|
22
|
+
data[:measure].insert(0, "komrade.")
|
|
23
|
+
end
|
|
24
|
+
if block_given?
|
|
25
|
+
start = Time.now
|
|
26
|
+
result = yield
|
|
27
|
+
data.merge!(val: (Time.now - start))
|
|
28
|
+
end
|
|
29
|
+
data.reduce(out=String.new) do |s, tup|
|
|
30
|
+
s << [tup.first, tup.last].join("=") << " "
|
|
31
|
+
end
|
|
32
|
+
$stdout.puts(out)
|
|
33
|
+
return result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'net/http'
|
|
3
|
+
require 'komrade'
|
|
4
|
+
require 'komrade/rate_limiter'
|
|
5
|
+
|
|
6
|
+
module Komrade
|
|
7
|
+
module HttpHelpers
|
|
8
|
+
MAX_RETRY = 4
|
|
9
|
+
|
|
10
|
+
def put(path, body)
|
|
11
|
+
make_request(Net::HTTP::Put.new(path), body)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def get(path)
|
|
15
|
+
make_request(Net::HTTP::Get.new(path))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def delete(path)
|
|
19
|
+
make_request(Net::HTTP::Delete.new(path))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def make_request(req, body=nil)
|
|
23
|
+
req.basic_auth(Komrade.url.user, Komrade.url.password)
|
|
24
|
+
if body
|
|
25
|
+
begin
|
|
26
|
+
req.content_type = 'application/json'
|
|
27
|
+
req.body = JSON.dump(body)
|
|
28
|
+
rescue JSON => e
|
|
29
|
+
raise(ArgumentError,
|
|
30
|
+
"Komrade is unable to convert enqueue payload to JSON.\n" +
|
|
31
|
+
"payload=#{body}\n")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
attempts = 0
|
|
35
|
+
while attempts < MAX_RETRY
|
|
36
|
+
begin
|
|
37
|
+
resp = nil
|
|
38
|
+
RateLimiter.limiter(req.path, 10, 1) do
|
|
39
|
+
resp = http.request(req)
|
|
40
|
+
end
|
|
41
|
+
if (Integer(resp.code) / 100) == 2
|
|
42
|
+
return JSON.parse(resp.body)
|
|
43
|
+
end
|
|
44
|
+
rescue Net::HTTPError => e
|
|
45
|
+
next
|
|
46
|
+
rescue RateLimiter::Unavailable
|
|
47
|
+
sleep(0.5)
|
|
48
|
+
ensure
|
|
49
|
+
attempts += 1
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
case req.class
|
|
53
|
+
when Net::HTTP::Delete
|
|
54
|
+
raise(Komrade::Error, "Unable to delete work from Komrade.")
|
|
55
|
+
when Net::HTTP::Put
|
|
56
|
+
raise(Komrade::Error, "Unable to send work to Komrade.")
|
|
57
|
+
when Net::HTTP::Get
|
|
58
|
+
raise(Komrade::Error, "Unable to get work from Komrade.")
|
|
59
|
+
default
|
|
60
|
+
raise(Komrade::Error, "Unable to communicate with Komrade.")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def http
|
|
65
|
+
@http ||= Net::HTTP.new(Komrade.url.host, Komrade.url.port).tap do |h|
|
|
66
|
+
if Komrade.url.scheme == 'https'
|
|
67
|
+
h.use_ssl = true
|
|
68
|
+
h.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
require 'komrade'
|
|
3
|
+
require 'komrade/http_helpers'
|
|
4
|
+
|
|
5
|
+
module Komrade
|
|
6
|
+
module Queue
|
|
7
|
+
extend self
|
|
8
|
+
extend HttpHelpers
|
|
9
|
+
|
|
10
|
+
def enqueue(method, *args)
|
|
11
|
+
SecureRandom.uuid.tap do |id|
|
|
12
|
+
put("/jobs/#{id}", method: method, args: args)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def dequeue(opts={})
|
|
17
|
+
limit = opts[:limit] || 1
|
|
18
|
+
get("/jobs?limit=#{limit}")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def remove(id)
|
|
22
|
+
delete("/jobs/#{id}")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Komrade
|
|
2
|
+
module RateLimiter
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
Unavailable = Class.new(Komrade::Error)
|
|
6
|
+
|
|
7
|
+
def limits
|
|
8
|
+
@limits ||= {counts: Hash.new(0), timestamps: Hash.new(0)}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def breaks
|
|
12
|
+
@breaks ||= {counts: Hash.new(0), timestamps: Hash.new(0)}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def stamp(data, attr, bucket)
|
|
16
|
+
timestamp = Time.now.to_i / bucket
|
|
17
|
+
data[:counts][attr] = 0 unless data[:timestamps][attr] == timestamp
|
|
18
|
+
timestamp
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def increment(data, attr, timestamp)
|
|
22
|
+
data[:timestamps][attr] = timestamp
|
|
23
|
+
data[:counts][attr] += 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def check(data, attr, value, &blk)
|
|
27
|
+
if value == 0 || data[:counts][attr] < value
|
|
28
|
+
yield
|
|
29
|
+
else
|
|
30
|
+
raise(Unavailable, attr)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def limiter(attr, value, bucket, &blk)
|
|
35
|
+
timestamp = stamp(limits, attr, bucket)
|
|
36
|
+
increment(limits, attr, timestamp)
|
|
37
|
+
check(limits, attr, value) do
|
|
38
|
+
yield
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def breaker(attr, value, bucket, &blk)
|
|
43
|
+
timestamp = stamp(breaks, attr, bucket)
|
|
44
|
+
check(breaks, attr, value) do
|
|
45
|
+
begin
|
|
46
|
+
yield
|
|
47
|
+
rescue => e
|
|
48
|
+
increment(breaks, attr, timestamp)
|
|
49
|
+
raise
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'komrade/worker'
|
|
2
|
+
|
|
3
|
+
task :environment
|
|
4
|
+
|
|
5
|
+
namespace :komrade do
|
|
6
|
+
desc "Start a new worker."
|
|
7
|
+
task :work => :environment do
|
|
8
|
+
trap('INT') {exit}
|
|
9
|
+
trap('TERM') {@worker.stop}
|
|
10
|
+
@worker = Komrade::Worker.new
|
|
11
|
+
@worker.start
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
desc "Returns the number of jobs in the (default or QUEUE) queue"
|
|
15
|
+
task :count => :environment do
|
|
16
|
+
$stdout.puts(Komrade::Worker.new.queue.count)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require 'komrade/queue'
|
|
2
|
+
|
|
3
|
+
module Komrade
|
|
4
|
+
class Worker
|
|
5
|
+
|
|
6
|
+
def initialize(args={})
|
|
7
|
+
@running = true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Start a loop and work jobs indefinitely.
|
|
11
|
+
# Call this method to start the worker.
|
|
12
|
+
# This is the easiest way to start working jobs.
|
|
13
|
+
def start
|
|
14
|
+
work while @running
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Call this method to stop the worker.
|
|
18
|
+
# The worker may not stop immediately if the worker
|
|
19
|
+
# is sleeping.
|
|
20
|
+
def stop
|
|
21
|
+
@running = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# This method will lock a job & evaluate the code defined by the job.
|
|
25
|
+
# Also, this method will make the best attempt to delete the job
|
|
26
|
+
# from the queue before returning.
|
|
27
|
+
def work
|
|
28
|
+
jobs = Queue.dequeue
|
|
29
|
+
until jobs.empty?
|
|
30
|
+
job = jobs.pop
|
|
31
|
+
begin
|
|
32
|
+
call(job["payload"])
|
|
33
|
+
rescue => e
|
|
34
|
+
handle_failure(job, e)
|
|
35
|
+
ensure
|
|
36
|
+
Queue.remove(job["id"])
|
|
37
|
+
log(:at => "remove-job", :job => job["id"])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Each job includes a method column. We will use ruby's eval
|
|
43
|
+
# to grab the ruby object from memory. We send the method to
|
|
44
|
+
# the object and pass the args.
|
|
45
|
+
def call(payload)
|
|
46
|
+
args = payload["args"]
|
|
47
|
+
klass = eval(payload["method"].split(".").first)
|
|
48
|
+
message = payload["method"].split(".").last
|
|
49
|
+
klass.send(message, *args)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# This method will be called when an exception
|
|
53
|
+
# is raised during the execution of the job.
|
|
54
|
+
def handle_failure(job,e)
|
|
55
|
+
log(:at => "handle_failure")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def log(data)
|
|
59
|
+
Komrade.log(data)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
end
|
|
63
|
+
end
|
data/readme.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Komrade
|
|
2
|
+
|
|
3
|
+
A client library for the komrade worker queue.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
$ heroku addons:add komrade:basic
|
|
9
|
+
$ gem install komrade-client
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
1. Install Gem
|
|
15
|
+
2. Enqueue
|
|
16
|
+
3. Dequeue
|
|
17
|
+
|
|
18
|
+
### Install
|
|
19
|
+
|
|
20
|
+
Gemfile
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
source :rubygems
|
|
24
|
+
gem 'komrade-client', '1.0.0'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Enqueue
|
|
28
|
+
|
|
29
|
+
Example Model
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
class User < ActiveRecord::Base
|
|
33
|
+
after_create :enqueue_welcome_email
|
|
34
|
+
|
|
35
|
+
def self.send_welcome_email(id)
|
|
36
|
+
if u = find(id)
|
|
37
|
+
Mailer.welcome(u).deliver
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def enqueue_welcome_email
|
|
42
|
+
Komrade.enqueue("User.send_welcome_email", self.id)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Dequeue
|
|
48
|
+
|
|
49
|
+
Rakefile
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
require 'komrade'
|
|
53
|
+
require 'komrade/tasks'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Procfile
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
web: bundle exec rails s
|
|
60
|
+
worker: bundle exec rake komrade:work
|
|
61
|
+
```
|
metadata
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: komrade-client
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Ryan Smith (♠ ace hacker)
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2013-01-26 00:00:00.000000000 Z
|
|
13
|
+
dependencies: []
|
|
14
|
+
description: A client library for the komrad worker queue.
|
|
15
|
+
email: komrade@32k.io
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- lib/komrade/http_helpers.rb
|
|
21
|
+
- lib/komrade/queue.rb
|
|
22
|
+
- lib/komrade/rate_limiter.rb
|
|
23
|
+
- lib/komrade/tasks.rb
|
|
24
|
+
- lib/komrade/worker.rb
|
|
25
|
+
- lib/komrade.rb
|
|
26
|
+
- readme.md
|
|
27
|
+
homepage: http://github.com/ryandotsmith/komrade-client
|
|
28
|
+
licenses:
|
|
29
|
+
- MIT
|
|
30
|
+
post_install_message:
|
|
31
|
+
rdoc_options: []
|
|
32
|
+
require_paths:
|
|
33
|
+
- lib
|
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
35
|
+
none: false
|
|
36
|
+
requirements:
|
|
37
|
+
- - ! '>='
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
41
|
+
none: false
|
|
42
|
+
requirements:
|
|
43
|
+
- - ! '>='
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0'
|
|
46
|
+
requirements: []
|
|
47
|
+
rubyforge_project:
|
|
48
|
+
rubygems_version: 1.8.23
|
|
49
|
+
signing_key:
|
|
50
|
+
specification_version: 3
|
|
51
|
+
summary: Queues are tough. Let Komrade work for you.
|
|
52
|
+
test_files: []
|