einhorn 0.4.9 → 0.5.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.
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: