Class: BgService::Server
- Inherits:
-
Object
- Object
- BgService::Server
- Defined in:
- lib/bg_service/server.rb
Overview
A network server that listens for incoming connections to be run as a background process.
Constant Summary collapse
- DEFAULT_BOOT_TIMEOUT =
Wait this long for the server to start listening before giving up
10- DEFAULT_TERM_TIMEOUT =
After sending a TERM signal, wait this long for the process to exit before sending a KILL signal
1- SLEEP_INTERVAL =
5ms (while waiting for the server to start or stop)
0.005
Instance Attribute Summary collapse
-
#cmd ⇒ Object
readonly
Returns the value of attribute cmd.
-
#exit_status ⇒ Object
readonly
Returns the value of attribute exit_status.
Instance Method Summary collapse
- #block_until_ready ⇒ Object
- #cleanup ⇒ Object
-
#initialize(cmd, port:, env: {}, boot_timeout: DEFAULT_BOOT_TIMEOUT, term_timeout: DEFAULT_TERM_TIMEOUT) ⇒ Server
constructor
A new instance of Server.
- #listening? ⇒ Boolean
- #logs ⇒ Object
- #now ⇒ Object
- #prefix_log(path, msg) ⇒ Object
- #running? ⇒ Boolean
- #start ⇒ Object
- #status ⇒ Object
- #stop ⇒ Object
-
#wait_for_exit ⇒ Object
Give the process @kill_timeout seconds to exit after sending a TERM signal and then forcibly terminate it with SIGKILL.
- #wait_status ⇒ Object
Constructor Details
#initialize(cmd, port:, env: {}, boot_timeout: DEFAULT_BOOT_TIMEOUT, term_timeout: DEFAULT_TERM_TIMEOUT) ⇒ Server
Returns a new instance of Server.
18 19 20 21 22 23 24 25 26 27 |
# File 'lib/bg_service/server.rb', line 18 def initialize(cmd, port:, env: {}, boot_timeout: DEFAULT_BOOT_TIMEOUT, term_timeout: DEFAULT_TERM_TIMEOUT) @cmd = cmd @env = env @port = port @tmpdir = Dir.mktmpdir @out = File.join(@tmpdir, 'out') @err = File.join(@tmpdir, 'err') @boot_timeout = boot_timeout @term_timeout = term_timeout end |
Instance Attribute Details
#cmd ⇒ Object (readonly)
Returns the value of attribute cmd.
16 17 18 |
# File 'lib/bg_service/server.rb', line 16 def cmd @cmd end |
#exit_status ⇒ Object (readonly)
Returns the value of attribute exit_status.
16 17 18 |
# File 'lib/bg_service/server.rb', line 16 def exit_status @exit_status end |
Instance Method Details
#block_until_ready ⇒ Object
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/bg_service/server.rb', line 47 def block_until_ready while (now - @started_at) < @boot_timeout s=status if s == :starting sleep SLEEP_INTERVAL elsif s == :listening return elsif s == :not_running raise CrashedOnStartup.new("process exited before listening on port #{@port}", self) else raise UnexpectedStatus.new("invariant violated: status was #{s.inspect}", self) end end stop raise TimedOut.new("not listening after #{@boot_timeout} seconds", self) end |
#cleanup ⇒ Object
102 103 104 105 |
# File 'lib/bg_service/server.rb', line 102 def cleanup @final_out = logs FileUtils.remove_entry @tmpdir end |
#listening? ⇒ Boolean
107 108 109 110 111 112 |
# File 'lib/bg_service/server.rb', line 107 def listening? TCPSocket.open('127.0.0.1', @port).close true rescue Errno::ECONNREFUSED false end |
#logs ⇒ Object
64 65 66 67 68 69 |
# File 'lib/bg_service/server.rb', line 64 def logs return @final_out if @final_out out = prefix_log(@out, 'out') err = prefix_log(@err, 'err') "#{out}#{err.chomp}" end |
#now ⇒ Object
43 44 45 |
# File 'lib/bg_service/server.rb', line 43 def now Process.clock_gettime(Process::CLOCK_MONOTONIC) end |
#prefix_log(path, msg) ⇒ Object
71 72 73 74 75 76 77 78 79 |
# File 'lib/bg_service/server.rb', line 71 def prefix_log(path, msg) lines = File.readlines(path) padding = lines.size.to_s.size lines.map.with_index { |line, i| " #{msg} #{i.to_s.rjust(padding, "0")}: #{line}" }.join.tap do |str| return "#{msg}: <empty>\n" if str.empty? end rescue Errno::ENOENT return "#{msg}: <missing>\n" end |
#running? ⇒ Boolean
114 115 116 117 118 |
# File 'lib/bg_service/server.rb', line 114 def running? return false unless @pid wait_status.nil? end |
#start ⇒ Object
29 30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/bg_service/server.rb', line 29 def start if @start_attempted raise InvalidState.new("cannot start a server that has already been stopped", self) end @start_attempted = now if listening? raise AlreadyRunning, "port #{@port} is already in use, refusing to start #{@cmd.inspect}" end @pid = spawn(@env, *@cmd, out: @out, err: @err) @started_at = now block_until_ready end |
#status ⇒ Object
120 121 122 123 |
# File 'lib/bg_service/server.rb', line 120 def status return :not_running if !running? listening? ? :listening : :starting end |
#stop ⇒ Object
81 82 83 84 85 86 87 |
# File 'lib/bg_service/server.rb', line 81 def stop if @pid wait_for_exit cleanup freeze # prevent further use end end |
#wait_for_exit ⇒ Object
Give the process @kill_timeout seconds to exit after sending a TERM signal and then forcibly terminate it with SIGKILL.
91 92 93 94 95 96 97 98 99 100 |
# File 'lib/bg_service/server.rb', line 91 def wait_for_exit Process.kill 'TERM', @pid termed_at = now loop do break if wait_status if (now - termed_at) > @term_timeout Process.kill 'KILL', @pid end end end |
#wait_status ⇒ Object
125 126 127 128 129 130 131 132 133 |
# File 'lib/bg_service/server.rb', line 125 def wait_status @wait_status ||= begin _, status = Process.wait2(@pid, Process::WNOHANG) if status @pid = nil @exit_status = status end end end |