gearman-ruby 3.0.8 → 4.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/.travis.yml +5 -0
- data/CHANGELOG.md +6 -4
- data/README.md +111 -0
- data/examples/client.rb +1 -2
- data/examples/client_reverse_nohost.rb +30 -0
- data/examples/{client_reverse.rb → client_reverse_wait.rb} +9 -11
- data/examples/worker.rb +8 -5
- data/examples/worker_reverse_string.rb +12 -17
- data/gearman-ruby.gemspec +3 -5
- data/lib/gearman.rb +17 -77
- data/lib/gearman/client.rb +129 -147
- data/lib/gearman/connection.rb +158 -0
- data/lib/gearman/connection_pool.rb +131 -0
- data/lib/gearman/exceptions.rb +24 -0
- data/lib/gearman/logging.rb +19 -0
- data/lib/gearman/packet.rb +61 -0
- data/lib/gearman/task.rb +1 -1
- data/lib/gearman/task_set.rb +67 -0
- data/lib/gearman/version.rb +1 -1
- data/lib/gearman/worker.rb +185 -412
- data/lib/gearman/worker/ability.rb +55 -0
- data/lib/gearman/worker/callbacks.rb +39 -0
- data/lib/gearman/worker/job.rb +44 -0
- data/spec/client_spec.rb +32 -20
- data/spec/connection_pool_spec.rb +55 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/task_spec.rb +10 -0
- data/spec/taskset_spec.rb +2 -2
- metadata +18 -37
- data/HOWTO +0 -146
- data/README +0 -9
- data/TODO +0 -8
- data/VERSION.yml +0 -4
- data/examples/calculus_client.rb +0 -39
- data/examples/calculus_worker.rb +0 -45
- data/examples/client.php +0 -23
- data/examples/client_background.rb +0 -14
- data/examples/client_data.rb +0 -16
- data/examples/client_epoch.rb +0 -23
- data/examples/client_exception.rb +0 -19
- data/examples/client_prefix.rb +0 -17
- data/examples/gearman_environment.sh +0 -25
- data/examples/scale_image.rb +0 -31
- data/examples/scale_image_worker.rb +0 -34
- data/examples/server.rb +0 -15
- data/examples/worker_data.rb +0 -16
- data/examples/worker_exception.rb +0 -14
- data/examples/worker_prefix.rb +0 -25
- data/examples/worker_reverse_to_file.rb +0 -18
- data/examples/worker_signals.rb +0 -36
- data/lib/gearman/taskset.rb +0 -293
- data/lib/gearman/util.rb +0 -211
- data/spec/util_spec.rb +0 -67
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Gearman
|
4
|
+
class Connection
|
5
|
+
include Logging
|
6
|
+
|
7
|
+
def initialize(hostname, port)
|
8
|
+
@hostname = hostname
|
9
|
+
@port = port
|
10
|
+
@real_socket = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :hostname, :port, :state, :socket
|
14
|
+
|
15
|
+
##
|
16
|
+
# Check server health status by sending an ECHO request
|
17
|
+
# Return true / false
|
18
|
+
##
|
19
|
+
def is_healthy?
|
20
|
+
if @real_socket == nil
|
21
|
+
logger.debug "Performing health check for #{self}"
|
22
|
+
begin
|
23
|
+
request = Packet.pack_request("echo_req", "ping")
|
24
|
+
response = send_request(request, 3)
|
25
|
+
logger.debug "Health check response for #{self} is #{response.inspect}"
|
26
|
+
raise ProtocolError unless response[0] == :echo_res and response[1] == "ping"
|
27
|
+
return true
|
28
|
+
rescue NetworkError
|
29
|
+
logger.debug "NetworkError -- unhealthy"
|
30
|
+
return false
|
31
|
+
rescue ProtocolError
|
32
|
+
logger.debug "ProtocolError -- unhealthy"
|
33
|
+
return false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# @param num_retries Number of times to retry
|
40
|
+
# @return This connection's Socket
|
41
|
+
##
|
42
|
+
def socket(num_retries=3)
|
43
|
+
# If we already have an open socket to this host, return it.
|
44
|
+
return @real_socket if @real_socket
|
45
|
+
num_retries.times do |i|
|
46
|
+
begin
|
47
|
+
logger.debug("Attempt ##{i} to connect to #{hostname}:#{port}")
|
48
|
+
@real_socket = TCPSocket.new(hostname, port)
|
49
|
+
rescue Exception => e
|
50
|
+
logger.error("Unable to connect: #{e}")
|
51
|
+
# Swallow error so we can retry -> num_retries times
|
52
|
+
else
|
53
|
+
return @real_socket
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
raise_exception("Unable to connect to job server #{hostname}:#{port}")
|
58
|
+
end
|
59
|
+
|
60
|
+
def close_socket
|
61
|
+
@real_socket.close if @real_socket
|
62
|
+
@real_socket = nil
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
def raise_exception(message)
|
67
|
+
close_socket
|
68
|
+
raise NetworkError, message
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# Read from a socket, giving up if it doesn't finish quickly enough.
|
73
|
+
# NetworkError is thrown if we don't read all the bytes in time.
|
74
|
+
#
|
75
|
+
# @param sock Socket from which we read
|
76
|
+
# @param len number of bytes to read
|
77
|
+
# @param timeout maximum number of seconds we'll take; nil for no timeout
|
78
|
+
# @return full data that was read
|
79
|
+
def timed_recv(sock, len, timeout=nil)
|
80
|
+
data = ''
|
81
|
+
start_time = Time.now.to_f
|
82
|
+
end_time = Time.now.to_f + timeout if timeout
|
83
|
+
while data.size < len and (not timeout or Time.now.to_f < end_time) do
|
84
|
+
IO::select([sock], nil, nil, timeout ? end_time - Time.now.to_f : nil) \
|
85
|
+
or break
|
86
|
+
begin
|
87
|
+
data += sock.readpartial(len - data.size)
|
88
|
+
rescue
|
89
|
+
close_socket
|
90
|
+
raise NetworkError, "Unable to read data from socket."
|
91
|
+
end
|
92
|
+
end
|
93
|
+
if data.size < len
|
94
|
+
now = Time.now.to_f
|
95
|
+
if now > end_time
|
96
|
+
time_lapse = now - start_time
|
97
|
+
raise SocketTimeoutError, "Took too long to read data: #{time_lapse} sec. to read on a #{timeout} sec. timeout"
|
98
|
+
else
|
99
|
+
raise_exception("Read #{data.size} byte(s) instead of #{len}")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
data
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Read a response packet from a socket.
|
107
|
+
#
|
108
|
+
# @param sock Socket connected to a job server
|
109
|
+
# @param timeout timeout in seconds, nil for no timeout
|
110
|
+
# @return array consisting of integer packet type and data
|
111
|
+
def read_response(timeout=nil)
|
112
|
+
end_time = Time.now.to_f + timeout if timeout
|
113
|
+
head = timed_recv(socket, 12, timeout)
|
114
|
+
magic, type, len = head.unpack('a4NN')
|
115
|
+
raise ProtocolError, "Invalid magic '#{magic}'" unless magic == "\0RES"
|
116
|
+
buf = len > 0 ?
|
117
|
+
timed_recv(socket, len, timeout ? end_time - Time.now.to_f : nil) : ''
|
118
|
+
type = Packet::COMMANDS[type]
|
119
|
+
raise ProtocolError, "Invalid packet type #{type}" unless type
|
120
|
+
[type, buf]
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# Send a request packet over a socket that needs a response.
|
125
|
+
#
|
126
|
+
# @param sock Socket connected to a job server
|
127
|
+
# @param req request packet to send
|
128
|
+
# @result response from server
|
129
|
+
def send_request(req, timeout = nil)
|
130
|
+
send_update(req, timeout)
|
131
|
+
return read_response(timeout)
|
132
|
+
end
|
133
|
+
|
134
|
+
def send_update(req, timeout = nil)
|
135
|
+
len = with_safe_socket_op{ socket.write(req) }
|
136
|
+
if len != req.size
|
137
|
+
raise_exception("Wrote #{len} instead of #{req.size}")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def with_safe_socket_op
|
142
|
+
begin
|
143
|
+
yield
|
144
|
+
rescue Exception => ex
|
145
|
+
raise_exception(ex.message)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def to_host_port
|
150
|
+
"#{hostname}:#{port}"
|
151
|
+
end
|
152
|
+
|
153
|
+
def to_s
|
154
|
+
"#{hostname}:#{port} (connected: #{@real_socket != nil})"
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Gearman
|
4
|
+
class ConnectionPool
|
5
|
+
include Logging
|
6
|
+
|
7
|
+
DEFAULT_PORT = 4730
|
8
|
+
|
9
|
+
def initialize(servers = [])
|
10
|
+
@bad_servers = []
|
11
|
+
@coalesce_connections = {}
|
12
|
+
@connection_handler = nil
|
13
|
+
@job_servers = []
|
14
|
+
@reconnect_seconds = 10
|
15
|
+
@server_counter = 0 # Round-robin distribution of requests
|
16
|
+
@servers_mutex = Mutex.new
|
17
|
+
|
18
|
+
add_servers(servers)
|
19
|
+
start_reconnect_thread
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_connection(connection)
|
23
|
+
@servers_mutex.synchronize do
|
24
|
+
if connection.is_healthy?
|
25
|
+
activate_connection(connection)
|
26
|
+
else
|
27
|
+
deactivate_connection(connection)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_host_port(host_port)
|
33
|
+
host, port = host_port.split(":")
|
34
|
+
connection = Connection.new(host, port.to_i)
|
35
|
+
add_connection(connection)
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_servers(servers)
|
39
|
+
if servers.class == String or servers.class == Symbol
|
40
|
+
servers = [ servers.to_s ]
|
41
|
+
end
|
42
|
+
|
43
|
+
servers = servers.map {|s| s =~ /:/ ? s : "#{s}:#{DEFAULT_PORT}" }
|
44
|
+
|
45
|
+
servers.each do |host_port|
|
46
|
+
add_host_port(host_port)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_connection(coalesce_key = nil)
|
51
|
+
@servers_mutex.synchronize do
|
52
|
+
|
53
|
+
logger.debug "Available job servers: #{@job_servers.inspect}"
|
54
|
+
raise NoJobServersError if @job_servers.empty?
|
55
|
+
@server_counter += 1
|
56
|
+
@job_servers[@server_counter % @job_servers.size]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def on_connection(&block)
|
61
|
+
@connection_handler = block
|
62
|
+
end
|
63
|
+
|
64
|
+
def poll_connections(timeout = nil)
|
65
|
+
available_sockets = []
|
66
|
+
@servers_mutex.synchronize do
|
67
|
+
available_sockets.concat @job_servers.collect { |conn| conn.socket }
|
68
|
+
end
|
69
|
+
if available_sockets.size > 0
|
70
|
+
logger.debug "Polling on #{available_sockets.size} available server(s) with a #{timeout} second timeout"
|
71
|
+
IO::select(available_sockets, nil, nil, timeout)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def with_all_connections(&block)
|
76
|
+
@servers_mutex.synchronize do
|
77
|
+
@job_servers.each do |connection|
|
78
|
+
begin
|
79
|
+
block.call(connection)
|
80
|
+
rescue NetworkError => ex
|
81
|
+
logger.debug "Error with #{connection}, marking as bad"
|
82
|
+
deactivate_connection(connection)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def deactivate_connection(connection)
|
92
|
+
@job_servers.reject! { |c| c == connection }
|
93
|
+
@bad_servers << connection
|
94
|
+
end
|
95
|
+
|
96
|
+
def activate_connection(connection)
|
97
|
+
@bad_servers.reject! { |c| c == connection }
|
98
|
+
@job_servers << connection
|
99
|
+
@connection_handler.call(connection) if @connection_handler
|
100
|
+
end
|
101
|
+
|
102
|
+
def start_reconnect_thread
|
103
|
+
Thread.new do
|
104
|
+
loop do
|
105
|
+
@servers_mutex.synchronize do
|
106
|
+
# If there are any failed servers, try to reconnect to them.
|
107
|
+
update_job_servers unless @bad_servers.empty?
|
108
|
+
end
|
109
|
+
sleep @reconnect_seconds
|
110
|
+
end
|
111
|
+
end.run
|
112
|
+
end
|
113
|
+
|
114
|
+
def update_job_servers
|
115
|
+
logger.debug "Found #{@bad_servers.size} zombie connections, checking pulse."
|
116
|
+
@bad_servers.each do |connection|
|
117
|
+
begin
|
118
|
+
message = "Testing server #{connection}..."
|
119
|
+
if connection.is_healthy?
|
120
|
+
logger.debug "#{message} Connection is healthy, putting back into service"
|
121
|
+
activate_connection(connection)
|
122
|
+
else
|
123
|
+
logger.debug "#{message} Still down."
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Gearman
|
2
|
+
|
3
|
+
class InvalidArgsError < Exception
|
4
|
+
end
|
5
|
+
|
6
|
+
class ProtocolError < Exception
|
7
|
+
end
|
8
|
+
|
9
|
+
class NetworkError < Exception
|
10
|
+
end
|
11
|
+
|
12
|
+
class NoJobServersError < Exception
|
13
|
+
end
|
14
|
+
|
15
|
+
class JobQueueError < Exception
|
16
|
+
end
|
17
|
+
|
18
|
+
class SocketTimeoutError < Exception
|
19
|
+
end
|
20
|
+
|
21
|
+
class ServerDownException < Exception
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Gearman
|
2
|
+
class Packet
|
3
|
+
# Map from Integer representations of commands used in the network
|
4
|
+
# protocol to more-convenient symbols.
|
5
|
+
COMMANDS = {
|
6
|
+
1 => :can_do, # W->J: FUNC
|
7
|
+
2 => :cant_do, # W->J: FUNC
|
8
|
+
3 => :reset_abilities, # W->J: --
|
9
|
+
4 => :pre_sleep, # W->J: --
|
10
|
+
#5 => (unused), # - -
|
11
|
+
6 => :noop, # J->W: --
|
12
|
+
7 => :submit_job, # C->J: FUNC[0]UNIQ[0]ARGS
|
13
|
+
8 => :job_created, # J->C: HANDLE
|
14
|
+
9 => :grab_job, # W->J: --
|
15
|
+
10 => :no_job, # J->W: --
|
16
|
+
11 => :job_assign, # J->W: HANDLE[0]FUNC[0]ARG
|
17
|
+
12 => :work_status, # W->J/C: HANDLE[0]NUMERATOR[0]DENOMINATOR
|
18
|
+
13 => :work_complete, # W->J/C: HANDLE[0]RES
|
19
|
+
14 => :work_fail, # W->J/C: HANDLE
|
20
|
+
15 => :get_status, # C->J: HANDLE
|
21
|
+
16 => :echo_req, # ?->J: TEXT
|
22
|
+
17 => :echo_res, # J->?: TEXT
|
23
|
+
18 => :submit_job_bg, # C->J: FUNC[0]UNIQ[0]ARGS
|
24
|
+
19 => :error, # J->?: ERRCODE[0]ERR_TEXT
|
25
|
+
20 => :status_res, # C->J: HANDLE[0]KNOWN[0]RUNNING[0]NUM[0]DENOM
|
26
|
+
21 => :submit_job_high, # C->J: FUNC[0]UNIQ[0]ARGS
|
27
|
+
22 => :set_client_id, # W->J: [RANDOM_STRING_NO_WHITESPACE]
|
28
|
+
23 => :can_do_timeout, # W->J: FUNC[0]TIMEOUT
|
29
|
+
24 => :all_yours, # REQ Worker
|
30
|
+
25 => :work_exception, # W->J: HANDLE[0]ARG
|
31
|
+
26 => :option_req, # C->J: TEXT
|
32
|
+
27 => :option_res, # J->C: TEXT
|
33
|
+
28 => :work_data, # REQ Worker
|
34
|
+
29 => :work_warning, # W->J/C: HANDLE[0]MSG
|
35
|
+
30 => :grab_job_uniq, # REQ Worker
|
36
|
+
31 => :job_assign_uniq, # RES Worker
|
37
|
+
32 => :submit_job_high_bg, # C->J: FUNC[0]UNIQ[0]ARGS
|
38
|
+
33 => :submit_job_low, # C->J: FUNC[0]UNIQ[0]ARGS
|
39
|
+
34 => :submit_job_low_bg, # C->J: FUNC[0]UNIQ[0]ARGS
|
40
|
+
35 => :submit_job_sched, # REQ Client
|
41
|
+
36 => :submit_job_epoch # C->J: FUNC[0]UNIQ[0]EPOCH[0]ARGS
|
42
|
+
}
|
43
|
+
|
44
|
+
# Map e.g. 'can_do' => 1
|
45
|
+
NUMS = COMMANDS.invert
|
46
|
+
|
47
|
+
##
|
48
|
+
# Construct a request packet.
|
49
|
+
#
|
50
|
+
# @param type_name command type's name (see COMMANDS)
|
51
|
+
# @param arg optional data to pack into the command
|
52
|
+
# @return packet (as a string)
|
53
|
+
def Packet.pack_request(type_name, arg='')
|
54
|
+
type_num = NUMS[type_name.to_sym]
|
55
|
+
raise InvalidArgsError, "Invalid type name '#{type_name}'" unless type_num
|
56
|
+
arg = '' if not arg
|
57
|
+
"\0REQ" + [type_num, arg.size].pack('NN') + arg
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
data/lib/gearman/task.rb
CHANGED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Gearman
|
4
|
+
class TaskSet
|
5
|
+
include Logging
|
6
|
+
|
7
|
+
def initialize(client)
|
8
|
+
@client = client
|
9
|
+
@tasks_in_progress = []
|
10
|
+
@finished_tasks = []
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# Add a new task to this TaskSet.
|
15
|
+
#
|
16
|
+
# @param args A Task object
|
17
|
+
# @return true if the task was created successfully, false otherwise
|
18
|
+
def add_task(task)
|
19
|
+
@tasks_in_progress << task
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Wait for all tasks in the set to finish.
|
24
|
+
#
|
25
|
+
# @param timeout maximum amount of time to wait, in seconds - if this is nil, waits forever
|
26
|
+
def wait(timeout = 1)
|
27
|
+
end_time = if timeout
|
28
|
+
Time.now.to_f + timeout
|
29
|
+
else
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
while not @tasks_in_progress.empty?
|
34
|
+
remaining = if end_time
|
35
|
+
(t = end_time - Time.now.to_f) > 0 ? t : 0
|
36
|
+
else
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
begin
|
40
|
+
task = @tasks_in_progress.pop
|
41
|
+
if
|
42
|
+
@client.submit_job(task, true, remaining)
|
43
|
+
@finished_tasks << task
|
44
|
+
end
|
45
|
+
rescue SocketTimeoutError
|
46
|
+
return false
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
@finished_tasks.each do |t|
|
52
|
+
if ( (t.background.nil? || t.background == false) && !t.successful)
|
53
|
+
logger.warn "GearmanRuby: TaskSet failed"
|
54
|
+
return false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
# Wait for all tasks in set to finish, with no timeout
|
61
|
+
def wait_forever
|
62
|
+
wait(nil)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|