gearman-ruby 3.0.8 → 4.0.2

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