geary 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,8 @@
1
+ require 'geary/worker'
2
+
3
+ module Geary
4
+ end
5
+
6
+ if defined?(Rails)
7
+ require 'geary/railtie'
8
+ end
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
@@ -0,0 +1,7 @@
1
+ require 'nestegg'
2
+
3
+ module Geary
4
+ class Error < StandardError
5
+ include Nestegg::NestingException
6
+ end
7
+ end
@@ -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
@@ -0,0 +1,9 @@
1
+ module Geary
2
+ class Railtie < Rails::Railtie
3
+
4
+ config.before_initialize do
5
+ Rails.application.paths.add 'app/workers', eager_load: true
6
+ end
7
+
8
+ end
9
+ end
@@ -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