geary 0.0.1

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