einhorn 0.4.9 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ === 0.5.0 2013-07-11
2
+
3
+ * 1 major enhancement:
4
+ * Improve Einhorn protocol to allow multiplexing of commands on a single socket
data/bin/einhornsh CHANGED
@@ -10,11 +10,33 @@ require 'einhorn'
10
10
 
11
11
  module Einhorn
12
12
  class EinhornSH
13
+
13
14
  def initialize(path_to_socket)
14
15
  @path_to_socket = path_to_socket
16
+ @request_id = 0
15
17
  reconnect
16
18
  end
17
19
 
20
+ def send_command(hash)
21
+ begin
22
+ @client.send_command(hash)
23
+ while response = @client.receive_message
24
+ if response.kind_of?(Hash)
25
+ yield response['message']
26
+ return unless response['wait']
27
+ else
28
+ puts "Invalid response type #{response.class}: #{response.inspect}"
29
+ end
30
+ end
31
+ rescue Errno::EPIPE => e
32
+ emit("einhornsh: Error communicating with Einhorn: #{e} (#{e.class})")
33
+ emit("einhornsh: Attempting to reconnect...")
34
+ reconnect
35
+
36
+ retry
37
+ end
38
+ end
39
+
18
40
  def run
19
41
  emit("Enter 'help' if you're not sure what to do.")
20
42
  emit
@@ -26,25 +48,16 @@ module Einhorn
26
48
  emit("Goodbye!")
27
49
  return
28
50
  end
29
-
30
- begin
31
- response = @client.command({'command' => command, 'args' => args})
32
- rescue Errno::EPIPE => e
33
- emit("einhornsh: Error communicating with Einhorn: #{e} (#{e.class})")
34
- emit("einhornsh: Attempting to reconnect...")
35
- reconnect
36
-
37
- retry
38
- end
39
-
40
- if response.kind_of?(Hash)
41
- puts response['message']
42
- else
43
- puts "Invalid response type #{response.class}: #{response.inspect}"
51
+ send_command({'id' => request_id, 'command' => command, 'args' => args}) do |message|
52
+ puts message
44
53
  end
45
54
  end
46
55
  end
47
56
 
57
+ def request_id
58
+ @request_id += 1
59
+ end
60
+
48
61
  def parse_command(line)
49
62
  command, *args = Shellwords.shellsplit(line)
50
63
  [command, args]
@@ -69,8 +82,9 @@ EOF
69
82
  end
70
83
 
71
84
  def ehlo
72
- response = @client.command('command' => 'ehlo', 'user' => ENV['USER'])
73
- emit(response['message'])
85
+ send_command({'command' => "ehlo", 'user' => ENV['USER']}) do |message|
86
+ emit(message)
87
+ end
74
88
  end
75
89
 
76
90
  def self.emit(message=nil, force=false)
@@ -7,6 +7,9 @@ module Einhorn
7
7
  # Keep this in this file so client can be loaded entirely
8
8
  # standalone by user code.
9
9
  module Transport
10
+
11
+ ParseError = defined?(Psych::SyntaxError) ? Psych::SyntaxError : ArgumentError
12
+
10
13
  def self.send_message(socket, message)
11
14
  line = serialize_message(message)
12
15
  socket.write(line)
@@ -29,8 +32,6 @@ module Einhorn
29
32
  end
30
33
  end
31
34
 
32
- @@responseless_commands = Set.new(['worker:ack'])
33
-
34
35
  def self.for_path(path_to_socket)
35
36
  socket = UNIXSocket.open(path_to_socket)
36
37
  self.new(socket)
@@ -45,13 +46,12 @@ module Einhorn
45
46
  @socket = socket
46
47
  end
47
48
 
48
- def command(command_hash)
49
+ def send_command(command_hash)
49
50
  Transport.send_message(@socket, command_hash)
50
- Transport.receive_message(@socket) if expect_response?(command_hash)
51
51
  end
52
52
 
53
- def expect_response?(command_hash)
54
- !@@responseless_commands.include?(command_hash['command'])
53
+ def receive_message
54
+ Transport.receive_message(@socket)
55
55
  end
56
56
 
57
57
  def close
@@ -202,38 +202,50 @@ module Einhorn::Command
202
202
  end
203
203
 
204
204
  def self.process_command(conn, command)
205
- response = generate_response(conn, command)
206
- if !response.nil?
207
- send_message(conn, response)
205
+ begin
206
+ request = Einhorn::Client::Transport.deserialize_message(command)
207
+ rescue Einhorn::Client::Transport::ParseError
208
+ end
209
+ unless request.kind_of?(Hash)
210
+ send_message(conn, "Could not parse command")
211
+ return
212
+ end
213
+
214
+ message = generate_message(conn, request)
215
+ if !message.nil?
216
+ send_message(conn, message, request['id'], true)
208
217
  else
209
218
  conn.log_debug("Got back nil response, so not responding to command.")
210
219
  end
211
220
  end
212
221
 
213
- def self.send_message(conn, response)
214
- if response.kind_of?(String)
215
- response = {'message' => response}
222
+ def self.send_tagged_message(tag, message, last=false)
223
+ Einhorn::Event.connections.each do |conn|
224
+ if id = conn.subscription(tag)
225
+ self.send_message(conn, message, id, last)
226
+ conn.unsubscribe(tag) if last
227
+ end
216
228
  end
217
- Einhorn::Client::Transport.send_message(conn, response)
218
229
  end
219
230
 
220
- def self.generate_response(conn, command)
221
- begin
222
- request = Einhorn::Client::Transport.deserialize_message(command)
223
- rescue ArgumentError => e
224
- return {
225
- 'message' => "Could not parse command: #{e}"
226
- }
231
+ def self.send_message(conn, message, request_id=nil, last=false)
232
+ if request_id
233
+ response = {'message' => message, 'request_id' => request_id }
234
+ response['wait'] = true unless last
235
+ else
236
+ # support old-style protocol
237
+ response = {'message' => message}
227
238
  end
239
+ Einhorn::Client::Transport.send_message(conn, response)
240
+ end
228
241
 
242
+ def self.generate_message(conn, request)
229
243
  unless command_name = request['command']
230
- return {
231
- 'message' => 'No "command" parameter provided; not sure what you want me to do.'
232
- }
244
+ return 'No "command" parameter provided; not sure what you want me to do.'
233
245
  end
234
246
 
235
247
  if command_spec = @@commands[command_name]
236
- conn.log_debug("Received command: #{command.inspect}")
248
+ conn.log_debug("Received command: #{request.inspect}")
237
249
  begin
238
250
  return command_spec[:code].call(conn, request)
239
251
  rescue StandardError => e
@@ -242,7 +254,7 @@ module Einhorn::Command
242
254
  return msg
243
255
  end
244
256
  else
245
- conn.log_debug("Received unrecognized command: #{command.inspect}")
257
+ conn.log_debug("Received unrecognized command: #{request.inspect}")
246
258
  return unrecognized_command(conn, request)
247
259
  end
248
260
  end
@@ -295,7 +307,7 @@ EOF
295
307
  YAML.dump(Einhorn::Command.dumpable_state)
296
308
  end
297
309
 
298
- command 'reload', 'Reload Einhorn' do |conn, _|
310
+ command 'reload', 'Reload Einhorn' do |conn, request|
299
311
  # TODO: make reload actually work (command socket reopening is
300
312
  # an issue). Would also be nice if user got a confirmation that
301
313
  # the reload completed, though that's not strictly necessary.
@@ -303,7 +315,7 @@ EOF
303
315
  # In the normal case, this will do a write
304
316
  # synchronously. Otherwise, the bytes will be stuck into the
305
317
  # buffer and lost upon reload.
306
- send_message(conn, 'Reloading, as commanded')
318
+ send_message(conn, 'Reloading, as commanded', request['id'], true)
307
319
  Einhorn::Command.reload
308
320
  end
309
321
 
@@ -323,10 +335,13 @@ EOF
323
335
  Einhorn::Command.louder
324
336
  end
325
337
 
326
- command 'upgrade', 'Upgrade all Einhorn workers. This may result in Einhorn reloading its own code as well.' do |conn, _|
327
- # TODO: send confirmation when this is done
328
- send_message(conn, 'Upgrading, as commanded')
329
- # This or may not return
338
+ command 'upgrade', 'Upgrade all Einhorn workers. This may result in Einhorn reloading its own code as well.' do |conn, request|
339
+ # send first message directly for old clients that don't support request
340
+ # ids or subscriptions. Everything else is sent tagged with request id
341
+ # for new clients.
342
+ send_message(conn, 'Upgrading, as commanded', request['id'])
343
+ conn.subscribe(:upgrade, request['id'])
344
+ # If the app is preloaded this doesn't return.
330
345
  Einhorn::Command.full_upgrade
331
346
  nil
332
347
  end
@@ -301,10 +301,10 @@ module Einhorn
301
301
 
302
302
  def self.upgrade_workers
303
303
  if Einhorn::State.upgrading
304
- Einhorn.log_info("Currently upgrading (#{Einhorn::WorkerPool.ack_count} / #{Einhorn::WorkerPool.ack_target} ACKs; bumping version and starting over)...")
304
+ Einhorn.log_info("Currently upgrading (#{Einhorn::WorkerPool.ack_count} / #{Einhorn::WorkerPool.ack_target} ACKs; bumping version and starting over)...", :upgrade)
305
305
  else
306
306
  Einhorn::State.upgrading = true
307
- Einhorn.log_info("Starting upgrade to #{Einhorn::State.version}...")
307
+ Einhorn.log_info("Starting upgrade from version #{Einhorn::State.version}...", :upgrade)
308
308
  end
309
309
 
310
310
  # Reset this, since we've just upgraded to a new universe (I'm
@@ -323,7 +323,8 @@ module Einhorn
323
323
 
324
324
  if Einhorn::State.upgrading && acked >= target
325
325
  Einhorn::State.upgrading = false
326
- Einhorn.log_info("Upgrade to version #{Einhorn::State.version} complete.")
326
+ Einhorn.log_info("Upgraded successfully to version #{Einhorn::State.version} (Einhorn #{Einhorn::VERSION}).", :upgrade)
327
+ Einhorn.send_tagged_message(:upgrade, "Upgrade done", true)
327
328
  end
328
329
 
329
330
  old_workers = Einhorn::WorkerPool.old_workers
@@ -2,6 +2,11 @@ module Einhorn::Event
2
2
  class Connection < AbstractTextDescriptor
3
3
  include Persistent
4
4
 
5
+ def initialize(*args)
6
+ @subscriptions = {}
7
+ super
8
+ end
9
+
5
10
  def parse_record
6
11
  split = @read_buffer.split("\n", 2)
7
12
  if split.length > 1
@@ -20,6 +25,7 @@ module Einhorn::Event
20
25
  # Don't include by default because it's not that pretty
21
26
  state[:read_buffer] = @read_buffer if @read_buffer.length > 0
22
27
  state[:write_buffer] = @write_buffer if @write_buffer.length > 0
28
+ state[:subscriptions] = @subscriptions
23
29
  state
24
30
  end
25
31
 
@@ -29,16 +35,36 @@ module Einhorn::Event
29
35
  conn = self.open(socket)
30
36
  conn.read_buffer = state[:read_buffer] if state[:read_buffer]
31
37
  conn.write_buffer = state[:write_buffer] if state[:write_buffer]
38
+ # subscriptions could be empty if upgrading from an older version of einhorn
39
+ state.fetch(:subscriptions, {}).each do |tag, id|
40
+ conn.subscribe(tag, id)
41
+ end
32
42
  conn
33
43
  end
34
44
 
45
+ def subscribe(tag, request_id)
46
+ if request_id
47
+ @subscriptions[tag] = request_id
48
+ end
49
+ end
50
+
51
+ def subscription(tag)
52
+ @subscriptions[tag]
53
+ end
54
+
55
+ def unsubscribe(tag)
56
+ @subscriptions.delete(tag)
57
+ end
58
+
35
59
  def register!
36
60
  log_info("client connected")
61
+ Einhorn::Event.register_connection(self, @socket.fileno)
37
62
  super
38
63
  end
39
64
 
40
65
  def deregister!
41
66
  log_info("client disconnected") if Einhorn::TransientState.whatami == :master
67
+ Einhorn::Event.deregister_connection(@socket.fileno)
42
68
  super
43
69
  end
44
70
  end
@@ -11,7 +11,7 @@ module Einhorn::Event
11
11
  if klass = @@persistent[klass_name]
12
12
  klass.from_state(state)
13
13
  else
14
- Einhorn.log_error("Unrecognized persistent descriptor class #{klass_name.inspect}. Ignoring. This most likely indicates that your Einhorn version has upgraded. Everything should still be working, but it may be worth a restart.")
14
+ Einhorn.log_error("Unrecognized persistent descriptor class #{klass_name.inspect}. Ignoring. This most likely indicates that your Einhorn version has upgraded. Everything should still be working, but it may be worth a restart.", :upgrade)
15
15
  nil
16
16
  end
17
17
  end
data/lib/einhorn/event.rb CHANGED
@@ -7,6 +7,7 @@ module Einhorn
7
7
  @@signal_actions = []
8
8
  @@readable = {}
9
9
  @@writeable = {}
10
+ @@connections = {}
10
11
  @@timers = {}
11
12
 
12
13
  def self.cloexec!(fd)
@@ -88,6 +89,18 @@ module Einhorn
88
89
  writers
89
90
  end
90
91
 
92
+ def self.register_connection(connection, fd)
93
+ @@connections[fd] = connection
94
+ end
95
+
96
+ def self.deregister_connection(fd)
97
+ @@connections.delete(fd)
98
+ end
99
+
100
+ def self.connections
101
+ @@connections.values
102
+ end
103
+
91
104
  def self.register_timer(timer)
92
105
  @@timers[timer.expires_at] ||= Set.new
93
106
  @@timers[timer.expires_at] << timer
@@ -1,3 +1,3 @@
1
1
  module Einhorn
2
- VERSION = '0.4.9'
2
+ VERSION = '0.5.0'
3
3
  end
@@ -69,7 +69,7 @@ module Einhorn
69
69
  raise "Unrecognized socket discovery mechanism: #{discovery.inspect}. Must be one of :filesystem, :argv, or :direct"
70
70
  end
71
71
 
72
- client.command({
72
+ client.send_command({
73
73
  'command' => 'worker:ack',
74
74
  'pid' => $$
75
75
  })
data/lib/einhorn.rb CHANGED
@@ -169,11 +169,17 @@ module Einhorn
169
169
  def self.log_debug(msg)
170
170
  $stderr.puts("#{log_tag} DEBUG: #{msg}") if Einhorn::State.verbosity <= 0
171
171
  end
172
- def self.log_info(msg)
172
+ def self.log_info(msg, tag=nil)
173
173
  $stderr.puts("#{log_tag} INFO: #{msg}") if Einhorn::State.verbosity <= 1
174
+ self.send_tagged_message(tag, msg) if tag
174
175
  end
175
- def self.log_error(msg)
176
+ def self.log_error(msg, tag=nil)
176
177
  $stderr.puts("#{log_tag} ERROR: #{msg}") if Einhorn::State.verbosity <= 2
178
+ self.send_tagged_message(tag, "ERROR: #{msg}") if tag
179
+ end
180
+
181
+ def self.send_tagged_message(tag, message, last=false)
182
+ Einhorn::Command::Interface.send_tagged_message(tag, message, last)
177
183
  end
178
184
 
179
185
  private
@@ -221,20 +227,20 @@ module Einhorn
221
227
  begin
222
228
  # If it's not going to be requireable, then load it.
223
229
  if !path.end_with?('.rb') && File.exists?(path)
224
- log_info("Loading #{path} (if this hangs, make sure your code can be properly loaded as a library)")
230
+ log_info("Loading #{path} (if this hangs, make sure your code can be properly loaded as a library)", :upgrade)
225
231
  load path
226
232
  else
227
- log_info("Requiring #{path} (if this hangs, make sure your code can be properly loaded as a library)")
233
+ log_info("Requiring #{path} (if this hangs, make sure your code can be properly loaded as a library)", :upgrade)
228
234
  require path
229
235
  end
230
236
  rescue Exception => e
231
- log_info("Proceeding with postload -- could not load #{path}: #{e} (#{e.class})\n #{e.backtrace.join("\n ")}")
237
+ log_info("Proceeding with postload -- could not load #{path}: #{e} (#{e.class})\n #{e.backtrace.join("\n ")}", :upgrade)
232
238
  else
233
239
  if defined?(einhorn_main)
234
- log_info("Successfully loaded #{path}")
240
+ log_info("Successfully loaded #{path}", :upgrade)
235
241
  Einhorn::TransientState.preloaded = true
236
242
  else
237
- log_info("Proceeding with postload -- loaded #{path}, but no einhorn_main method was defined")
243
+ log_info("Proceeding with postload -- loaded #{path}, but no einhorn_main method was defined", :upgrade)
238
244
  end
239
245
  end
240
246
  end
@@ -59,12 +59,9 @@ class ClientTest < EinhornTestCase
59
59
 
60
60
  it "raises an error when deserializing invalid YAML" do
61
61
  invalid_serialized = "-%0A\t-"
62
- expected = [ArgumentError]
63
- expected << Psych::SyntaxError if defined?(Psych::SyntaxError) # 1.9
64
-
65
62
  begin
66
63
  Einhorn::Client::Transport.deserialize_message(invalid_serialized)
67
- rescue *expected
64
+ rescue Einhorn::Client::Transport::ParseError
68
65
  end
69
66
  end
70
67
  end
@@ -24,8 +24,8 @@ class EventTest < EinhornTestCase
24
24
  end
25
25
 
26
26
  it "selects on readable descriptors" do
27
- sock1 = mock(:fileno => 4)
28
- sock2 = mock(:fileno => 5)
27
+ sock1 = stub(:fileno => 4)
28
+ sock2 = stub(:fileno => 5)
29
29
 
30
30
  conn1 = Einhorn::Event::Connection.open(sock1)
31
31
  conn2 = Einhorn::Event::Connection.open(sock2)
@@ -41,8 +41,8 @@ class EventTest < EinhornTestCase
41
41
  end
42
42
 
43
43
  it "selects on writeable descriptors" do
44
- sock1 = mock(:fileno => 4)
45
- sock2 = mock(:fileno => 5)
44
+ sock1 = stub(:fileno => 4)
45
+ sock2 = stub(:fileno => 5)
46
46
 
47
47
  conn1 = Einhorn::Event::Connection.open(sock1)
48
48
  conn2 = Einhorn::Event::Connection.open(sock2)
@@ -61,8 +61,8 @@ class EventTest < EinhornTestCase
61
61
  end
62
62
 
63
63
  it "runs callbacks for ready selectables" do
64
- sock1 = mock(:fileno => 4)
65
- sock2 = mock(:fileno => 5)
64
+ sock1 = stub(:fileno => 4)
65
+ sock2 = stub(:fileno => 5)
66
66
 
67
67
  conn1 = Einhorn::Event::Connection.open(sock1)
68
68
  conn2 = Einhorn::Event::Connection.open(sock2)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: einhorn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.9
4
+ version: 0.5.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-08-23 00:00:00.000000000 Z
12
+ date: 2013-08-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -74,6 +74,7 @@ files:
74
74
  - .gitignore
75
75
  - .travis.yml
76
76
  - Gemfile
77
+ - History.txt
77
78
  - LICENSE
78
79
  - README.md
79
80
  - README.md.in
@@ -129,12 +130,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
129
130
  - - ! '>='
130
131
  - !ruby/object:Gem::Version
131
132
  version: '0'
133
+ segments:
134
+ - 0
135
+ hash: 3689317997291483105
132
136
  required_rubygems_version: !ruby/object:Gem::Requirement
133
137
  none: false
134
138
  requirements:
135
139
  - - ! '>='
136
140
  - !ruby/object:Gem::Version
137
141
  version: '0'
142
+ segments:
143
+ - 0
144
+ hash: 3689317997291483105
138
145
  requirements: []
139
146
  rubyforge_project:
140
147
  rubygems_version: 1.8.23
@@ -149,4 +156,3 @@ test_files:
149
156
  - test/unit/einhorn/command/interface.rb
150
157
  - test/unit/einhorn/event.rb
151
158
  - test/unit/einhorn/worker_pool.rb
152
- has_rdoc: