evented-gearman-ruby 1.0.0

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 (45) hide show
  1. data/.gitignore +1 -0
  2. data/HOWTO +146 -0
  3. data/LICENSE +20 -0
  4. data/README +4 -0
  5. data/Rakefile +41 -0
  6. data/TODO +8 -0
  7. data/VERSION.yml +4 -0
  8. data/evented-gearman-ruby.gemspec +110 -0
  9. data/examples/calculus_client.rb +41 -0
  10. data/examples/calculus_worker.rb +42 -0
  11. data/examples/client.rb +19 -0
  12. data/examples/client_background.rb +14 -0
  13. data/examples/client_data.rb +16 -0
  14. data/examples/client_exception.rb +17 -0
  15. data/examples/client_prefix.rb +15 -0
  16. data/examples/evented_client.rb +23 -0
  17. data/examples/evented_worker.rb +26 -0
  18. data/examples/gearman_environment.sh +25 -0
  19. data/examples/scale_image.rb +30 -0
  20. data/examples/scale_image_worker.rb +34 -0
  21. data/examples/server.rb +15 -0
  22. data/examples/worker.rb +23 -0
  23. data/examples/worker_data.rb +16 -0
  24. data/examples/worker_exception.rb +14 -0
  25. data/examples/worker_prefix.rb +25 -0
  26. data/lib/gearman.rb +29 -0
  27. data/lib/gearman/client.rb +80 -0
  28. data/lib/gearman/evented/client.rb +99 -0
  29. data/lib/gearman/evented/reactor.rb +86 -0
  30. data/lib/gearman/evented/worker.rb +118 -0
  31. data/lib/gearman/job.rb +38 -0
  32. data/lib/gearman/protocol.rb +110 -0
  33. data/lib/gearman/server.rb +94 -0
  34. data/lib/gearman/task.rb +99 -0
  35. data/lib/gearman/taskset.rb +11 -0
  36. data/lib/gearman/util.rb +52 -0
  37. data/lib/gearman/worker.rb +39 -0
  38. data/test/basic_integration_test.rb +121 -0
  39. data/test/crash_test.rb +69 -0
  40. data/test/job_test.rb +30 -0
  41. data/test/protocol_test.rb +132 -0
  42. data/test/test_helper.rb +31 -0
  43. data/test/util_test.rb +12 -0
  44. data/test/worker_test.rb +45 -0
  45. metadata +149 -0
@@ -0,0 +1,86 @@
1
+ module Gearman
2
+ module Evented
3
+
4
+ module Reactor
5
+ include EM::Deferrable
6
+
7
+ def self.included(mod)
8
+ mod.instance_eval do
9
+ def connect(host, port, opts = {})
10
+ Gearman::Evented::Reactor.connect(host, port, self, opts)
11
+ end
12
+ end
13
+ end
14
+
15
+ def self.connect(host, port, reactor, opts = {})
16
+ EM.connect(host, (port || 4730), reactor) do |c|
17
+ c.instance_eval do
18
+ @host = host
19
+ @port = port || 4730
20
+ @opts = opts
21
+ end
22
+ end
23
+ end
24
+
25
+ def connected?
26
+ @connected
27
+ end
28
+
29
+ def server
30
+ @hostport ||= [ @host, @port ].join(":")
31
+ end
32
+
33
+ def connection_completed
34
+ log "connected to #{@host}:#{@port}"
35
+ @connected = true
36
+ @reconnecting = false
37
+ @reconnect = true
38
+ succeed
39
+ end
40
+
41
+ def unbind
42
+ log "disconnected from #{@host}:#{@port}"
43
+ @connected = false
44
+ EM.next_tick { reconnect } unless !@reconnect
45
+ @reconnect = true
46
+ end
47
+
48
+ def disconnect
49
+ log "force disconnect from #{@host}:#{@port}"
50
+ @reconnect = false
51
+ close_connection_after_writing
52
+ end
53
+
54
+ def reconnect(force = false)
55
+ if @reconnecting
56
+ EM.add_timer(@opts[:reconnect_sec] || 30) { reconnect }
57
+ return
58
+ elsif !@reconnect && !force
59
+ log "forced disconnect, aborting reconnect attempt"
60
+ @reconnect = true
61
+ return
62
+ else
63
+ @reconnecting = true
64
+ @deferred_status = nil
65
+ end
66
+
67
+ log "reconnecting to #{@host}:#{@port}"
68
+ EM.reconnect(@host, @port.to_i, self)
69
+ end
70
+
71
+ def send(command, data = nil)
72
+ log "send #{command} #{data.inspect} (#{server})"
73
+ send_data(Gearman::Protocol.encode_request(command, data))
74
+ end
75
+
76
+ def log(msg, force = false)
77
+ Gearman::Util.log(msg, force)
78
+ end
79
+
80
+ def to_s
81
+ "#{@host}:#{@port}"
82
+ end
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,118 @@
1
+ module Gearman
2
+ module Evented
3
+
4
+ module WorkerReactor
5
+ include Gearman::Evented::Reactor
6
+
7
+ def connection_completed
8
+ send :set_client_id, client_id
9
+ super
10
+
11
+ @abilities ||= @opts.delete(:abilities) || []
12
+ @abilities.each do |ability, args|
13
+ announce_ability(ability, args[:timeout])
14
+ end
15
+
16
+ grab_job
17
+ end
18
+
19
+ def announce_ability(name, timeout)
20
+ cmd = timeout ? :can_do_timeout : :can_do
21
+ arg = timeout ? "#{name}\0#{timeout.to_s}" : name
22
+ log "announce_ability #{name} #{timeout}"
23
+ send cmd, arg
24
+ end
25
+
26
+ def announce_disability(name)
27
+ send :cant_do, name
28
+ end
29
+
30
+ def grab_job
31
+ log "Grab Job"
32
+ send :grab_job_uniq
33
+ end
34
+
35
+ def work_fail(handle)
36
+ send :work_fail, handle
37
+ end
38
+
39
+ def work_complete(handle, data)
40
+ send :work_complete, "#{handle}\0#{data}"
41
+ end
42
+
43
+ def work_warning(handle, message)
44
+ send :work_warning, "#{handle}\0#{message}"
45
+ end
46
+
47
+ def receive_data(data)
48
+ Gearman::Protocol.decode_response(data).each do |type, handle, *data|
49
+ dispatch_packet(type, handle, *data)
50
+ end
51
+ end
52
+
53
+ def dispatch_packet(type, handle, *data)
54
+ success = true
55
+ timer = 0
56
+ case type
57
+ when :no_job
58
+ send :pre_sleep
59
+ timer = @opts[:reconnect_sec] || 30
60
+ when :job_assign, :job_assign_uniq
61
+ log "job assign #{handle}, #{data.inspect}"
62
+ handle_job_assign(handle, data[0], data[1])
63
+ when :noop
64
+ log "NOOP"
65
+ when :error
66
+ log "[ERROR]: error from server #{server}: #{data}"
67
+ else
68
+ log "Got unknown #{type}, #{data} from #{server}"
69
+ end
70
+
71
+ EM.add_timer(timer) { grab_job }
72
+ succeed [handle, data]
73
+ end
74
+
75
+ def handle_job_assign(handle, func, args = '')
76
+ return unless handle
77
+ unless func
78
+ log "ERROR: Ignoring job_assign with no function"
79
+ return
80
+ end
81
+
82
+ log "Got job_assign '#{func}' with handle #{handle} and #{args.size rescue 0} byte(s)"
83
+
84
+ unless @abilities.has_key?(func)
85
+ log "Ignoring job_assign for unsupported func #{func} with handle #{handle}"
86
+ work_fail handle
87
+ return
88
+ end
89
+
90
+ exception = nil
91
+ begin
92
+ ret = @abilities[func][:callback].call(args, Gearman::Job.new(self, handle))
93
+ rescue Exception => e
94
+ exception = e
95
+ end
96
+
97
+ if ret && exception.nil?
98
+ ret = ret.to_s
99
+ log "Sending work_complete for #{handle} with #{ret.size} byte(s)"
100
+ work_complete handle, ret
101
+ elsif exception.nil?
102
+ log "Sending work_fail for #{handle} to #{server}"
103
+ work_fail handle
104
+ elsif exception
105
+ log "exception #{exception.message}, sending work_warning, work_fail for #{handle}"
106
+ work_warning handle, exception.message
107
+ work_fail handle
108
+ end
109
+ end
110
+
111
+ def client_id
112
+ @client_id ||= `uuidgen`.strip
113
+ end
114
+
115
+ end
116
+
117
+ end
118
+ end
@@ -0,0 +1,38 @@
1
+ module Gearman
2
+ # = Job
3
+ #
4
+ # == Description
5
+ # Interface to allow a worker to report information to a job server.
6
+ class Job
7
+ ##
8
+ # Create a new Job.
9
+ #
10
+ # @param sock Socket connected to job server
11
+ # @param handle job server-supplied job handle
12
+ def initialize(client, handle)
13
+ @client = client
14
+ @handle = handle
15
+ end
16
+ ##
17
+ # Report our status to the job server.
18
+ def report_status(numerator, denominator)
19
+ @client.send :work_status, "#{@handle}\0#{numerator}\0#{denominator}"
20
+ self
21
+ end
22
+
23
+ ##
24
+ # Send data before job completes
25
+ def send_partial(data)
26
+ @client.send :work_data, "#{@handle}\0#{data}"
27
+ self
28
+ end
29
+ alias :send_data :send_partial
30
+
31
+ ##
32
+ # Send a warning explicitly
33
+ def report_warning(warning)
34
+ @client.send :work_warning, "#{@handle}\0#{warning}"
35
+ self
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,110 @@
1
+ module Gearman
2
+
3
+ class ProtocolError < Exception; end
4
+
5
+
6
+ class Protocol
7
+ # Map from Integer representations of commands used in the network
8
+ # protocol to more-convenient symbols.
9
+ COMMANDS = {
10
+ 1 => :can_do, # W->J: FUNC
11
+ 2 => :cant_do, # W->J: FUNC
12
+ 3 => :reset_abilities, # W->J: --
13
+ 4 => :pre_sleep, # W->J: --
14
+ #5 => (unused), # - -
15
+ 6 => :noop, # J->W: --
16
+ 7 => :submit_job, # C->J: FUNC[0]UNIQ[0]ARGS
17
+ 8 => :job_created, # J->C: HANDLE
18
+ 9 => :grab_job, # W->J: --
19
+ 10 => :no_job, # J->W: --
20
+ 11 => :job_assign, # J->W: HANDLE[0]FUNC[0]ARG
21
+ 12 => :work_status, # W->J/C: HANDLE[0]NUMERATOR[0]DENOMINATOR
22
+ 13 => :work_complete, # W->J/C: HANDLE[0]RES
23
+ 14 => :work_fail, # W->J/C: HANDLE
24
+ 15 => :get_status, # C->J: HANDLE
25
+ 16 => :echo_req, # ?->J: TEXT
26
+ 17 => :echo_res, # J->?: TEXT
27
+ 18 => :submit_job_bg, # C->J: FUNC[0]UNIQ[0]ARGS
28
+ 19 => :error, # J->?: ERRCODE[0]ERR_TEXT
29
+ 20 => :status_res, # C->J: HANDLE[0]KNOWN[0]RUNNING[0]NUM[0]DENOM
30
+ 21 => :submit_job_high, # C->J: FUNC[0]UNIQ[0]ARGS
31
+ 22 => :set_client_id, # W->J: [RANDOM_STRING_NO_WHITESPACE]
32
+ 23 => :can_do_timeout, # W->J: FUNC[0]TIMEOUT
33
+ 24 => :all_yours, # REQ Worker
34
+ 25 => :work_exception, # W->J: HANDLE[0]ARG
35
+ 26 => :option_req, # C->J: TEXT
36
+ 27 => :option_res, # J->C: TEXT
37
+ 28 => :work_data, # REQ Worker
38
+ 29 => :work_warning, # W->J/C: HANDLE[0]MSG
39
+ 30 => :grab_job_uniq, # REQ Worker
40
+ 31 => :job_assign_uniq, # RES Worker
41
+ 32 => :submit_job_high_bg, # C->J: FUNC[0]UNIQ[0]ARGS
42
+ 33 => :submit_job_low, # C->J: FUNC[0]UNIQ[0]ARGS
43
+ 34 => :submit_job_low_bg, # C->J: FUNC[0]UNIQ[0]ARGS
44
+ 35 => :submit_job_sched, # REQ Client
45
+ 36 => :submit_job_epoch # REQ Client
46
+ }
47
+
48
+ # Map e.g. 'can_do' => 1
49
+ COMMANDS_NUMERIC = COMMANDS.invert
50
+
51
+ # Default job server port.
52
+ DEFAULT_PORT = 4730
53
+
54
+ class << self
55
+
56
+ def encode_request(type_name, arg = nil)
57
+ type_num = COMMANDS_NUMERIC[type_name.to_sym]
58
+ raise InvalidArgsError, "Invalid type name '#{type_name}'" unless type_num
59
+ arg = '' if not arg
60
+ "\0REQ" + [type_num, arg.size].pack('NN') + arg
61
+ end
62
+
63
+ def decode_response(data, packets = [])
64
+ # p data
65
+ magic, type, len = data[0..12].unpack('a4NN')
66
+ raise ProtocolError, "Invalid magic '#{magic}'" unless magic == "\0RES"
67
+ type = COMMANDS[type]
68
+ raise ProtocolError, "Invalid packet type #{type}" unless type
69
+
70
+ buf, *more = data[12, data.size].split("\0RES")
71
+ handle, data = buf ? buf.split("\0", 2) : nil
72
+ packets << response_packet(type, handle, data)
73
+ more.each do |packet|
74
+ decode_response("\0RES#{packet}", packets)
75
+ end
76
+ packets
77
+ end
78
+
79
+ def response_packet(type, handle, data)
80
+ case type
81
+ when :work_complete,
82
+ :work_exception,
83
+ :work_warning,
84
+ :work_data,
85
+ :error
86
+ [type, handle, data]
87
+ when :job_assign
88
+ func, data = data.split("\0", 3)
89
+ [type, handle, func.to_s, data]
90
+ when :job_assign_uniq
91
+ func, uuid, data = data.split("\0", 3)
92
+ [type, handle, func.to_s, data, uuid]
93
+ when :work_fail,
94
+ :job_created,
95
+ :no_job,
96
+ :noop
97
+ [type, handle]
98
+ when :work_status
99
+ num, den = data.split("\0", 3)
100
+ [type, handle, num, den]
101
+ when :status_res
102
+ known, running, num, den = data.split("\0", 4)
103
+ [type, handle, known, running, num, den]
104
+ else
105
+ raise ProtocolError, "Invalid packet #{type}"
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'socket'
4
+ require 'gearman'
5
+
6
+ module Gearman
7
+
8
+ # = Server
9
+ #
10
+ # == Description
11
+ # A client for managing Gearman job servers.
12
+ class Server
13
+ ##
14
+ # Create a new client.
15
+ #
16
+ # @param job_servers "host:port"; either a single server or an array
17
+ # @param prefix function name prefix (namespace)
18
+ def initialize(hostport)
19
+ @hostport = hostport # "host:port"
20
+ end
21
+ attr_reader :hostport
22
+
23
+ ##
24
+ # Get a socket for a job server.
25
+ #
26
+ # @param hostport job server "host:port"
27
+ # @return a Socket
28
+ def socket(num_retries=3)
29
+ return @socket if @socket
30
+ num_retries.times do
31
+ begin
32
+ sock = TCPSocket.new(*hostport.split(':'))
33
+ rescue Exception
34
+ else
35
+ return @socket = sock
36
+ end
37
+ end
38
+ raise RuntimeError, "Unable to connect to job server #{hostport}"
39
+ end
40
+
41
+ ##
42
+ # Sends a command to the server.
43
+ #
44
+ # @return a response string
45
+ def send_command(name)
46
+ response = ''
47
+ socket.puts(name)
48
+ while true do
49
+ if buf = socket.recv_nonblock(65536) rescue nil
50
+ response << buf
51
+ return response if response =~ /\n.\n$/
52
+ end
53
+ end
54
+ end
55
+
56
+ ##
57
+ # Returns results of a 'status' command.
58
+ #
59
+ # @return a hash of abilities with queued, active and workers keys.
60
+ def status
61
+ status = {}
62
+ if response = send_command('status')
63
+ response.split("\n").each do |line|
64
+ if line.match /^([A-Za-z_]+)\t([A-Za-z_]+)\t(\d+)\t(\d+)\t(\d+)$/
65
+ (status[$1] ||= {})[$2] = { :queue => $3, :active => $4, :workers => $5 }
66
+ end
67
+ end
68
+ end
69
+ status
70
+ end
71
+
72
+ ##
73
+ # Returns results of a 'workers' command.
74
+ #
75
+ # @return an array of worker hashes, containing host, status and functions keys.
76
+ def workers
77
+ workers = []
78
+ if response = send_command('workers')
79
+ response.split("\n").each do |line|
80
+ if line.match /^(\d+)\s([a-z0-9\:\.]+)\s([^\s]*)\s:\s([a-z_\s\t]+)$/
81
+ func_parts = $4.split(' ')
82
+ functions = []
83
+ while !func_parts.empty?
84
+ functions << func_parts.shift << '.' << func_parts.shift
85
+ end
86
+ workers << { :host => $2, :status => $3, :functions => functions }
87
+ end
88
+ end
89
+ end
90
+ workers
91
+ end
92
+ end
93
+
94
+ end