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.
@@ -0,0 +1,247 @@
1
+ # Based on https://github.com/rails/spring/blob/master/lib/spring/server.rb
2
+ require 'json'
3
+ require 'socket'
4
+ require "expedite/env"
5
+ require "expedite/protocol"
6
+ require "expedite/signals"
7
+
8
+ module Expedite
9
+ module Server
10
+ class Controller
11
+ include Signals
12
+
13
+ def self.boot(options = {})
14
+ new(options).boot
15
+ end
16
+
17
+ attr_reader :env
18
+
19
+ def initialize(foreground: true, env: nil)
20
+ @foreground = foreground
21
+ @env = env || default_env
22
+ @pidfile = @env.pidfile_path.open('a')
23
+ @mutex = Mutex.new
24
+ end
25
+
26
+ def foreground?
27
+ @foreground
28
+ end
29
+
30
+ def log(message)
31
+ env.log "[server] #{message}"
32
+ end
33
+
34
+ def boot
35
+ env.load_helper
36
+
37
+ write_pidfile
38
+ set_pgid unless foreground?
39
+ ignore_signals unless foreground?
40
+ set_exit_hook
41
+ set_process_title
42
+ start_server
43
+ exit 0
44
+ end
45
+
46
+ def pid
47
+ @env.pidfile_path.read.to_i
48
+ rescue Errno::ENOENT
49
+ nil
50
+ end
51
+
52
+ def running?
53
+ pidfile = @env.pidfile_path.open('r+')
54
+ !pidfile.flock(File::LOCK_EX | File::LOCK_NB)
55
+ rescue Errno::ENOENT
56
+ false
57
+ ensure
58
+ if pidfile
59
+ pidfile.flock(File::LOCK_UN)
60
+ pidfile.close
61
+ end
62
+ end
63
+
64
+ # timeout: Defaults to 2 seconds
65
+ def stop
66
+ if running?
67
+ timeout = Time.now + @env.graceful_termination_timeout
68
+ kill 'TERM'
69
+ sleep 0.1 until !running? || Time.now >= timeout
70
+
71
+ if running?
72
+ kill 'KILL'
73
+ :killed
74
+ else
75
+ :stopped
76
+ end
77
+ else
78
+ :not_running
79
+ end
80
+ end
81
+
82
+ def kill(sig)
83
+ pid = self.pid
84
+ Process.kill(sig, pid) if pid
85
+ rescue Errno::ESRCH
86
+ # already dead
87
+ end
88
+
89
+ def start_server
90
+ server = UNIXServer.open(env.socket_path)
91
+ log "started on #{env.socket_path}"
92
+ loop { serve server.accept }
93
+ rescue Interrupt
94
+ end
95
+
96
+ def serve(client)
97
+ log "accepted client"
98
+ client.puts env.version
99
+
100
+ # Corresponds to Client::Invoke#connect_to_agent
101
+ app_client = client.recv_io
102
+ command = client.recv_object
103
+
104
+ args, agent = command.values_at('args', 'agent')
105
+ cmd = args.first
106
+
107
+ if agent == '__server__'
108
+ case cmd
109
+ when 'application_pids'
110
+ # Corresponds to Client::Invoke#run_command
111
+ client.puts
112
+
113
+ unix_socket = UNIXSocket.for_fd(app_client.fileno)
114
+ _stdout = unix_socket.recv_io
115
+ _stderr = unix_socket.recv_io
116
+ _stdin = unix_socket.recv_io
117
+
118
+ client.puts Process.pid
119
+
120
+ application_pids = []
121
+ env.applications.each do |k, v|
122
+ application_pids << v.pid if v.pid
123
+ end
124
+ unix_socket.send_object("return" => application_pids)
125
+
126
+ unix_socket.close
127
+ client.close
128
+ else
129
+ end
130
+ elsif Expedite::Actions.lookup(cmd)
131
+ # Corresponds to Client::Invoke#run_command
132
+ log "running command #{cmd}: #{args}"
133
+
134
+ client.puts
135
+
136
+ begin
137
+ target = env.applications[agent]
138
+
139
+ client.puts target.run(app_client)
140
+ rescue AgentNotFoundError => e
141
+ unix_socket = UNIXSocket.for_fd(app_client.fileno)
142
+ _stdout = unix_socket.recv_io
143
+ _stderr = unix_socket.recv_io
144
+ _stdin = unix_socket.recv_io
145
+
146
+ args, env = unix_socket.recv_object.values_at("args", "env")
147
+
148
+ client.puts Process.pid
149
+
150
+ # boot only
151
+ #@child_socket = client.recv_io
152
+ #@log_file = client.recv_io
153
+ unix_socket.send_object("exception" => e)
154
+
155
+ unix_socket.close
156
+ client.close
157
+ end
158
+ else
159
+ log "command not found #{cmd}"
160
+ client.close
161
+ end
162
+ rescue AgentNotFoundError => e
163
+ rescue SocketError => e
164
+ raise e unless client.eof?
165
+ ensure
166
+ redirect_output
167
+ end
168
+
169
+ # Boot the server into the process group of the current session.
170
+ # This will cause it to be automatically killed once the session
171
+ # ends (i.e. when the user closes their terminal).
172
+ def set_pgid
173
+ # Process.setpgid(0, SID.pgid)
174
+ end
175
+
176
+ # Ignore SIGINT and SIGQUIT otherwise the user typing ^C or ^\ on the command line
177
+ # will kill the server/application.
178
+ def ignore_signals
179
+ IGNORE_SIGNALS.each { |sig| trap(sig, "IGNORE") }
180
+ end
181
+
182
+ def set_exit_hook
183
+ server_pid = Process.pid
184
+
185
+ # We don't want this hook to run in any forks of the current process
186
+ at_exit { shutdown if Process.pid == server_pid }
187
+ end
188
+
189
+ def shutdown
190
+ log "shutting down"
191
+
192
+ [env.socket_path, env.pidfile_path].each do |path|
193
+ if path.exist?
194
+ path.unlink rescue nil
195
+ end
196
+ end
197
+
198
+ env.applications.values.map { |a| Expedite.failsafe_thread { a.stop } }.map(&:join)
199
+ end
200
+
201
+ def write_pidfile
202
+ if @pidfile.flock(File::LOCK_EX | File::LOCK_NB)
203
+ @pidfile.truncate(0)
204
+ @pidfile.write("#{Process.pid}\n")
205
+ @pidfile.fsync
206
+ else
207
+ raise "Failed to lock #{@env.pidfile_path}"
208
+ end
209
+ end
210
+
211
+ # We need to redirect STDOUT and STDERR, otherwise the server will
212
+ # keep the original FDs open which would break piping. (e.g.
213
+ # `spring rake -T | grep db` would hang forever because the server
214
+ # would keep the stdout FD open.)
215
+ def redirect_output
216
+ [STDOUT, STDERR].each { |stream| stream.reopen(env.log_file) }
217
+ end
218
+
219
+ def set_process_title
220
+ $0 = "expedite server | #{env.app_name}"
221
+ end
222
+
223
+ private
224
+
225
+ def default_env
226
+ Env.new(log_file: default_log_file)
227
+ end
228
+
229
+ def default_log_file
230
+ if foreground? && !ENV["SPRING_LOG"]
231
+ $stderr
232
+ else
233
+ nil
234
+ end
235
+ end
236
+
237
+ # Server command
238
+ def application_pids
239
+ pids = []
240
+ env.applications.each do |k, v|
241
+ pids << v.pid if v.pid
242
+ end
243
+ return pids
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,26 @@
1
+ require 'expedite/actions'
2
+ require 'expedite/agents'
3
+
4
+ module Expedite
5
+ module Syntax
6
+ def define(&block)
7
+ DSL.run(&block)
8
+ end
9
+
10
+ class DSL
11
+ def action(name, &block)
12
+ Expedite::Actions.register(name, &block)
13
+ end
14
+
15
+ def agent(name, parent:nil, &block)
16
+ Expedite::Agents.register(name, parent: parent, &block)
17
+ end
18
+
19
+ def self.run(&block)
20
+ new.instance_eval(&block)
21
+ end
22
+ end
23
+ end
24
+
25
+ extend Syntax
26
+ end
@@ -1,3 +1,3 @@
1
1
  module Expedite
2
- VERSION = '0.0.2'
2
+ VERSION = '0.1.1'
3
3
  end
data/lib/expedite.rb CHANGED
@@ -1,19 +1,33 @@
1
- require "expedite/client"
1
+ require 'expedite/client/exec'
2
+ require 'expedite/client/invoke'
3
+ require 'expedite/syntax'
2
4
 
3
5
  module Expedite
4
- ##
5
- # Returns a client to dispatch actions to the specified variant
6
- def self.variant(variant)
7
- @clients ||= Hash.new do |h, k|
8
- Client.new(env: Env.new, variant: variant)
6
+ class AgentProxy
7
+ attr_accessor :env, :agent
8
+
9
+ def initialize(env:, agent:)
10
+ self.env = env
11
+ self.agent = agent
12
+ end
13
+
14
+ def exec(*args)
15
+ Client::Exec.new(env: env, agent: agent).call(*args)
16
+ end
17
+
18
+ def invoke(*args)
19
+ Client::Invoke.new(env: env, agent: agent).call(*args)
9
20
  end
10
- @clients[variant]
11
21
  end
22
+ end
12
23
 
24
+ module Expedite
13
25
  ##
14
- # Alias for self.variant
15
- def self.v(variant)
16
- self.variant(variant)
26
+ # Returns a client to dispatch actions to the specified agent
27
+ def self.agent(agent)
28
+ @clients ||= Hash.new do |h, k|
29
+ AgentProxy.new(env: Env.new, agent: agent)
30
+ end
31
+ @clients[agent]
17
32
  end
18
33
  end
19
-
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.0.2
4
+ version: 0.1.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: 2021-07-03 00:00:00.000000000 Z
11
+ date: 2022-12-08 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
@@ -20,24 +20,27 @@ files:
20
20
  - README.md
21
21
  - bin/expedite
22
22
  - lib/expedite.rb
23
- - lib/expedite/application.rb
24
- - lib/expedite/application/boot.rb
25
- - lib/expedite/application_manager.rb
23
+ - lib/expedite/action/block.rb
24
+ - lib/expedite/action/boot.rb
25
+ - lib/expedite/actions.rb
26
+ - lib/expedite/agents.rb
26
27
  - lib/expedite/cli.rb
27
28
  - lib/expedite/cli/server.rb
29
+ - lib/expedite/cli/status.rb
28
30
  - lib/expedite/cli/stop.rb
29
- - lib/expedite/client.rb
30
- - lib/expedite/command/basic.rb
31
- - lib/expedite/command/boot.rb
32
- - lib/expedite/command/info.rb
33
- - lib/expedite/commands.rb
31
+ - lib/expedite/client/exec.rb
32
+ - lib/expedite/client/invoke.rb
34
33
  - lib/expedite/env.rb
35
34
  - lib/expedite/errors.rb
36
35
  - lib/expedite/failsafe_thread.rb
37
- - lib/expedite/send_json.rb
38
- - lib/expedite/server.rb
36
+ - lib/expedite/helper/rails.rb
37
+ - lib/expedite/protocol.rb
38
+ - lib/expedite/server/agent.rb
39
+ - lib/expedite/server/agent_boot.rb
40
+ - lib/expedite/server/agent_manager.rb
41
+ - lib/expedite/server/controller.rb
39
42
  - lib/expedite/signals.rb
40
- - lib/expedite/variants.rb
43
+ - lib/expedite/syntax.rb
41
44
  - lib/expedite/version.rb
42
45
  homepage: https://rubygems.org/gems/expedite
43
46
  licenses:
@@ -58,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
58
61
  - !ruby/object:Gem::Version
59
62
  version: '0'
60
63
  requirements: []
61
- rubygems_version: 3.1.2
64
+ rubygems_version: 3.1.6
62
65
  signing_key:
63
66
  specification_version: 4
64
67
  summary: Expedite startup of Ruby process
@@ -1,316 +0,0 @@
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/commands'
7
- require 'expedite/env'
8
- require 'expedite/failsafe_thread'
9
- require 'expedite/signals'
10
- require 'expedite/variants'
11
-
12
- module Expedite
13
- def self.variant
14
- app.variant
15
- end
16
-
17
- def self.app=(app)
18
- @app = app
19
- end
20
- def self.app
21
- @app
22
- end
23
-
24
- class Application
25
- include Signals
26
-
27
- attr_reader :variant
28
- attr_reader :manager, :env
29
-
30
- def initialize(variant:, manager:, env:)
31
- @variant = variant
32
- @manager = manager
33
- @env = env
34
- @mutex = Mutex.new
35
- @waiting = Set.new
36
- @preloaded = false
37
- @state = :initialized
38
- @interrupt = IO.pipe
39
- end
40
-
41
- def boot
42
- # This is necessary for the terminal to work correctly when we reopen stdin.
43
- Process.setsid rescue Errno::EPERM
44
-
45
- Expedite.app = self
46
-
47
- Signal.trap("TERM") { terminate }
48
-
49
- env.load_helper
50
- eager_preload if false #if ENV.delete("SPRING_PRELOAD") == "1"
51
- run
52
- end
53
-
54
- def state(val)
55
- return if exiting?
56
- log "#{@state} -> #{val}"
57
- @state = val
58
- end
59
-
60
- def state!(val)
61
- state val
62
- @interrupt.last.write "."
63
- end
64
-
65
- def app_name
66
- env.app_name
67
- end
68
-
69
- def log(message)
70
- env.log "[application:#{variant}] #{message}"
71
- end
72
-
73
- def preloaded?
74
- @preloaded
75
- end
76
-
77
- def preload_failed?
78
- @preloaded == :failure
79
- end
80
-
81
- def exiting?
82
- @state == :exiting
83
- end
84
-
85
- def terminating?
86
- @state == :terminating
87
- end
88
-
89
- def initialized?
90
- @state == :initialized
91
- end
92
-
93
- def preload
94
- log "preloading app"
95
-
96
- @preloaded = :success
97
- rescue Exception => e
98
- @preloaded = :failure
99
- raise e unless initialized?
100
- end
101
-
102
- def eager_preload
103
- with_pty { preload }
104
- end
105
-
106
- def run
107
- $0 = "expedite variant | #{app_name} | #{variant}"
108
-
109
- Expedite::Variants.lookup(variant).after_fork(variant)
110
-
111
- state :running
112
- manager.puts
113
-
114
- loop do
115
- IO.select [manager, @interrupt.first]
116
-
117
- if terminating? || preload_failed?
118
- exit
119
- else
120
- serve manager.recv_io(UNIXSocket)
121
- end
122
- end
123
- end
124
-
125
- def serve(client)
126
- log "got client"
127
- manager.puts
128
-
129
- _stdout, stderr, _stdin = streams = 3.times.map { client.recv_io }
130
- [STDOUT, STDERR, STDIN].zip(streams).each { |a, b| a.reopen(b) }
131
-
132
- preload unless preloaded?
133
-
134
- args, env = JSON.load(client.read(client.gets.to_i)).values_at("args", "env")
135
-
136
- exec_name = args.shift
137
- command = Expedite::Commands.lookup(exec_name)
138
- command.setup(client)
139
-
140
- connect_database
141
-
142
- pid = fork {
143
- Process.setsid
144
- IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
145
- trap("TERM", "DEFAULT")
146
-
147
- ARGV.replace(args)
148
- $0 = exec_name
149
-
150
- # Load in the current env vars, except those which *were* changed when Spring started
151
- env.each { |k, v| ENV[k] = v }
152
-
153
- # requiring is faster, so if config.cache_classes was true in
154
- # the environment's config file, then we can respect that from
155
- # here on as we no longer need constant reloading.
156
- if @original_cache_classes
157
- ActiveSupport::Dependencies.mechanism = :require
158
- Rails.application.config.cache_classes = true
159
- end
160
-
161
- connect_database
162
- srand
163
-
164
- invoke_after_fork_callbacks
165
- shush_backtraces
166
-
167
- command.call
168
- }
169
-
170
- disconnect_database
171
-
172
- log "forked #{pid}"
173
- manager.puts pid
174
-
175
- # Boot makes a new application, so we don't wait for it
176
- if command.is_a?(Expedite::Command::Boot)
177
- Process.detach(pid)
178
- else
179
- wait pid, streams, client
180
- end
181
- rescue Exception => e
182
- log "exception: #{e} at #{e.backtrace.join("\n")}"
183
- manager.puts unless pid
184
-
185
- if streams && !e.is_a?(SystemExit)
186
- print_exception(stderr, e)
187
- streams.each(&:close)
188
- end
189
-
190
- client.puts(1) if pid
191
- client.close
192
- ensure
193
- # Redirect STDOUT and STDERR to prevent from keeping the original FDs
194
- # (i.e. to prevent `spring rake -T | grep db` from hanging forever),
195
- # even when exception is raised before forking (i.e. preloading).
196
- reset_streams
197
- end
198
-
199
- def terminate
200
- if exiting?
201
- # Ensure that we do not ignore subsequent termination attempts
202
- log "forced exit"
203
- @waiting.each { |pid| Process.kill("TERM", pid) }
204
- Kernel.exit
205
- else
206
- state! :terminating
207
- end
208
- end
209
-
210
- def exit
211
- state :exiting
212
- manager.shutdown(:RDWR)
213
- exit_if_finished
214
- sleep
215
- end
216
-
217
- def exit_if_finished
218
- @mutex.synchronize {
219
- Kernel.exit if exiting? && @waiting.empty?
220
- }
221
- end
222
-
223
- def invoke_after_fork_callbacks
224
- # TODO:
225
- end
226
-
227
- def disconnect_database
228
- ActiveRecord::Base.remove_connection if active_record_configured?
229
- end
230
-
231
- def connect_database
232
- ActiveRecord::Base.establish_connection if active_record_configured?
233
- end
234
-
235
- # This feels very naughty
236
- def shush_backtraces
237
- Kernel.module_eval do
238
- old_raise = Kernel.method(:raise)
239
- remove_method :raise
240
- define_method :raise do |*args|
241
- begin
242
- old_raise.call(*args)
243
- ensure
244
- if $!
245
- lib = File.expand_path("..", __FILE__)
246
- $!.backtrace.reject! { |line| line.start_with?(lib) }
247
- end
248
- end
249
- end
250
- private :raise
251
- end
252
- end
253
-
254
- def print_exception(stream, error)
255
- first, rest = error.backtrace.first, error.backtrace.drop(1)
256
- stream.puts("#{first}: #{error} (#{error.class})")
257
- rest.each { |line| stream.puts("\tfrom #{line}") }
258
- end
259
-
260
- def with_pty
261
- PTY.open do |master, slave|
262
- [STDOUT, STDERR, STDIN].each { |s| s.reopen slave }
263
- reader_thread = Expedite.failsafe_thread { master.read }
264
- begin
265
- yield
266
- ensure
267
- reader_thread.kill
268
- reset_streams
269
- end
270
- end
271
- end
272
-
273
- def reset_streams
274
- [STDOUT, STDERR].each do |stream|
275
- stream.reopen(env.log_file)
276
- end
277
- STDIN.reopen("/dev/null")
278
- end
279
-
280
- def wait(pid, streams, client)
281
- @mutex.synchronize { @waiting << pid }
282
-
283
- # Wait in a separate thread so we can run multiple commands at once
284
- Expedite.failsafe_thread {
285
- begin
286
- _, status = Process.wait2 pid
287
- log "#{pid} exited with #{status.exitstatus}"
288
-
289
- streams.each(&:close)
290
- client.puts(status.exitstatus)
291
- client.close
292
- ensure
293
- @mutex.synchronize { @waiting.delete pid }
294
- exit_if_finished
295
- end
296
- }
297
-
298
- Expedite.failsafe_thread {
299
- while signal = client.gets.chomp
300
- begin
301
- Process.kill(signal, -Process.getpgid(pid))
302
- client.puts(0)
303
- rescue Errno::ESRCH
304
- client.puts(1)
305
- end
306
- end
307
- }
308
- end
309
-
310
- private
311
-
312
- def active_record_configured?
313
- defined?(ActiveRecord::Base) && ActiveRecord::Base.configurations.any?
314
- end
315
- end
316
- end