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
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'celluloid'
|
2
|
+
require 'gearman/connection'
|
3
|
+
require 'gearman/packet'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module Gearman
|
7
|
+
class Worker
|
8
|
+
include Celluloid
|
9
|
+
|
10
|
+
trap_exit :reconnect
|
11
|
+
finalizer :disconnect
|
12
|
+
|
13
|
+
def initialize(address)
|
14
|
+
@address = URI(address)
|
15
|
+
configure_connection Connection.method(:new_link)
|
16
|
+
end
|
17
|
+
|
18
|
+
def can_do(ability)
|
19
|
+
@connection.write(Packet::CAN_DO.new(function_name: ability))
|
20
|
+
end
|
21
|
+
|
22
|
+
def pre_sleep
|
23
|
+
@connection.write(Packet::PRE_SLEEP.new)
|
24
|
+
@connection.next(Packet::NOOP)
|
25
|
+
end
|
26
|
+
|
27
|
+
def grab_job
|
28
|
+
@connection.write(Packet::GRAB_JOB.new)
|
29
|
+
@connection.next(Packet::JOB_ASSIGN, Packet::NO_JOB)
|
30
|
+
end
|
31
|
+
|
32
|
+
def work_exception(handle, data)
|
33
|
+
@connection.write(Packet::WORK_EXCEPTION.new(handle: handle, data: data))
|
34
|
+
end
|
35
|
+
|
36
|
+
def work_complete(handle, data)
|
37
|
+
@connection.write(Packet::WORK_COMPLETE.new(handle: handle, data: data))
|
38
|
+
end
|
39
|
+
|
40
|
+
def disconnect
|
41
|
+
if @connection
|
42
|
+
@connection.terminate if @connection.alive?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_connection
|
47
|
+
@connection = @connect.call(@address)
|
48
|
+
end
|
49
|
+
|
50
|
+
def reconnect(*_)
|
51
|
+
disconnect
|
52
|
+
build_connection
|
53
|
+
end
|
54
|
+
|
55
|
+
def configure_connection(connection_routine)
|
56
|
+
@connect = connection_routine
|
57
|
+
reconnect
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
data/lib/geary.rb
ADDED
data/lib/geary/cli.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
require 'celluloid'
|
4
|
+
|
5
|
+
require 'geary/option_parser'
|
6
|
+
require 'geary/manager'
|
7
|
+
|
8
|
+
module Geary
|
9
|
+
|
10
|
+
class CLI
|
11
|
+
|
12
|
+
Shutdown = Class.new(StandardError) unless defined? Shutdown
|
13
|
+
|
14
|
+
attr_reader :configuration, :internal_signal_queue, :external_signal_queue
|
15
|
+
|
16
|
+
def initialize(argv, stdout = STDOUT, stderr = STDERR, kernel = Kernel,
|
17
|
+
pipe = IO.pipe)
|
18
|
+
@argv = argv
|
19
|
+
@stdout = stdout
|
20
|
+
@stdout = stderr
|
21
|
+
@kernel = kernel
|
22
|
+
@internal_signal_queue, @external_signal_queue = pipe
|
23
|
+
@configuration = OptionParser.new.parse(@argv)
|
24
|
+
end
|
25
|
+
|
26
|
+
def execute!
|
27
|
+
Celluloid.logger.level = configuration.log_level
|
28
|
+
|
29
|
+
%w(INT TERM).each do |signal|
|
30
|
+
trap signal do
|
31
|
+
external_signal_queue.puts(signal)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
munge_environment_given(configuration)
|
36
|
+
load_rails
|
37
|
+
|
38
|
+
manager = Manager.new(configuration: configuration)
|
39
|
+
manager.start
|
40
|
+
|
41
|
+
begin
|
42
|
+
loop do
|
43
|
+
IO.select([internal_signal_queue])
|
44
|
+
signal = internal_signal_queue.gets.strip
|
45
|
+
|
46
|
+
handle(signal)
|
47
|
+
end
|
48
|
+
rescue Shutdown
|
49
|
+
manager.async.stop
|
50
|
+
|
51
|
+
manager.wait(:done)
|
52
|
+
|
53
|
+
@kernel.exit(0)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle(signal)
|
58
|
+
if %w(INT TERM).include?(signal)
|
59
|
+
raise Shutdown
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def munge_environment_given(configuration)
|
66
|
+
$:.concat(configuration.included_paths)
|
67
|
+
configuration.required_files.each { |file| require file }
|
68
|
+
end
|
69
|
+
|
70
|
+
def load_rails
|
71
|
+
begin
|
72
|
+
require 'rails'
|
73
|
+
rescue LoadError
|
74
|
+
Celluloid.logger.debug "Unable to load Rails"
|
75
|
+
else
|
76
|
+
Celluloid.logger.debug "Loading Rails"
|
77
|
+
|
78
|
+
require 'geary/railtie'
|
79
|
+
require 'config/environment'
|
80
|
+
|
81
|
+
::Rails.application.eager_load!
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'virtus'
|
3
|
+
require 'virtus/uri'
|
4
|
+
|
5
|
+
module Geary
|
6
|
+
class Configuration
|
7
|
+
include Virtus
|
8
|
+
|
9
|
+
attribute :server_addresses, Array[URI], default: ['gearman://localhost:4730']
|
10
|
+
attribute :concurrency, Integer, default: 25
|
11
|
+
attribute :included_paths, Array, default: %w(.)
|
12
|
+
attribute :required_files, Array, default: []
|
13
|
+
attribute :failure_monitor_interval, Integer, default: 1
|
14
|
+
attribute :jitter, Float, default: 0.01
|
15
|
+
attribute :log_level, Object, default: Logger::INFO
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
data/lib/geary/error.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'celluloid'
|
2
|
+
require 'geary/error'
|
3
|
+
require 'geary/performer'
|
4
|
+
|
5
|
+
module Geary
|
6
|
+
class Manager
|
7
|
+
include Celluloid
|
8
|
+
|
9
|
+
UnexpectedRestart = Class.new(Error) unless defined? UnexpectedRestart
|
10
|
+
|
11
|
+
attr_reader :configuration, :performers
|
12
|
+
|
13
|
+
trap_exit :performer_crashed
|
14
|
+
|
15
|
+
def initialize(options = {})
|
16
|
+
@configuration = options.fetch(:configuration)
|
17
|
+
@performer_type = options.fetch(:performer_type, Performer)
|
18
|
+
@performers = []
|
19
|
+
@crashes = []
|
20
|
+
@server_addresses_by_performer = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
async.monitor_crashes
|
25
|
+
|
26
|
+
configuration.server_addresses.each do |server_address|
|
27
|
+
configuration.concurrency.times do
|
28
|
+
start_performer(server_address)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def stop
|
34
|
+
@performers.select(&:alive?).each(&:terminate)
|
35
|
+
|
36
|
+
after(0) { signal(:done) }
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def monitor_crashes
|
42
|
+
every(configuration.failure_monitor_interval) do
|
43
|
+
@crashes.reject! do |server_address|
|
44
|
+
momentarily { start_performer(server_address) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def performer_crashed(performer, reason)
|
50
|
+
if String(reason).size > 0
|
51
|
+
forget_performer(performer) do |server_address|
|
52
|
+
@crashes.unshift(server_address)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def forget_performer(performer, &wants_server_address)
|
58
|
+
_id = performer.object_id
|
59
|
+
|
60
|
+
@performers.delete(performer) do
|
61
|
+
raise UnexpectedRestart, "we don't know about Performer #{_id}"
|
62
|
+
end
|
63
|
+
|
64
|
+
server_address = @server_addresses_by_performer.delete(_id)
|
65
|
+
|
66
|
+
wants_server_address.call(server_address)
|
67
|
+
end
|
68
|
+
|
69
|
+
def start_performer(server_address)
|
70
|
+
performer = @performer_type.new_link(server_address)
|
71
|
+
|
72
|
+
@performers << performer
|
73
|
+
@server_addresses_by_performer[performer.object_id] = server_address
|
74
|
+
|
75
|
+
performer.async.start
|
76
|
+
end
|
77
|
+
|
78
|
+
def momentarily(&action)
|
79
|
+
after(rand + configuration.jitter, &action)
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
require 'geary/configuration'
|
4
|
+
|
5
|
+
module Geary
|
6
|
+
class OptionParser
|
7
|
+
|
8
|
+
def parse(args)
|
9
|
+
Configuration.new.tap do |configuration|
|
10
|
+
parser_which_configures(configuration).parse!(Array(args))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def parser_which_configures(configuration)
|
15
|
+
::OptionParser.new do |parser|
|
16
|
+
parser.on('-s', '--server SERVERS', Array) do |server_addresses|
|
17
|
+
configuration.server_addresses = server_addresses
|
18
|
+
end
|
19
|
+
|
20
|
+
parser.on('-r', '--require FILES', Array) do |files|
|
21
|
+
configuration.required_files = files
|
22
|
+
end
|
23
|
+
|
24
|
+
parser.on('-I', '--include PATHS', Array) do |paths|
|
25
|
+
configuration.included_paths = paths
|
26
|
+
end
|
27
|
+
|
28
|
+
parser.on('-c', '--concurrency NUMBER', 'number of concurrent tasks to run per server') do |number|
|
29
|
+
configuration.concurrency = Integer(number)
|
30
|
+
end
|
31
|
+
|
32
|
+
parser.on('-l', '--level LOG_LEVEL', 'log level (FATAL|ERROR|WARN|INFO|DEBUG)') do |level|
|
33
|
+
begin
|
34
|
+
configuration.log_level = Logger.const_get(String(level).upcase)
|
35
|
+
rescue NameError
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'celluloid'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'gearman/worker'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Geary
|
7
|
+
|
8
|
+
class Performer
|
9
|
+
include Celluloid
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
finalizer :disconnect
|
13
|
+
trap_exit :reconnect
|
14
|
+
|
15
|
+
def initialize(address)
|
16
|
+
@address = address
|
17
|
+
configure_connection Gearman::Worker.method(:new_link)
|
18
|
+
end
|
19
|
+
|
20
|
+
def start
|
21
|
+
@gearman.can_do('Geary.default')
|
22
|
+
|
23
|
+
loop do
|
24
|
+
packet = @gearman.grab_job
|
25
|
+
|
26
|
+
case packet
|
27
|
+
when Gearman::Packet::JOB_ASSIGN
|
28
|
+
perform(packet)
|
29
|
+
when Gearman::Packet::NO_JOB
|
30
|
+
@gearman.pre_sleep
|
31
|
+
else
|
32
|
+
break
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def perform(packet)
|
38
|
+
job = JSON.parse(packet.data)
|
39
|
+
job_result = nil
|
40
|
+
|
41
|
+
begin
|
42
|
+
worker = ::Object.const_get(job['class']).new
|
43
|
+
|
44
|
+
job_result = worker.perform(*job['args'])
|
45
|
+
rescue => error
|
46
|
+
@gearman.async.work_exception(packet.handle, error.message)
|
47
|
+
else
|
48
|
+
@gearman.async.work_complete(packet.handle, job_result)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def disconnect
|
53
|
+
if @gearman
|
54
|
+
@gearman.terminate if @gearman.alive?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def reconnect(actor, reason)
|
59
|
+
disconnect
|
60
|
+
build_connection
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_connection
|
64
|
+
@gearman = @connect.call(@address)
|
65
|
+
end
|
66
|
+
|
67
|
+
def configure_connection(connection_routine)
|
68
|
+
@connect = connection_routine
|
69
|
+
reconnect(current_actor, nil)
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
data/lib/geary/worker.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'gearman/client'
|
3
|
+
require 'geary/error'
|
4
|
+
|
5
|
+
module Geary
|
6
|
+
module Worker
|
7
|
+
|
8
|
+
def perform_async(*args)
|
9
|
+
payload = payload_for(args)
|
10
|
+
|
11
|
+
operation do |gearman|
|
12
|
+
gearman.submit_job_bg('Geary.default', payload.to_json)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def use_gearman_client(*args)
|
19
|
+
@gearman_client = Gearman::Client.new(*args)
|
20
|
+
end
|
21
|
+
|
22
|
+
def gearman_client
|
23
|
+
@gearman_client || use_gearman_client('gearman://localhost:4730')
|
24
|
+
end
|
25
|
+
|
26
|
+
def payload_for(args)
|
27
|
+
payload = { class: self.to_s, args: args }
|
28
|
+
end
|
29
|
+
|
30
|
+
def operation(&block)
|
31
|
+
attempts = 0
|
32
|
+
failure_threshold = 1
|
33
|
+
|
34
|
+
begin
|
35
|
+
block.call(gearman_client)
|
36
|
+
rescue
|
37
|
+
attempts += 1
|
38
|
+
|
39
|
+
if attempts > failure_threshold
|
40
|
+
raise Error
|
41
|
+
else
|
42
|
+
gearman_client.reconnect
|
43
|
+
retry
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|