expedite 0.0.2 → 0.1.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.
@@ -5,4 +5,7 @@ module Expedite
5
5
 
6
6
  class CommandNotFound < Error
7
7
  end
8
+
9
+ class UnknownError < Error
10
+ end
8
11
  end
@@ -0,0 +1,18 @@
1
+ require 'socket'
2
+
3
+ module Expedite
4
+ module Protocol
5
+ def send_object(object)
6
+ data = Marshal.dump(object)
7
+
8
+ self.puts data.bytesize
9
+ self.write data
10
+ end
11
+
12
+ def recv_object
13
+ Marshal.load(self.read(self.gets.to_i))
14
+ end
15
+ end
16
+ end
17
+
18
+ UNIXSocket.include ::Expedite::Protocol
@@ -0,0 +1,322 @@
1
+ # Based on https://github.com/rails/spring/blob/master/lib/spring/application.rb
2
+ require 'json'
3
+ require 'pty'
4
+ require 'set'
5
+ require 'socket'
6
+ require 'expedite/actions'
7
+ require 'expedite/env'
8
+ require 'expedite/failsafe_thread'
9
+ require 'expedite/protocol'
10
+ require 'expedite/signals'
11
+ require 'expedite/agents'
12
+
13
+ module Expedite
14
+ def self.agent
15
+ app.agent
16
+ end
17
+
18
+ def self.app=(app)
19
+ @app = app
20
+ end
21
+ def self.app
22
+ @app
23
+ end
24
+
25
+ module Server
26
+ class Agent
27
+ include Signals
28
+
29
+ attr_reader :agent
30
+ attr_reader :manager, :env
31
+
32
+ def initialize(agent:, manager:, env:)
33
+ @agent = agent
34
+ @manager = manager
35
+ @env = env
36
+ @mutex = Mutex.new
37
+ @waiting = Set.new
38
+ @preloaded = false
39
+ @state = :initialized
40
+ @interrupt = IO.pipe
41
+ end
42
+
43
+ def boot
44
+ # This is necessary for the terminal to work correctly when we reopen stdin.
45
+ Process.setsid rescue Errno::EPERM
46
+
47
+ Expedite.app = self
48
+
49
+ Signal.trap("TERM") { terminate }
50
+
51
+ env.load_helper
52
+ eager_preload if false #if ENV.delete("SPRING_PRELOAD") == "1"
53
+ run
54
+ end
55
+
56
+ def state(val)
57
+ return if exiting?
58
+ log "#{@state} -> #{val}"
59
+ @state = val
60
+ end
61
+
62
+ def state!(val)
63
+ state val
64
+ @interrupt.last.write "."
65
+ end
66
+
67
+ def app_name
68
+ env.app_name
69
+ end
70
+
71
+ def log(message)
72
+ env.log "[application:#{agent}] #{message}"
73
+ end
74
+
75
+ def preloaded?
76
+ @preloaded
77
+ end
78
+
79
+ def preload_failed?
80
+ @preloaded == :failure
81
+ end
82
+
83
+ def exiting?
84
+ @state == :exiting
85
+ end
86
+
87
+ def terminating?
88
+ @state == :terminating
89
+ end
90
+
91
+ def initialized?
92
+ @state == :initialized
93
+ end
94
+
95
+ def preload
96
+ log "preloading app"
97
+
98
+ @preloaded = :success
99
+ rescue Exception => e
100
+ @preloaded = :failure
101
+ raise e unless initialized?
102
+ end
103
+
104
+ def eager_preload
105
+ with_pty { preload }
106
+ end
107
+
108
+ def run
109
+ $0 = "expedite agent | #{app_name} | #{agent}"
110
+
111
+ Expedite::Agents.lookup(agent).after_fork(agent)
112
+
113
+ state :running
114
+ manager.puts
115
+
116
+ loop do
117
+ IO.select [manager, @interrupt.first]
118
+
119
+ if terminating? || preload_failed?
120
+ exit
121
+ else
122
+ serve manager.recv_io(UNIXSocket)
123
+ end
124
+ end
125
+ end
126
+
127
+ def serve(client)
128
+ log "got client"
129
+ manager.puts
130
+
131
+ _stdout, stderr, _stdin = streams = 3.times.map { client.recv_io }
132
+ [STDOUT, STDERR, STDIN].zip(streams).each { |a, b| a.reopen(b) }
133
+
134
+ preload unless preloaded?
135
+
136
+ args, env = client.recv_object.values_at("args", "env")
137
+
138
+ exec_name = args.shift
139
+ action = Expedite::Actions.lookup(exec_name)
140
+ action.setup(client)
141
+
142
+ connect_database
143
+
144
+ pid = fork do
145
+ Process.setsid
146
+ IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
147
+ trap("TERM", "DEFAULT")
148
+
149
+ # Load in the current env vars, except those which *were* changed when Spring started
150
+ env.each { |k, v| ENV[k] = v }
151
+
152
+ # requiring is faster, so if config.cache_classes was true in
153
+ # the environment's config file, then we can respect that from
154
+ # here on as we no longer need constant reloading.
155
+ if @original_cache_classes
156
+ ActiveSupport::Dependencies.mechanism = :require
157
+ Rails.application.config.cache_classes = true
158
+ end
159
+
160
+ connect_database
161
+ srand
162
+
163
+ invoke_after_fork_callbacks
164
+ shush_backtraces
165
+
166
+ begin
167
+ ret = action.call(*args)
168
+ rescue => e
169
+ client.send_object("exception" => e)
170
+ else
171
+ client.send_object("return" => ret )
172
+ end
173
+ end
174
+
175
+ disconnect_database
176
+
177
+ log "forked #{pid}"
178
+ manager.puts pid
179
+
180
+ # Boot makes a new application, so we don't wait for it
181
+ if action.is_a?(Expedite::Action::Boot)
182
+ Process.detach(pid)
183
+ else
184
+ wait pid, streams, client
185
+ end
186
+ rescue Exception => e
187
+ log "exception: #{e} at #{e.backtrace.join("\n")}"
188
+ manager.puts unless pid
189
+
190
+ if streams && !e.is_a?(SystemExit)
191
+ print_exception(stderr, e)
192
+ streams.each(&:close)
193
+ end
194
+
195
+ client.puts(1) if pid
196
+ client.close
197
+ ensure
198
+ # Redirect STDOUT and STDERR to prevent from keeping the original FDs
199
+ # (i.e. to prevent `spring rake -T | grep db` from hanging forever),
200
+ # even when exception is raised before forking (i.e. preloading).
201
+ reset_streams
202
+ end
203
+
204
+ def terminate
205
+ if exiting?
206
+ # Ensure that we do not ignore subsequent termination attempts
207
+ log "forced exit"
208
+ @waiting.each { |pid| Process.kill("TERM", pid) }
209
+ Kernel.exit
210
+ else
211
+ state! :terminating
212
+ end
213
+ end
214
+
215
+ def exit
216
+ state :exiting
217
+ manager.shutdown(:RDWR)
218
+ exit_if_finished
219
+ sleep
220
+ end
221
+
222
+ def exit_if_finished
223
+ @mutex.synchronize {
224
+ Kernel.exit if exiting? && @waiting.empty?
225
+ }
226
+ end
227
+
228
+ def invoke_after_fork_callbacks
229
+ # TODO:
230
+ end
231
+
232
+ def disconnect_database
233
+ ActiveRecord::Base.remove_connection if active_record_configured?
234
+ end
235
+
236
+ def connect_database
237
+ ActiveRecord::Base.establish_connection if active_record_configured?
238
+ end
239
+
240
+ # This feels very naughty
241
+ def shush_backtraces
242
+ Kernel.module_eval do
243
+ old_raise = Kernel.method(:raise)
244
+ remove_method :raise
245
+ define_method :raise do |*args|
246
+ begin
247
+ old_raise.call(*args)
248
+ ensure
249
+ if $!
250
+ lib = File.expand_path("..", __FILE__)
251
+ $!.backtrace.reject! { |line| line.start_with?(lib) }
252
+ end
253
+ end
254
+ end
255
+ private :raise
256
+ end
257
+ end
258
+
259
+ def print_exception(stream, error)
260
+ first, rest = error.backtrace.first, error.backtrace.drop(1)
261
+ stream.puts("#{first}: #{error} (#{error.class})")
262
+ rest.each { |line| stream.puts("\tfrom #{line}") }
263
+ end
264
+
265
+ def with_pty
266
+ PTY.open do |master, slave|
267
+ [STDOUT, STDERR, STDIN].each { |s| s.reopen slave }
268
+ reader_thread = Expedite.failsafe_thread { master.read }
269
+ begin
270
+ yield
271
+ ensure
272
+ reader_thread.kill
273
+ reset_streams
274
+ end
275
+ end
276
+ end
277
+
278
+ def reset_streams
279
+ [STDOUT, STDERR].each do |stream|
280
+ stream.reopen(env.log_file)
281
+ end
282
+ STDIN.reopen("/dev/null")
283
+ end
284
+
285
+ def wait(pid, streams, client)
286
+ @mutex.synchronize { @waiting << pid }
287
+
288
+ # Wait in a separate thread so we can run multiple actions at once
289
+ Expedite.failsafe_thread {
290
+ begin
291
+ _, status = Process.wait2 pid
292
+ log "#{pid} exited with #{status.exitstatus}"
293
+
294
+ streams.each(&:close)
295
+ client.puts(status.exitstatus)
296
+ client.close
297
+ ensure
298
+ @mutex.synchronize { @waiting.delete pid }
299
+ exit_if_finished
300
+ end
301
+ }
302
+
303
+ Expedite.failsafe_thread {
304
+ while signal = client.gets.chomp
305
+ begin
306
+ Process.kill(signal, -Process.getpgid(pid))
307
+ client.puts(0)
308
+ rescue Errno::ESRCH
309
+ client.puts(1)
310
+ end
311
+ end
312
+ }
313
+ end
314
+
315
+ private
316
+
317
+ def active_record_configured?
318
+ defined?(ActiveRecord::Base) && ActiveRecord::Base.configurations.any?
319
+ end
320
+ end
321
+ end
322
+ end
@@ -1,7 +1,7 @@
1
- require "expedite/application"
1
+ require "expedite/server/agent"
2
2
 
3
- app = Expedite::Application.new(
4
- variant: ENV['EXPEDITE_VARIANT'],
3
+ app = Expedite::Server::Agent.new(
4
+ agent: ENV['EXPEDITE_VARIANT'],
5
5
  manager: UNIXSocket.for_fd(3),
6
6
  env: Expedite::Env.new(
7
7
  root: ENV['EXPEDITE_ROOT'],
@@ -0,0 +1,191 @@
1
+ # Based on https://github.com/rails/spring/blob/master/lib/spring/application_manager.rb
2
+
3
+ require 'bundler'
4
+ require 'expedite/failsafe_thread'
5
+ require 'expedite/protocol'
6
+ require 'expedite/agents'
7
+
8
+ module Expedite
9
+ module Server
10
+ class AgentManager
11
+ attr_reader :pid, :child, :name, :env, :status, :agent
12
+
13
+ def initialize(name, env)
14
+ @name = name.to_s
15
+ @env = env
16
+ @mutex = Mutex.new
17
+ @state = :running
18
+ @pid = nil
19
+
20
+ @agent = Expedite::Agents.lookup(@name)
21
+ end
22
+
23
+ def log(message)
24
+ env.log "[application_manager:#{name}] #{message}"
25
+ end
26
+
27
+ # We're not using @mutex.synchronize to avoid the weird "<internal:prelude>:10"
28
+ # line which messes with backtraces in e.g. rspec
29
+ def synchronize
30
+ @mutex.lock
31
+ yield
32
+ ensure
33
+ @mutex.unlock
34
+ end
35
+
36
+ def start
37
+ start_child
38
+ end
39
+
40
+ def restart
41
+ return if @state == :stopping
42
+ start_child(true)
43
+ end
44
+
45
+ def alive?
46
+ @pid
47
+ end
48
+
49
+ def with_child
50
+ synchronize do
51
+ if alive?
52
+ begin
53
+ yield child
54
+ rescue Errno::ECONNRESET, Errno::EPIPE
55
+ # The child has died but has not been collected by the wait thread yet,
56
+ # so start a new child and try again.
57
+ log "child dead; starting"
58
+ start
59
+ yield child
60
+ end
61
+ else
62
+ log "child not running; starting"
63
+ start
64
+ yield child
65
+ end
66
+ end
67
+ end
68
+
69
+ # Returns the pid of the process running the command, or nil if the application process died.
70
+ def run(client)
71
+ with_child do |child|
72
+ child.send_io client
73
+ child.gets or raise Errno::EPIPE
74
+ end
75
+
76
+ pid = child.gets.to_i
77
+
78
+ unless pid.zero?
79
+ log "got worker pid #{pid}"
80
+ pid
81
+ end
82
+ rescue Errno::ECONNRESET, Errno::EPIPE => e
83
+ log "#{e} while reading from child; returning no pid"
84
+ nil
85
+ ensure
86
+ client.close
87
+ end
88
+
89
+ def stop
90
+ log "stopping"
91
+ @state = :stopping
92
+
93
+ if pid
94
+ Process.kill('TERM', pid)
95
+ Process.wait(pid)
96
+ end
97
+ rescue Errno::ESRCH, Errno::ECHILD
98
+ # Don't care
99
+ end
100
+
101
+ def keep_alive
102
+ agent.keep_alive
103
+ end
104
+
105
+ def parent
106
+ agent.parent
107
+ end
108
+
109
+ private
110
+
111
+ def start_child(preload = false)
112
+ if parent
113
+ fork_child(preload)
114
+ else
115
+ spawn_child(preload)
116
+ end
117
+ end
118
+
119
+ def fork_child(preload = false)
120
+ @child, child_socket = UNIXSocket.pair
121
+
122
+ # Compose command
123
+ wr, rd = UNIXSocket.pair
124
+ wr.send_io STDOUT
125
+ wr.send_io STDERR
126
+ wr.send_io STDIN
127
+
128
+ wr.send_object(
129
+ 'args' => ['expedite/boot', name],
130
+ 'env' => {}
131
+ )
132
+
133
+ wr.send_io child_socket
134
+ wr.send_io env.log_file
135
+ wr.close
136
+
137
+ @pid = env.applications[parent].run(rd)
138
+
139
+ start_wait_thread(pid, child) if child.gets
140
+ child_socket.close
141
+ end
142
+
143
+ def spawn_child(preload = false)
144
+ @child, child_socket = UNIXSocket.pair
145
+
146
+ Bundler.with_original_env do
147
+ puts File.expand_path("..", __FILE__)
148
+ bundler_dir = File.expand_path("../..", $LOADED_FEATURES.grep(/bundler\/setup\.rb$/).first)
149
+ @pid = Process.spawn(
150
+ {
151
+ "EXPEDITE_VARIANT" => name,
152
+ "EXPEDITE_ROOT" => env.root,
153
+ },
154
+ "ruby",
155
+ *(bundler_dir != RbConfig::CONFIG["rubylibdir"] ? ["-I", bundler_dir] : []),
156
+ "-I", File.expand_path("../../..", __FILE__),
157
+ "-e", "require 'expedite/server/agent_boot'",
158
+ 3 => child_socket,
159
+ 4 => env.log_file,
160
+ )
161
+ end
162
+
163
+ start_wait_thread(pid, child) if child.gets
164
+ child_socket.close
165
+ end
166
+
167
+ def start_wait_thread(pid, child)
168
+ Process.detach(pid)
169
+
170
+ Expedite.failsafe_thread do
171
+ # The recv can raise an ECONNRESET, killing the thread, but that's ok
172
+ # as if it does we're no longer interested in the child
173
+ loop do
174
+ IO.select([child])
175
+ break if child.recv(1, Socket::MSG_PEEK).empty?
176
+ sleep 0.01
177
+ end
178
+
179
+ log "child #{pid} shutdown"
180
+
181
+ synchronize {
182
+ if @pid == pid
183
+ @pid = nil
184
+ restart if keep_alive
185
+ end
186
+ }
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end