expedite 0.1.1 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 909035b6bad8ef3ccf6333cd38e136fd0c935197c2d467fe962ffc6bb04cb13c
4
- data.tar.gz: 7f375e9c3d42219f7d737300fc9ea89db06ca51de3e8514fa515ebd144a9d2b6
3
+ metadata.gz: da93957645e66819877d4c8638d2d42eb4880c631d6b4f8f224a3a9502c45a36
4
+ data.tar.gz: 179e78a453744f0375f6b184ffa1a2cff321b7ab92deafc9be5297fb00c63c4b
5
5
  SHA512:
6
- metadata.gz: 1ef388a4e305be7d6ece922be81b72d333e29d307de0e09e8e65ceb77562ade04b83394fe626028f4a8e75ada8cef00b20e798f4abd6243bcb6cd1e652f99dd8
7
- data.tar.gz: e0bce02b6ed7a305dea12daf4cccc768fc13919bc375883f5f55fd9398f339da0e51bc1423d85be23012c605eebb0b6f90986ff4834630ef89ea71484b9365a9
6
+ metadata.gz: 8cf3e2d403eab2d5cb7960a1d5cc43a4f94c3d0495ce36b360d32ec77ad7e5f9887a0e7aca183afa2e441ac3ff23a555ec6254f8441d971783376bdfc84db471
7
+ data.tar.gz: 30f00102d8d8c6cc59f138841caa116421f3002871551dca20718dfc5100aec80f469e8bb31b1d1ee8158020852090b2c053bff5d5fd7c9c728fdc26e4c3c6d0
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
@@ -58,7 +58,16 @@ module Expedite
58
58
  def run
59
59
  status = perform(*@args)
60
60
 
61
- exit status.to_i
61
+ code = if status.respond_to?(:to_i)
62
+ status.to_i
63
+ elsif status == true
64
+ 0
65
+ elsif status == false
66
+ 1
67
+ else
68
+ 126
69
+ end
70
+ exit code
62
71
  rescue Errno::ECONNRESET
63
72
  exit 1
64
73
  end
@@ -49,7 +49,7 @@ module Expedite
49
49
  def connect_to_agent(client, args)
50
50
  server.send_io client
51
51
 
52
- server.send_object("args" => args, "agent" => agent)
52
+ server.send_object({"args" => args, "agent" => agent}, env)
53
53
 
54
54
  if IO.select([server], [], [], CONNECT_TIMEOUT)
55
55
  server.gets or raise CommandNotFound
@@ -65,7 +65,11 @@ module Expedite
65
65
  agent.send_io STDERR
66
66
  agent.send_io STDIN
67
67
 
68
- agent.send_object("args" => args, "env" => ENV.to_hash)
68
+ agent.send_object({
69
+ "args" => args,
70
+ "env" => ENV.to_hash,
71
+ "method" => "invoke"
72
+ }, env)
69
73
 
70
74
  pid = server.gets
71
75
  pid = pid.chomp if pid
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,23 @@ 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")
137
144
 
138
145
  exec_name = args.shift
139
146
  action = Expedite::Actions.lookup(exec_name)
140
147
  action.setup(client)
141
148
 
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
149
+ connect_database # why are we connecting prior? is this for invoke?
150
+ pid = case method
151
+ when "invoke"
152
+ serve_invoke(client, action, args, env)
153
+ else
154
+ serve_fork(client, action, args, env)
173
155
  end
174
156
 
175
157
  disconnect_database
176
158
 
177
- log "forked #{pid}"
159
+ log "forked #{pid}" # pid is current process
178
160
  manager.puts pid
179
161
 
180
162
  # Boot makes a new application, so we don't wait for it
@@ -201,6 +183,51 @@ module Expedite
201
183
  reset_streams
202
184
  end
203
185
 
186
+ # Returns pid of the current process
187
+ def serve_invoke(client, action, args, env)
188
+ begin
189
+ ret = action.call(*args)
190
+ rescue Exception => e
191
+ client.send_object({"exception" => e}, self.env)
192
+ else
193
+ client.send_object({"return" => ret}, self.env)
194
+ end
195
+ Process.pid
196
+ end
197
+
198
+ def serve_fork(client, action, args, env)
199
+ fork do
200
+ Process.setsid
201
+ IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
202
+ trap("TERM", "DEFAULT")
203
+
204
+ # Load in the current env vars, except those which *were* changed when Spring started
205
+ env.each { |k, v| ENV[k] = v }
206
+
207
+ # requiring is faster, so if config.cache_classes was true in
208
+ # the environment's config file, then we can respect that from
209
+ # here on as we no longer need constant reloading.
210
+ if @original_cache_classes
211
+ ActiveSupport::Dependencies.mechanism = :require
212
+ Rails.application.config.cache_classes = true
213
+ end
214
+
215
+ connect_database
216
+ srand
217
+
218
+ invoke_after_fork_callbacks
219
+ shush_backtraces
220
+
221
+ begin
222
+ ret = action.call(*args)
223
+ rescue => e
224
+ client.send_object({"exception" => e}, self.env)
225
+ else
226
+ client.send_object({"return" => ret}, self.env)
227
+ end
228
+ end
229
+ end
230
+
204
231
  def terminate
205
232
  if exiting?
206
233
  # Ensure that we do not ignore subsequent termination attempts
@@ -283,19 +310,28 @@ module Expedite
283
310
  end
284
311
 
285
312
  def wait(pid, streams, client)
286
- @mutex.synchronize { @waiting << pid }
313
+ if pid != Process.pid
314
+ @mutex.synchronize { @waiting << pid }
315
+ end
287
316
 
288
317
  # Wait in a separate thread so we can run multiple actions at once
289
318
  Expedite.failsafe_thread {
290
319
  begin
291
- _, status = Process.wait2 pid
292
- log "#{pid} exited with #{status.exitstatus}"
320
+ exitstatue = if pid == Process.pid
321
+ log "#{pid} is current process"
322
+ 0
323
+ else
324
+ _, status = Process.wait2 pid
325
+ log "#{pid} exited with #{status.exitstatus}"
326
+ end
293
327
 
294
328
  streams.each(&:close)
295
- client.puts(status.exitstatus)
329
+ client.puts(exitstatus)
296
330
  client.close
297
331
  ensure
298
- @mutex.synchronize { @waiting.delete pid }
332
+ if pid != Process.pid
333
+ @mutex.synchronize { @waiting.delete pid }
334
+ end
299
335
  exit_if_finished
300
336
  end
301
337
  }
@@ -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.0'
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.0
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-16 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,6 +25,7 @@ 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
@@ -38,6 +39,8 @@ files:
38
39
  - lib/expedite/server/agent.rb
39
40
  - lib/expedite/server/agent_boot.rb
40
41
  - lib/expedite/server/agent_manager.rb
42
+ - lib/expedite/server/agent_pool.rb
43
+ - lib/expedite/server/application_manager.rb
41
44
  - lib/expedite/server/controller.rb
42
45
  - lib/expedite/signals.rb
43
46
  - lib/expedite/syntax.rb