expedite 0.1.1 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 909035b6bad8ef3ccf6333cd38e136fd0c935197c2d467fe962ffc6bb04cb13c
4
- data.tar.gz: 7f375e9c3d42219f7d737300fc9ea89db06ca51de3e8514fa515ebd144a9d2b6
3
+ metadata.gz: '0218f397835f80f748b0c255e8097aad9b8e8063122d15415a19f88e35546335'
4
+ data.tar.gz: 643e16d53333346a199595b2cb9fcab21116f53c606a505a40675d6ca074baaf
5
5
  SHA512:
6
- metadata.gz: 1ef388a4e305be7d6ece922be81b72d333e29d307de0e09e8e65ceb77562ade04b83394fe626028f4a8e75ada8cef00b20e798f4abd6243bcb6cd1e652f99dd8
7
- data.tar.gz: e0bce02b6ed7a305dea12daf4cccc768fc13919bc375883f5f55fd9398f339da0e51bc1423d85be23012c605eebb0b6f90986ff4834630ef89ea71484b9365a9
6
+ metadata.gz: c27ae0ac8775724ae0da4983e2f4ec04754e1cacd29bd632043c36f2c7634dd3914b92fc2e9d15f0b263418fdf0813e4d34aef7d4191fa0b14c368fed4993fb9
7
+ data.tar.gz: '09d8bdf813c976cffe6bfb3c2a6a31f14f1e0e1e6577a06a8e67e2be66271dc09a1180723bda57e1fa91a891f152f0316c5d893f795357d33ba090139782b6aa'
data/README.md CHANGED
@@ -89,6 +89,13 @@ You can also start the server in the foreground.
89
89
  $ bundle exec expedite server
90
90
  ```
91
91
 
92
+ If `rails` is in the `Gemfile`, then you can also start the rails commands through expedite.
93
+
94
+ ```
95
+ $ bundle exec expedite rails console
96
+ $ echo "puts ActiveRecord::Base.connection" | bundle exec expedite rails runner -
97
+ ```
98
+
92
99
  ## Acknowledgements
93
100
 
94
101
  Expedite's server core is modified from [Spring](https://github.com/rails/spring)
@@ -35,9 +35,9 @@ module Expedite
35
35
  end
36
36
 
37
37
  def lookup(name)
38
- ret = @registrations[name]
39
- raise NotImplementedError, "Action #{name.inspect} not found" if ret.nil?
40
- ret
38
+ ret = @registrations[name.to_s]
39
+ raise NotImplementedError, "Action #{name.inspect} not found" if ret.nil?
40
+ ret
41
41
  end
42
42
 
43
43
  def register(name, klass_or_nil = nil, **named_options, &block)
@@ -47,7 +47,7 @@ module Expedite
47
47
  klass_or_nil.new(**named_options)
48
48
  end
49
49
 
50
- @registrations[name] = cmd
50
+ @registrations[name.to_s] = cmd
51
51
  end
52
52
 
53
53
  def reset
@@ -0,0 +1,13 @@
1
+ module Expedite
2
+ module Cli
3
+ class Rails
4
+ def run(args)
5
+ Expedite.agent(:rails_environment).exec(:rails_commands, args)
6
+ end
7
+
8
+ def summary
9
+ 'Starts the expedite server'
10
+ end
11
+ end
12
+ end
13
+ end
data/lib/expedite/cli.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'expedite/cli/rails'
1
2
  require 'expedite/cli/server'
2
3
  require 'expedite/cli/status'
3
4
  require 'expedite/cli/stop'
@@ -13,7 +14,7 @@ module Expedite
13
14
  puts
14
15
  puts "Commands:"
15
16
 
16
- cmds = Expedite::Cli::COMMANDS
17
+ cmds = Expedite::Cli.commands
17
18
  cmds.keys.sort!.each do |cmd|
18
19
  c = cmds[cmd].new
19
20
  puts " #{cmd}: #{c.summary}"
@@ -27,12 +28,18 @@ module Expedite
27
28
 
28
29
  module_function
29
30
 
30
- COMMANDS = {
31
- 'help' => Cli::Help,
32
- 'server' => Cli::Server,
33
- 'status' => Cli::Status,
34
- 'stop' => Cli::Stop,
35
- }
31
+ def commands
32
+ return @commands unless @commands.nil?
33
+
34
+ cmds = {
35
+ 'help' => Cli::Help,
36
+ 'server' => Cli::Server,
37
+ 'status' => Cli::Status,
38
+ 'stop' => Cli::Stop,
39
+ }
40
+ cmds['rails'] = Cli::Rails if Gem.loaded_specs["rails"]
41
+ @commands = cmds
42
+ end
36
43
 
37
44
  def run(args)
38
45
  command(args.first).run(args[1..])
@@ -42,7 +49,7 @@ module Expedite
42
49
  end
43
50
 
44
51
  def command(cmd)
45
- klass = COMMANDS[cmd]
52
+ klass = commands[cmd]
46
53
  raise UnknownCommandError, "Unknown command '#{cmd}'" if klass.nil?
47
54
  klass.new
48
55
  end
@@ -0,0 +1,165 @@
1
+ require 'socket'
2
+
3
+ require 'expedite/env'
4
+ require 'expedite/errors'
5
+ require 'expedite/protocol'
6
+
7
+ module Expedite
8
+ module Client
9
+ class Base
10
+ CONNECT_TIMEOUT = 1
11
+ BOOT_TIMEOUT = 20
12
+
13
+ attr_reader :args, :env, :agent
14
+ attr_reader :server
15
+
16
+ def initialize(env: nil, agent: nil)
17
+ @env = env || Env.new
18
+ @agent = agent
19
+
20
+ @server_booted = false
21
+ end
22
+
23
+ def call(*args)
24
+ begin
25
+ connect
26
+ rescue Errno::ENOENT, Errno::ECONNRESET, Errno::ECONNREFUSED
27
+ boot_server
28
+ connect
29
+ end
30
+
31
+ perform(*args)
32
+ ensure
33
+ server.close if server
34
+ end
35
+
36
+ def perform(*args)
37
+ verify_server_version
38
+
39
+ agent, client = UNIXSocket.pair
40
+ connect_to_agent(client, args)
41
+ run_command(client, agent, args)
42
+ end
43
+
44
+ def verify_server_version
45
+ server_version = server.gets.chomp
46
+ raise ArgumentError, "Server mismatch. Expected #{env.version}, got #{server_version}." if server_version != env.version
47
+ end
48
+
49
+ def connect_to_agent(client, args)
50
+ server.send_io client
51
+
52
+ server.send_object({"args" => args, "agent" => agent}, env)
53
+
54
+ if IO.select([server], [], [], CONNECT_TIMEOUT)
55
+ server.gets or raise CommandNotFound
56
+ else
57
+ raise "Error connecting to Expedite server"
58
+ end
59
+ end
60
+
61
+ def run_command(client, agent, args)
62
+ log "sending command"
63
+
64
+ agent.send_io STDOUT
65
+ agent.send_io STDERR
66
+ agent.send_io STDIN
67
+
68
+ agent.send_object({
69
+ "args" => args,
70
+ "env" => ENV.to_hash,
71
+ "method" => run_command_method,
72
+ }, env)
73
+
74
+ pid = server.gets
75
+ pid = pid.chomp if pid
76
+
77
+ # We must not close the client socket until we are sure that the application has
78
+ # received the FD. Otherwise the FD can end up getting closed while it's in the server
79
+ # socket buffer on OS X. This doesn't happen on Linux.
80
+ client.close
81
+
82
+ if pid && !pid.empty?
83
+ log "got pid: #{pid}"
84
+
85
+ ## suspend_resume_on_tstp_cont(pid)
86
+
87
+ ## forward_signals(application)
88
+ result = agent.recv_object
89
+ if result.key?("exception")
90
+ e = result["exception"]
91
+ log "got exception #{e}"
92
+ raise e
93
+ end
94
+
95
+ ret = result["return"]
96
+ log "got return value #{ret}"
97
+ return ret
98
+ else
99
+ log "got no pid"
100
+ raise UnknownError, "got no pid"
101
+ end
102
+ ensure
103
+ agent.close
104
+ end
105
+
106
+ def boot_server
107
+ env.socket_path.unlink if env.socket_path.exist?
108
+
109
+ pid = Process.spawn(gem_env, env.server_command, out: File::NULL)
110
+ timeout = Time.now + BOOT_TIMEOUT
111
+
112
+ @server_booted = true
113
+
114
+ until env.socket_path.exist?
115
+ _, status = Process.waitpid2(pid, Process::WNOHANG)
116
+
117
+ if status
118
+ # Server did not start
119
+ raise ArgumentError, "Server exited: #{status.exitstatus}"
120
+ elsif Time.now > timeout
121
+ $stderr.puts "Starting Expedite server with `#{env.server_command}` " \
122
+ "timed out after #{BOOT_TIMEOUT} seconds"
123
+ exit 1
124
+ end
125
+
126
+ sleep 0.1
127
+ end
128
+ end
129
+
130
+ def server_booted?
131
+ @server_booted
132
+ end
133
+
134
+ def stop_server
135
+ server.close
136
+ @server = nil
137
+ env.stop
138
+ end
139
+
140
+ def log(message)
141
+ env.log "[client] #{message}"
142
+ end
143
+
144
+ def connect
145
+ @server = UNIXSocket.open(env.socket_path)
146
+ end
147
+
148
+ def gem_env
149
+ bundle = Bundler.bundle_path.to_s
150
+ paths = Gem.path + ENV["GEM_PATH"].to_s.split(File::PATH_SEPARATOR)
151
+
152
+ {
153
+ "GEM_PATH" => [bundle, *paths].uniq.join(File::PATH_SEPARATOR),
154
+ "GEM_HOME" => bundle
155
+ }
156
+ end
157
+
158
+
159
+ protected
160
+ def run_command_method
161
+ raise NotImplementedError, "run_command_method must be overriden"
162
+ end
163
+ end
164
+ end
165
+ end
@@ -1,18 +1,10 @@
1
1
 
2
2
  # Based on https://github.com/rails/spring/blob/master/lib/spring/client/run.rb
3
-
4
- require 'bundler'
5
- require 'rbconfig'
6
- require 'socket'
7
-
8
- require 'expedite/client/invoke'
9
- require 'expedite/env'
10
- require 'expedite/errors'
11
- require 'expedite/protocol'
3
+ require 'expedite/client/base'
12
4
 
13
5
  module Expedite
14
6
  module Client
15
- class Exec < Invoke
7
+ class Exec < Base
16
8
  FORWARDED_SIGNALS = %w(INT QUIT USR1 USR2 INFO WINCH) & Signal.list.keys
17
9
 
18
10
  def initialize(env: nil, agent: nil)
@@ -58,7 +50,16 @@ module Expedite
58
50
  def run
59
51
  status = perform(*@args)
60
52
 
61
- exit status.to_i
53
+ code = if status.respond_to?(:to_i)
54
+ status.to_i
55
+ elsif status == true
56
+ 0
57
+ elsif status == false
58
+ 1
59
+ else
60
+ 126
61
+ end
62
+ exit code
62
63
  rescue Errno::ECONNRESET
63
64
  exit 1
64
65
  end
@@ -119,6 +120,11 @@ module Expedite
119
120
  application.puts(sig)
120
121
  application.gets.to_i
121
122
  end
123
+
124
+ protected
125
+ def run_command_method
126
+ "exec"
127
+ end
122
128
  end
123
129
  end
124
130
  end
@@ -1,154 +1,13 @@
1
1
  require 'socket'
2
2
 
3
- require 'expedite/env'
4
- require 'expedite/errors'
5
- require 'expedite/protocol'
3
+ require 'expedite/client/base'
6
4
 
7
5
  module Expedite
8
6
  module Client
9
- class Invoke
10
- CONNECT_TIMEOUT = 1
11
- BOOT_TIMEOUT = 20
12
-
13
- attr_reader :args, :env, :agent
14
- attr_reader :server
15
-
16
- def initialize(env: nil, agent: nil)
17
- @env = env || Env.new
18
- @agent = agent
19
-
20
- @server_booted = false
21
- end
22
-
23
- def call(*args)
24
- begin
25
- connect
26
- rescue Errno::ENOENT, Errno::ECONNRESET, Errno::ECONNREFUSED
27
- boot_server
28
- connect
29
- end
30
-
31
- perform(*args)
32
- ensure
33
- server.close if server
34
- end
35
-
36
- def perform(*args)
37
- verify_server_version
38
-
39
- agent, client = UNIXSocket.pair
40
- connect_to_agent(client, args)
41
- run_command(client, agent, args)
42
- end
43
-
44
- def verify_server_version
45
- server_version = server.gets.chomp
46
- raise ArgumentError, "Server mismatch. Expected #{env.version}, got #{server_version}." if server_version != env.version
47
- end
48
-
49
- def connect_to_agent(client, args)
50
- server.send_io client
51
-
52
- server.send_object("args" => args, "agent" => agent)
53
-
54
- if IO.select([server], [], [], CONNECT_TIMEOUT)
55
- server.gets or raise CommandNotFound
56
- else
57
- raise "Error connecting to Expedite server"
58
- end
59
- end
60
-
61
- def run_command(client, agent, args)
62
- log "sending command"
63
-
64
- agent.send_io STDOUT
65
- agent.send_io STDERR
66
- agent.send_io STDIN
67
-
68
- agent.send_object("args" => args, "env" => ENV.to_hash)
69
-
70
- pid = server.gets
71
- pid = pid.chomp if pid
72
-
73
- # We must not close the client socket until we are sure that the application has
74
- # received the FD. Otherwise the FD can end up getting closed while it's in the server
75
- # socket buffer on OS X. This doesn't happen on Linux.
76
- client.close
77
-
78
- if pid && !pid.empty?
79
- log "got pid: #{pid}"
80
-
81
- ## suspend_resume_on_tstp_cont(pid)
82
-
83
- ## forward_signals(application)
84
- result = agent.recv_object
85
- if result.key?("exception")
86
- e = result["exception"]
87
- log "got exception #{e}"
88
- raise e
89
- end
90
-
91
- ret = result["return"]
92
- log "got return value #{ret}"
93
- return ret
94
- else
95
- log "got no pid"
96
- raise UnknownError, "got no pid"
97
- end
98
- ensure
99
- agent.close
100
- end
101
-
102
- def boot_server
103
- env.socket_path.unlink if env.socket_path.exist?
104
-
105
- pid = Process.spawn(gem_env, env.server_command, out: File::NULL)
106
- timeout = Time.now + BOOT_TIMEOUT
107
-
108
- @server_booted = true
109
-
110
- until env.socket_path.exist?
111
- _, status = Process.waitpid2(pid, Process::WNOHANG)
112
-
113
- if status
114
- # Server did not start
115
- raise ArgumentError, "Server exited: #{status.exitstatus}"
116
- elsif Time.now > timeout
117
- $stderr.puts "Starting Expedite server with `#{env.server_command}` " \
118
- "timed out after #{BOOT_TIMEOUT} seconds"
119
- exit 1
120
- end
121
-
122
- sleep 0.1
123
- end
124
- end
125
-
126
- def server_booted?
127
- @server_booted
128
- end
129
-
130
- def stop_server
131
- server.close
132
- @server = nil
133
- env.stop
134
- end
135
-
136
- def log(message)
137
- env.log "[client] #{message}"
138
- end
139
-
140
- def connect
141
- @server = UNIXSocket.open(env.socket_path)
142
- end
143
-
144
- def gem_env
145
- bundle = Bundler.bundle_path.to_s
146
- paths = Gem.path + ENV["GEM_PATH"].to_s.split(File::PATH_SEPARATOR)
147
-
148
- {
149
- "GEM_PATH" => [bundle, *paths].uniq.join(File::PATH_SEPARATOR),
150
- "GEM_HOME" => bundle
151
- }
7
+ class Invoke < Base
8
+ protected
9
+ def run_command_method
10
+ "invoke"
152
11
  end
153
12
  end
154
13
  end
data/lib/expedite/env.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'digest'
2
2
  require 'pathname'
3
3
  require 'expedite/version'
4
- require 'expedite/server/agent_manager'
4
+ require 'expedite/server/application_manager'
5
5
 
6
6
  module Expedite
7
7
  class Env
@@ -18,9 +18,7 @@ module Expedite
18
18
  @application_id = Digest::SHA1.hexdigest(@root)
19
19
 
20
20
  env = self
21
- @applications = Hash.new do |h, k|
22
- h[k] = Server::AgentManager.new(k, env)
23
- end
21
+ @applications = Server::ApplicationManager.new(env)
24
22
  end
25
23
 
26
24
  def version
@@ -1,14 +1,14 @@
1
-
2
1
  Expedite.define do
2
+ # Agent that rails environment loaded
3
3
  agent :rails_environment do
4
4
  app_root = Dir.pwd
5
+ require "#{app_root}/config/environment.rb"
6
+ end
5
7
 
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!
8
+ # Actions that runs rails commands
9
+ action :rails_commands do |args|
10
+ ARGV.replace(args)
11
+ require "rails/commands"
12
+ true
13
13
  end
14
14
  end
@@ -2,7 +2,8 @@ require 'socket'
2
2
 
3
3
  module Expedite
4
4
  module Protocol
5
- def send_object(object)
5
+ def send_object(object, env)
6
+ env.log "send_object #{object.inspect}"
6
7
  data = Marshal.dump(object)
7
8
 
8
9
  self.puts data.bytesize.to_i
@@ -23,6 +23,13 @@ module Expedite
23
23
  end
24
24
 
25
25
  module Server
26
+ # This code runs in the process that has the actual code pre-loaded, and is
27
+ # used to serve requests.
28
+ # * An "invoke" request is handled in the agent itself.
29
+ # * A "fork" request causes the agent to fork, and the forked process handles
30
+ # the request.
31
+ # Each agent processes a single request at a time, unless it is an
32
+ # Expedite::Action::Boot request which is used to make derived agents.
26
33
  class Agent
27
34
  include Signals
28
35
 
@@ -125,7 +132,7 @@ module Expedite
125
132
  end
126
133
 
127
134
  def serve(client)
128
- log "got client"
135
+ puts "got client"
129
136
  manager.puts
130
137
 
131
138
  _stdout, stderr, _stdin = streams = 3.times.map { client.recv_io }
@@ -133,48 +140,25 @@ module Expedite
133
140
 
134
141
  preload unless preloaded?
135
142
 
136
- args, env = client.recv_object.values_at("args", "env")
143
+ args, env, method = client.recv_object.values_at("args", "env", "method")
144
+ log "serve #{args} using #{method}"
137
145
 
138
146
  exec_name = args.shift
139
147
  action = Expedite::Actions.lookup(exec_name)
140
148
  action.setup(client)
141
149
 
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
150
+ connect_database # why are we connecting prior? is this for invoke?
151
+ pid = case method
152
+ when "invoke"
153
+ # TODO: Invoke in a worker process instead of the preloader
154
+ serve_invoke(client, action, args, env)
155
+ else
156
+ serve_fork(client, action, args, env)
173
157
  end
174
158
 
175
159
  disconnect_database
176
160
 
177
- log "forked #{pid}"
161
+ log "forked #{pid}" # pid is current process
178
162
  manager.puts pid
179
163
 
180
164
  # Boot makes a new application, so we don't wait for it
@@ -201,6 +185,51 @@ module Expedite
201
185
  reset_streams
202
186
  end
203
187
 
188
+ # Returns pid of the current process
189
+ def serve_invoke(client, action, args, env)
190
+ begin
191
+ ret = action.call(*args)
192
+ rescue Exception => e
193
+ client.send_object({"exception" => e}, self.env)
194
+ else
195
+ client.send_object({"return" => ret}, self.env)
196
+ end
197
+ Process.pid
198
+ end
199
+
200
+ def serve_fork(client, action, args, env)
201
+ fork do
202
+ Process.setsid
203
+ IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
204
+ trap("TERM", "DEFAULT")
205
+
206
+ # Load in the current env vars, except those which *were* changed when Spring started
207
+ env.each { |k, v| ENV[k] = v }
208
+
209
+ # requiring is faster, so if config.cache_classes was true in
210
+ # the environment's config file, then we can respect that from
211
+ # here on as we no longer need constant reloading.
212
+ if @original_cache_classes
213
+ ActiveSupport::Dependencies.mechanism = :require
214
+ Rails.application.config.cache_classes = true
215
+ end
216
+
217
+ connect_database
218
+ srand
219
+
220
+ invoke_after_fork_callbacks
221
+ shush_backtraces
222
+
223
+ begin
224
+ ret = action.call(*args)
225
+ rescue => e
226
+ client.send_object({"exception" => e}, self.env)
227
+ else
228
+ client.send_object({"return" => ret}, self.env)
229
+ end
230
+ end
231
+ end
232
+
204
233
  def terminate
205
234
  if exiting?
206
235
  # Ensure that we do not ignore subsequent termination attempts
@@ -283,19 +312,28 @@ module Expedite
283
312
  end
284
313
 
285
314
  def wait(pid, streams, client)
286
- @mutex.synchronize { @waiting << pid }
315
+ if pid != Process.pid
316
+ @mutex.synchronize { @waiting << pid }
317
+ end
287
318
 
288
319
  # Wait in a separate thread so we can run multiple actions at once
289
320
  Expedite.failsafe_thread {
290
321
  begin
291
- _, status = Process.wait2 pid
292
- log "#{pid} exited with #{status.exitstatus}"
322
+ exitstatue = if pid == Process.pid
323
+ log "#{pid} is current process"
324
+ 0
325
+ else
326
+ _, status = Process.wait2 pid
327
+ log "#{pid} exited with #{status.exitstatus}"
328
+ end
293
329
 
294
330
  streams.each(&:close)
295
- client.puts(status.exitstatus)
331
+ client.puts(exitstatus)
296
332
  client.close
297
333
  ensure
298
- @mutex.synchronize { @waiting.delete pid }
334
+ if pid != Process.pid
335
+ @mutex.synchronize { @waiting.delete pid }
336
+ end
299
337
  exit_if_finished
300
338
  end
301
339
  }
@@ -7,6 +7,7 @@ require 'expedite/agents'
7
7
 
8
8
  module Expedite
9
9
  module Server
10
+ # Locates agent processes and spawns them if necessary
10
11
  class AgentManager
11
12
  attr_reader :pid, :child, :name, :env, :status, :agent
12
13
 
@@ -81,7 +82,7 @@ module Expedite
81
82
  end
82
83
  rescue Exception => e
83
84
  # NotImplementedError is an Exception, not StandardError
84
- client.send_object("exception" => e)
85
+ client.send_object({"exception" => e}, env)
85
86
  return Process.pid
86
87
  rescue Errno::ECONNRESET, Errno::EPIPE => e
87
88
  log "#{e} while reading from child; returning no pid"
@@ -120,6 +121,7 @@ module Expedite
120
121
  end
121
122
  end
122
123
 
124
+ # Creates a child that is forked from a parent
123
125
  def fork_child(preload = false)
124
126
  @child, child_socket = UNIXSocket.pair
125
127
 
@@ -129,21 +131,25 @@ module Expedite
129
131
  wr.send_io STDERR
130
132
  wr.send_io STDIN
131
133
 
132
- wr.send_object(
134
+ wr.send_object({
133
135
  'args' => ['expedite/boot', name],
134
- 'env' => {}
135
- )
136
+ 'env' => {},
137
+ 'method' => "fork",
138
+ }, env)
136
139
 
137
140
  wr.send_io child_socket
138
141
  wr.send_io env.log_file
139
142
  wr.close
140
143
 
141
- @pid = env.applications[parent].run(rd)
144
+ @pid = env.applications.with(parent) do |target|
145
+ target.run(rd)
146
+ end
142
147
 
143
148
  start_wait_thread(pid, child) if child.gets
144
149
  child_socket.close
145
150
  end
146
151
 
152
+ # Creates a child that is started from scratch
147
153
  def spawn_child(preload = false)
148
154
  @child, child_socket = UNIXSocket.pair
149
155
 
@@ -0,0 +1,39 @@
1
+ require 'set'
2
+ require 'expedite/server/agent_manager'
3
+
4
+ module Expedite
5
+ module Server
6
+ class AgentPool
7
+ def initialize(name, env)
8
+ @name = name
9
+ @env = env
10
+ @checked_in = []
11
+ @checked_out = Set.new
12
+ end
13
+
14
+ # Get a free agent from the pool
15
+ def checkout
16
+ agent = @checked_in.pop
17
+
18
+ agent = build_agent if agent.nil?
19
+ @checked_out.add(agent)
20
+
21
+ agent
22
+ end
23
+
24
+ def checkin(agent)
25
+ @checked_out.delete(agent)
26
+ @checked_in.push(agent)
27
+ end
28
+
29
+ def build_agent
30
+ Server::AgentManager.new(@name, @env)
31
+ end
32
+
33
+ # Returns all agents, both checked in and checked out
34
+ def all
35
+ @checked_in + @checked_out.to_a
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,27 @@
1
+ require 'expedite/server/agent_pool'
2
+
3
+ module Expedite
4
+ module Server
5
+ class ApplicationManager
6
+ attr_reader :pools
7
+
8
+ def initialize(env)
9
+ @env = env
10
+ @pools = Hash.new do |h, k|
11
+ h[k] = AgentPool.new(k, @env)
12
+ end
13
+ end
14
+
15
+ def with(name)
16
+ pool = @pools[name]
17
+ target = pool.checkout
18
+ begin
19
+ ret = yield target
20
+ ensure
21
+ pool.checkin(target)
22
+ end
23
+ ret
24
+ end
25
+ end
26
+ end
27
+ end
@@ -101,12 +101,12 @@ module Expedite
101
101
  app_client = client.recv_io
102
102
  command = client.recv_object
103
103
 
104
- args, agent = command.values_at('args', 'agent')
104
+ args, agent = command.values_at("args", "agent")
105
105
  cmd = args.first
106
106
 
107
- if agent == '__server__'
107
+ if agent == "__server__"
108
108
  case cmd
109
- when 'application_pids'
109
+ when "application_pids"
110
110
  # Corresponds to Client::Invoke#run_command
111
111
  client.puts
112
112
 
@@ -118,10 +118,10 @@ module Expedite
118
118
  client.puts Process.pid
119
119
 
120
120
  application_pids = []
121
- env.applications.each do |k, v|
122
- application_pids << v.pid if v.pid
121
+ env.applications.pools.each do |k, pool|
122
+ application_pids.concat(pool.all.map(&:pid))
123
123
  end
124
- unix_socket.send_object("return" => application_pids)
124
+ unix_socket.send_object({"return" => application_pids}, env)
125
125
 
126
126
  unix_socket.close
127
127
  client.close
@@ -134,9 +134,9 @@ module Expedite
134
134
  client.puts
135
135
 
136
136
  begin
137
- target = env.applications[agent]
138
-
139
- client.puts target.run(app_client)
137
+ env.applications.with(agent) do |target|
138
+ client.puts target.run(app_client)
139
+ end
140
140
  rescue AgentNotFoundError => e
141
141
  unix_socket = UNIXSocket.for_fd(app_client.fileno)
142
142
  _stdout = unix_socket.recv_io
@@ -150,7 +150,7 @@ module Expedite
150
150
  # boot only
151
151
  #@child_socket = client.recv_io
152
152
  #@log_file = client.recv_io
153
- unix_socket.send_object("exception" => e)
153
+ unix_socket.send_object({"exception" => e}, env)
154
154
 
155
155
  unix_socket.close
156
156
  client.close
@@ -195,7 +195,13 @@ module Expedite
195
195
  end
196
196
  end
197
197
 
198
- env.applications.values.map { |a| Expedite.failsafe_thread { a.stop } }.map(&:join)
198
+ thrs = []
199
+ env.applications.pools.each do |k, pool|
200
+ pool.all.each do |a|
201
+ thrs << Expedite.failsafe_thread { a.stop }
202
+ end
203
+ end
204
+ thrs.map(&:join)
199
205
  end
200
206
 
201
207
  def write_pidfile
@@ -1,3 +1,3 @@
1
1
  module Expedite
2
- VERSION = '0.1.1'
2
+ VERSION = '0.2.1'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: expedite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bing-Chang Lai
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-08 00:00:00.000000000 Z
11
+ date: 2022-12-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Manages Ruby processes that can be used to spawn child processes faster.
14
14
  email: johnny.lai@me.com
@@ -25,9 +25,11 @@ files:
25
25
  - lib/expedite/actions.rb
26
26
  - lib/expedite/agents.rb
27
27
  - lib/expedite/cli.rb
28
+ - lib/expedite/cli/rails.rb
28
29
  - lib/expedite/cli/server.rb
29
30
  - lib/expedite/cli/status.rb
30
31
  - lib/expedite/cli/stop.rb
32
+ - lib/expedite/client/base.rb
31
33
  - lib/expedite/client/exec.rb
32
34
  - lib/expedite/client/invoke.rb
33
35
  - lib/expedite/env.rb
@@ -38,6 +40,8 @@ files:
38
40
  - lib/expedite/server/agent.rb
39
41
  - lib/expedite/server/agent_boot.rb
40
42
  - lib/expedite/server/agent_manager.rb
43
+ - lib/expedite/server/agent_pool.rb
44
+ - lib/expedite/server/application_manager.rb
41
45
  - lib/expedite/server/controller.rb
42
46
  - lib/expedite/signals.rb
43
47
  - lib/expedite/syntax.rb