geary 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift(
4
+ File.expand_path(File.join(File.expand_path(__FILE__), '..', '..', 'lib'))
5
+ )
6
+
7
+ require 'geary/cli'
8
+
9
+ Geary::CLI.new(ARGV.dup).execute!
@@ -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,7 @@
1
+ require 'nestegg'
2
+
3
+ module Gearman
4
+ class Error < StandardError
5
+ include Nestegg::NestingException
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ require 'gearman/packet/repository'
2
+
3
+ module Gearman
4
+ module Packet
5
+ self.tap do
6
+ Repository.new
7
+ end
8
+ end
9
+ 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