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.
Files changed (54) hide show
  1. checksums.yaml +8 -8
  2. data/.travis.yml +5 -0
  3. data/CHANGELOG.md +6 -4
  4. data/README.md +111 -0
  5. data/examples/client.rb +1 -2
  6. data/examples/client_reverse_nohost.rb +30 -0
  7. data/examples/{client_reverse.rb → client_reverse_wait.rb} +9 -11
  8. data/examples/worker.rb +8 -5
  9. data/examples/worker_reverse_string.rb +12 -17
  10. data/gearman-ruby.gemspec +3 -5
  11. data/lib/gearman.rb +17 -77
  12. data/lib/gearman/client.rb +129 -147
  13. data/lib/gearman/connection.rb +158 -0
  14. data/lib/gearman/connection_pool.rb +131 -0
  15. data/lib/gearman/exceptions.rb +24 -0
  16. data/lib/gearman/logging.rb +19 -0
  17. data/lib/gearman/packet.rb +61 -0
  18. data/lib/gearman/task.rb +1 -1
  19. data/lib/gearman/task_set.rb +67 -0
  20. data/lib/gearman/version.rb +1 -1
  21. data/lib/gearman/worker.rb +185 -412
  22. data/lib/gearman/worker/ability.rb +55 -0
  23. data/lib/gearman/worker/callbacks.rb +39 -0
  24. data/lib/gearman/worker/job.rb +44 -0
  25. data/spec/client_spec.rb +32 -20
  26. data/spec/connection_pool_spec.rb +55 -0
  27. data/spec/spec_helper.rb +5 -0
  28. data/spec/task_spec.rb +10 -0
  29. data/spec/taskset_spec.rb +2 -2
  30. metadata +18 -37
  31. data/HOWTO +0 -146
  32. data/README +0 -9
  33. data/TODO +0 -8
  34. data/VERSION.yml +0 -4
  35. data/examples/calculus_client.rb +0 -39
  36. data/examples/calculus_worker.rb +0 -45
  37. data/examples/client.php +0 -23
  38. data/examples/client_background.rb +0 -14
  39. data/examples/client_data.rb +0 -16
  40. data/examples/client_epoch.rb +0 -23
  41. data/examples/client_exception.rb +0 -19
  42. data/examples/client_prefix.rb +0 -17
  43. data/examples/gearman_environment.sh +0 -25
  44. data/examples/scale_image.rb +0 -31
  45. data/examples/scale_image_worker.rb +0 -34
  46. data/examples/server.rb +0 -15
  47. data/examples/worker_data.rb +0 -16
  48. data/examples/worker_exception.rb +0 -14
  49. data/examples/worker_prefix.rb +0 -25
  50. data/examples/worker_reverse_to_file.rb +0 -18
  51. data/examples/worker_signals.rb +0 -36
  52. data/lib/gearman/taskset.rb +0 -293
  53. data/lib/gearman/util.rb +0 -211
  54. 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,19 @@
1
+ module Gearman
2
+ module Logging
3
+
4
+ def self.included(target)
5
+ target.extend ClassMethods
6
+ end
7
+
8
+ def logger
9
+ self.class.logger
10
+ end
11
+
12
+ module ClassMethods
13
+ def logger
14
+ Gearman.logger
15
+ end
16
+ end
17
+
18
+ end
19
+ 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
@@ -213,7 +213,7 @@ module Gearman
213
213
  end
214
214
 
215
215
  mode = modes.join('_')
216
- Util::pack_request(mode, args.join("\0"))
216
+ Packet::pack_request(mode, args.join("\0"))
217
217
  end
218
218
  end
219
219
 
@@ -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