expedite 0.0.2 → 0.1.1

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/lib/expedite/env.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'digest'
2
2
  require 'pathname'
3
3
  require 'expedite/version'
4
+ require 'expedite/server/agent_manager'
4
5
 
5
6
  module Expedite
6
7
  class Env
@@ -18,7 +19,7 @@ module Expedite
18
19
 
19
20
  env = self
20
21
  @applications = Hash.new do |h, k|
21
- h[k] = ApplicationManager.new(k, env)
22
+ h[k] = Server::AgentManager.new(k, env)
22
23
  end
23
24
  end
24
25
 
@@ -5,4 +5,10 @@ module Expedite
5
5
 
6
6
  class CommandNotFound < Error
7
7
  end
8
+
9
+ class UnknownError < Error
10
+ end
11
+
12
+ class AgentNotFoundError < Error
13
+ end
8
14
  end
@@ -0,0 +1,14 @@
1
+
2
+ Expedite.define do
3
+ agent :rails_environment do
4
+ app_root = Dir.pwd
5
+
6
+ require "#{app_root}/config/boot.rb"
7
+
8
+ require "rack"
9
+ rackup_file = "#{app_root}/config.ru"
10
+ Rack::Builder.load_file(rackup_file)
11
+
12
+ Rails.application.eager_load!
13
+ end
14
+ end
@@ -0,0 +1,20 @@
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.to_i
9
+ self.write data
10
+ end
11
+
12
+ def recv_object
13
+ len = self.gets.to_i
14
+ data = self.read(len)
15
+ Marshal.load(data)
16
+ end
17
+ end
18
+ end
19
+
20
+ IO.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,192 @@
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 Exception => e
83
+ # NotImplementedError is an Exception, not StandardError
84
+ client.send_object("exception" => e)
85
+ return Process.pid
86
+ rescue Errno::ECONNRESET, Errno::EPIPE => e
87
+ log "#{e} while reading from child; returning no pid"
88
+ nil
89
+ ensure
90
+ client.close
91
+ end
92
+
93
+ def stop
94
+ log "stopping"
95
+ @state = :stopping
96
+
97
+ if pid
98
+ Process.kill('TERM', pid)
99
+ Process.wait(pid)
100
+ end
101
+ rescue Errno::ESRCH, Errno::ECHILD
102
+ # Don't care
103
+ end
104
+
105
+ def keep_alive
106
+ agent.keep_alive
107
+ end
108
+
109
+ def parent
110
+ agent.parent
111
+ end
112
+
113
+ private
114
+
115
+ def start_child(preload = false)
116
+ if parent
117
+ fork_child(preload)
118
+ else
119
+ spawn_child(preload)
120
+ end
121
+ end
122
+
123
+ def fork_child(preload = false)
124
+ @child, child_socket = UNIXSocket.pair
125
+
126
+ # Compose command
127
+ wr, rd = UNIXSocket.pair
128
+ wr.send_io STDOUT
129
+ wr.send_io STDERR
130
+ wr.send_io STDIN
131
+
132
+ wr.send_object(
133
+ 'args' => ['expedite/boot', name],
134
+ 'env' => {}
135
+ )
136
+
137
+ wr.send_io child_socket
138
+ wr.send_io env.log_file
139
+ wr.close
140
+
141
+ @pid = env.applications[parent].run(rd)
142
+
143
+ start_wait_thread(pid, child) if child.gets
144
+ child_socket.close
145
+ end
146
+
147
+ def spawn_child(preload = false)
148
+ @child, child_socket = UNIXSocket.pair
149
+
150
+ bundler_dir = File.expand_path("../..", $LOADED_FEATURES.grep(/bundler\/setup\.rb$/).first)
151
+ @pid = Process.spawn(
152
+ {
153
+ "EXPEDITE_VARIANT" => name,
154
+ "EXPEDITE_ROOT" => env.root,
155
+ },
156
+ "ruby",
157
+ *(bundler_dir != RbConfig::CONFIG["rubylibdir"] ? ["-I", bundler_dir] : []),
158
+ "-I", File.expand_path("../../..", __FILE__),
159
+ "-e", "require 'expedite/server/agent_boot'",
160
+ 3 => child_socket,
161
+ 4 => env.log_file,
162
+ )
163
+
164
+ start_wait_thread(@pid, child) if child.gets
165
+ child_socket.close
166
+ end
167
+
168
+ def start_wait_thread(pid, child)
169
+ Process.detach(pid)
170
+
171
+ Expedite.failsafe_thread do
172
+ # The recv can raise an ECONNRESET, killing the thread, but that's ok
173
+ # as if it does we're no longer interested in the child
174
+ loop do
175
+ IO.select([child])
176
+ break if child.recv(1, Socket::MSG_PEEK).empty?
177
+ sleep 0.01
178
+ end
179
+
180
+ log "child #{pid} shutdown"
181
+
182
+ synchronize {
183
+ if @pid == pid
184
+ @pid = nil
185
+ restart if keep_alive
186
+ end
187
+ }
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end