geary 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.markdown +76 -0
- data/bin/geary +9 -0
- data/lib/gearman/client.rb +80 -0
- data/lib/gearman/connection.rb +132 -0
- data/lib/gearman/error.rb +7 -0
- data/lib/gearman/packet.rb +9 -0
- data/lib/gearman/packet/repository.rb +35 -0
- data/lib/gearman/packet/sugar.rb +103 -0
- data/lib/gearman/worker.rb +61 -0
- data/lib/geary.rb +8 -0
- data/lib/geary/cli.rb +86 -0
- data/lib/geary/configuration.rb +18 -0
- data/lib/geary/error.rb +7 -0
- data/lib/geary/manager.rb +84 -0
- data/lib/geary/option_parser.rb +42 -0
- data/lib/geary/performer.rb +73 -0
- data/lib/geary/railtie.rb +9 -0
- data/lib/geary/worker.rb +49 -0
- data/spec/gearman/client_spec.rb +36 -0
- data/spec/gearman/connection_spec.rb +67 -0
- data/spec/gearman/packet/sugar_spec.rb +58 -0
- data/spec/gearman/packet_spec.rb +22 -0
- data/spec/gearman/worker_spec.rb +68 -0
- data/spec/geary/cli_spec.rb +40 -0
- data/spec/geary/manager_spec.rb +123 -0
- data/spec/geary/option_parser_spec.rb +17 -0
- data/spec/geary/performer_spec.rb +128 -0
- data/spec/geary/worker_spec.rb +23 -0
- metadata +222 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 89f649c73ee64b715f88ce8ba647f5e80673edc2
|
4
|
+
data.tar.gz: 2146c0c967c2c1dda7387d850fcf6fc0546db318
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f84185ba53c50612584adf5ddc04e0ffc70b56e21be6f9f1959c184f1dc7bac4bb6955103accabd5ae80c3d6a13120472037262d8e023c00c5e986ec217d9356
|
7
|
+
data.tar.gz: 9e7bc90e9bfad7f8779e6836c6e3a9c2de748bc53ebdc1fc237040f8c9831b2d64624cd4a74dd0ed22d7534a24bc38d40598d8041f5aa9239794156aa472cc5a
|
data/README.markdown
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# Geary
|
2
|
+
|
3
|
+
Geary gives Gearman job processing a familiar face.
|
4
|
+
|
5
|
+
## Getting Started
|
6
|
+
|
7
|
+
1. Add Geary to your Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'geary', require: false
|
11
|
+
```
|
12
|
+
|
13
|
+
2. Create a worker:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
require 'geary/worker'
|
17
|
+
|
18
|
+
class FollowUpWorker
|
19
|
+
extend Geary::Worker
|
20
|
+
|
21
|
+
def perform(id)
|
22
|
+
# User.find(id).tap do |user|
|
23
|
+
# FollowUpMailer.follow_up_with(user)
|
24
|
+
# end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
3. Start working:
|
31
|
+
|
32
|
+
```
|
33
|
+
geary
|
34
|
+
```
|
35
|
+
|
36
|
+
4. Send jobs to the workers:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
FollowUpWorker.perform_async(1)
|
40
|
+
```
|
41
|
+
|
42
|
+
## Configuring Geary
|
43
|
+
|
44
|
+
Without configuration, Geary will spawn 25 workers, each of which will process jobs from a Gearman server running on `localhost:4730`. Geary can be configured to process jobs from a different Gearman server, as well as from multiple servers. For instance, if you're running a Gearman server on a different address, you might start the workers like this:
|
45
|
+
|
46
|
+
```
|
47
|
+
geary -s gearman://localhost:4731
|
48
|
+
```
|
49
|
+
|
50
|
+
Processing jobs from multiple servers is a matter of passing in comma-delimited addresses:
|
51
|
+
|
52
|
+
```
|
53
|
+
geary -s gearman://localhost:4730,gearman://localhost:4731
|
54
|
+
```
|
55
|
+
|
56
|
+
Classes which extend themselves with `Geary::Worker` submit background jobs to a Gearman server running on `localhost:4730` by default, but can be configured to submit jobs to multiple servers like so:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
require 'geary/worker'
|
60
|
+
|
61
|
+
class OverheadWorker
|
62
|
+
extend Geary::Worker
|
63
|
+
|
64
|
+
use_gearman_client 'gearman://localhost:4730', 'gearman://localhost:4731'
|
65
|
+
|
66
|
+
def perform ; end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
The following code will submit four jobs.
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
4.times { OverheadWorker.perform_async }
|
74
|
+
```
|
75
|
+
|
76
|
+
If the server listening on port 4730 disappears midway, our gearman client will disconnect from it, and submit future jobs to the server listening on 4731. As of right now, there is no backoff behavior. If the server listening on 4731 disappears and we're still not out of jobs to submit, we'll attempt to reconnect to `localhost:4730`, potentially to our never-ending dismay.
|
data/bin/geary
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'celluloid'
|
2
|
+
require 'gearman/connection'
|
3
|
+
require 'gearman/packet'
|
4
|
+
require 'securerandom'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
module Gearman
|
8
|
+
class Client
|
9
|
+
include Celluloid
|
10
|
+
|
11
|
+
trap_exit :reconnect
|
12
|
+
finalizer :disconnect
|
13
|
+
|
14
|
+
def initialize(*addresses)
|
15
|
+
@addresses = addresses.map(&Kernel.method(:URI))
|
16
|
+
@generate_unique_id = SecureRandom.method(:uuid)
|
17
|
+
@addresses_by_connection_id = {}
|
18
|
+
@connections = []
|
19
|
+
build_connections
|
20
|
+
end
|
21
|
+
|
22
|
+
def submit_job_bg(function_name, data)
|
23
|
+
packet = Packet::SUBMIT_JOB_BG.new(
|
24
|
+
function_name: function_name,
|
25
|
+
unique_id: @generate_unique_id.(),
|
26
|
+
data: data
|
27
|
+
)
|
28
|
+
|
29
|
+
with_connection do |connection|
|
30
|
+
connection.write(packet)
|
31
|
+
connection.async.next
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def generate_unique_id_with(methodology)
|
36
|
+
@generate_unique_id = methodology
|
37
|
+
end
|
38
|
+
|
39
|
+
def disconnect
|
40
|
+
@connections.select(&:alive?).each(&:terminate)
|
41
|
+
end
|
42
|
+
|
43
|
+
def build_connections
|
44
|
+
@addresses.each do |address|
|
45
|
+
build_connection(address)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_connection(address)
|
50
|
+
connection = Connection.new_link(address)
|
51
|
+
@addresses_by_connection_id[connection.object_id] = address
|
52
|
+
@connections << connection
|
53
|
+
end
|
54
|
+
|
55
|
+
def reconnect(connection = nil, reason = nil)
|
56
|
+
Celluloid.logger.debug(reason) if reason
|
57
|
+
|
58
|
+
connection ||= current_connection
|
59
|
+
connection.terminate if connection.alive?
|
60
|
+
|
61
|
+
forget_connection(connection) do |address|
|
62
|
+
build_connection(address)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def with_connection(&action)
|
67
|
+
action.call(current_connection)
|
68
|
+
end
|
69
|
+
|
70
|
+
def current_connection
|
71
|
+
@connections.first
|
72
|
+
end
|
73
|
+
|
74
|
+
def forget_connection(connection)
|
75
|
+
@connections.delete(connection)
|
76
|
+
yield @addresses_by_connection_id[connection.object_id]
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'celluloid/io'
|
2
|
+
require 'gearman/packet'
|
3
|
+
require 'gearman/error'
|
4
|
+
|
5
|
+
module Gearman
|
6
|
+
class Connection
|
7
|
+
include Celluloid::IO
|
8
|
+
|
9
|
+
finalizer :disconnect
|
10
|
+
|
11
|
+
unless defined? IncompleteReadError
|
12
|
+
IncompleteReadError = Class.new(Gearman::Error)
|
13
|
+
IncompleteWriteError = Class.new(Gearman::Error)
|
14
|
+
NoConnectionError = Class.new(Gearman::Error)
|
15
|
+
UnexpectedPacketError = Class.new(Gearman::Error)
|
16
|
+
ServerError = Class.new(Gearman::Error)
|
17
|
+
|
18
|
+
NULL_BYTE = "\0"
|
19
|
+
REQ = [NULL_BYTE, "REQ"].join
|
20
|
+
HEADER_FORMAT = "a4NN"
|
21
|
+
HEADER_SIZE = 12
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(address)
|
25
|
+
@address = address
|
26
|
+
@repository = Packet::Repository.new
|
27
|
+
@socket = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def write(packet)
|
31
|
+
connect if disconnected?
|
32
|
+
|
33
|
+
body = packet.arguments.join(NULL_BYTE)
|
34
|
+
header = [REQ, packet.number, body.size].pack(HEADER_FORMAT)
|
35
|
+
|
36
|
+
serialized_packet = header + body
|
37
|
+
|
38
|
+
length_written = @socket.write(serialized_packet)
|
39
|
+
|
40
|
+
debug "Wrote #{packet.inspect}"
|
41
|
+
|
42
|
+
if length_written != serialized_packet.length
|
43
|
+
lengths = [serialized_packet.length, lengths]
|
44
|
+
message = "expected to write %d bytes, but only read %d" % lengths
|
45
|
+
|
46
|
+
raise IncompleteWriteError, message
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def next(*expected_packet_types)
|
51
|
+
connect if disconnected?
|
52
|
+
|
53
|
+
header = read(HEADER_SIZE)
|
54
|
+
magic, type, length = header.unpack(HEADER_FORMAT)
|
55
|
+
|
56
|
+
body = read(length)
|
57
|
+
arguments = String(body).split(NULL_BYTE)
|
58
|
+
|
59
|
+
@repository.load(type).new(arguments).tap do |packet|
|
60
|
+
debug "Read #{packet.inspect}"
|
61
|
+
|
62
|
+
if packet.is_a?(Packet::ERROR)
|
63
|
+
message = "server sent error #{packet.error_code}: #{packet.text}"
|
64
|
+
|
65
|
+
raise ServerError, message
|
66
|
+
end
|
67
|
+
|
68
|
+
verify packet, expected_packet_types
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def disconnect
|
73
|
+
if @socket
|
74
|
+
@socket.close unless @socket.closed?
|
75
|
+
@socket = nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def read(length)
|
82
|
+
return unless length > 0
|
83
|
+
|
84
|
+
data = @socket.read(length)
|
85
|
+
|
86
|
+
if data.nil?
|
87
|
+
raise NoConnectionError, "lost connection to #{@address}"
|
88
|
+
elsif data.length != length
|
89
|
+
lengths = [length, data.length]
|
90
|
+
message = "expected to read %d bytes, but only read %d" % lengths
|
91
|
+
|
92
|
+
raise IncompleteReadError, message
|
93
|
+
else
|
94
|
+
data
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def connect
|
99
|
+
begin
|
100
|
+
@socket = TCPSocket.new(@address.host, @address.port)
|
101
|
+
|
102
|
+
info "Connected"
|
103
|
+
rescue => error
|
104
|
+
raise NoConnectionError.new("could not connect to #{@address}", error)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def disconnected?
|
109
|
+
@socket.nil?
|
110
|
+
end
|
111
|
+
|
112
|
+
def verify(packet, valid_packet_types)
|
113
|
+
return if valid_packet_types.empty?
|
114
|
+
|
115
|
+
unless valid_packet_types.include?(packet.class)
|
116
|
+
valid_type = valid_packet_types.join(' or ')
|
117
|
+
message = "expected #{packet} to be a #{valid_type}"
|
118
|
+
|
119
|
+
raise UnexpectedPacketError, message
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def debug(note)
|
124
|
+
Celluloid.logger.debug "#{@address}: #{note}"
|
125
|
+
end
|
126
|
+
|
127
|
+
def info(note)
|
128
|
+
Celluloid.logger.info "#{@address}: #{note}"
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'gearman/packet/sugar'
|
2
|
+
|
3
|
+
module Gearman
|
4
|
+
module Packet
|
5
|
+
class Repository
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@by_number = {}
|
9
|
+
store(1, 'CAN_DO', [:function_name])
|
10
|
+
store(2, 'CANT_DO', [:function_name])
|
11
|
+
store(4, 'PRE_SLEEP')
|
12
|
+
store(6, 'NOOP')
|
13
|
+
store(8, 'JOB_CREATED', [:handle])
|
14
|
+
store(9, 'GRAB_JOB')
|
15
|
+
store(10, 'NO_JOB')
|
16
|
+
store(11, 'JOB_ASSIGN', [:handle, :function_name, :data])
|
17
|
+
store(13, 'WORK_COMPLETE', [:handle, :data])
|
18
|
+
store(18, 'SUBMIT_JOB_BG', [:function_name, :unique_id, :data])
|
19
|
+
store(19, 'ERROR', [:error_code, :text])
|
20
|
+
store(25, 'WORK_EXCEPTION', [:handle, :data])
|
21
|
+
end
|
22
|
+
|
23
|
+
def store(number, type, takes = [])
|
24
|
+
Sugar.type(type, number: number, takes: takes).tap do |packet_type|
|
25
|
+
@by_number[number] = packet_type
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def load(number)
|
30
|
+
@by_number[number]
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Gearman
|
2
|
+
module Packet
|
3
|
+
module Sugar
|
4
|
+
|
5
|
+
def self.type(name, options)
|
6
|
+
if Gearman::Packet.const_defined?(name)
|
7
|
+
return Gearman::Packet.const_get(name)
|
8
|
+
end
|
9
|
+
|
10
|
+
class_ = Class.new do
|
11
|
+
extend Sugar
|
12
|
+
|
13
|
+
takes *Array(options[:takes])
|
14
|
+
number Integer(options[:number])
|
15
|
+
|
16
|
+
def inspect
|
17
|
+
info = argument_names.map do |argument_name|
|
18
|
+
"#{argument_name}=#{public_send(argument_name)}"
|
19
|
+
end
|
20
|
+
|
21
|
+
if info.any?
|
22
|
+
"#<#{self.class.name} #{info.join(' ')}>"
|
23
|
+
else
|
24
|
+
"#<#{self.class.name}>"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def ==(other)
|
29
|
+
quack = other.respond_to?(:arguments) && other.respond_to?(:number)
|
30
|
+
|
31
|
+
if quack
|
32
|
+
arguments == other.arguments && number == other.number
|
33
|
+
else
|
34
|
+
false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
alias :eql? :==
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
Gearman::Packet.const_set(name, class_)
|
42
|
+
end
|
43
|
+
|
44
|
+
def takes(*arguments)
|
45
|
+
define_method(:initialize) do |attributes_or_arguments = []|
|
46
|
+
__sugar_set = ->(argument, value) do
|
47
|
+
ivar = "@#{argument}"
|
48
|
+
|
49
|
+
instance_variable_set(ivar, value)
|
50
|
+
end
|
51
|
+
|
52
|
+
if attributes_or_arguments.is_a?(Hash)
|
53
|
+
attributes = attributes_or_arguments
|
54
|
+
arguments.each do |argument|
|
55
|
+
begin
|
56
|
+
value = attributes.fetch(argument.to_sym)
|
57
|
+
rescue KeyError
|
58
|
+
raise ArgumentError, "expected to be given :#{argument}"
|
59
|
+
end
|
60
|
+
|
61
|
+
__sugar_set.(argument, value)
|
62
|
+
end
|
63
|
+
elsif attributes_or_arguments.is_a?(Array)
|
64
|
+
given = attributes_or_arguments
|
65
|
+
|
66
|
+
if given.size != arguments.size
|
67
|
+
raise ArgumentError,
|
68
|
+
"expected to be given #{arguments.size} arguments"
|
69
|
+
end
|
70
|
+
|
71
|
+
arguments.zip(given).each do |argument, value|
|
72
|
+
__sugar_set.(argument, value)
|
73
|
+
end
|
74
|
+
else
|
75
|
+
raise ArgumentError,
|
76
|
+
"expected either a Hash of attributes or an Array of arguments"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
attr_reader(*arguments)
|
81
|
+
|
82
|
+
define_method(:argument_names) do
|
83
|
+
self.class.const_get('ARGUMENTS')
|
84
|
+
end
|
85
|
+
|
86
|
+
define_method(:arguments) do
|
87
|
+
argument_names.map { |argument_name| public_send(argument_name) }
|
88
|
+
end
|
89
|
+
|
90
|
+
self.const_set('ARGUMENTS', arguments)
|
91
|
+
end
|
92
|
+
|
93
|
+
def number(n)
|
94
|
+
define_method(:number) do
|
95
|
+
self.class.const_get('NUMBER')
|
96
|
+
end
|
97
|
+
|
98
|
+
self.const_set('NUMBER', n)
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|