|
| 1 | +require 'securerandom' |
| 2 | + |
| 3 | +module Spring |
| 4 | + module ApplicationManager |
| 5 | + class PoolStrategy |
| 6 | + class Worker |
| 7 | + attr_reader :pid, :uuid, :socket |
| 8 | + attr_accessor :on_done |
| 9 | + |
| 10 | + def initialize(env, args) |
| 11 | + @spring_env = Env.new |
| 12 | + @uuid = SecureRandom.uuid |
| 13 | + path = @spring_env.tmp_path.join("#{@uuid}.sock").to_s |
| 14 | + @server = UNIXServer.open(path) |
| 15 | + |
| 16 | + Bundler.with_clean_env do |
| 17 | + spawn_app( |
| 18 | + env.merge("SPRING_SOCKET" => path), |
| 19 | + args |
| 20 | + ) |
| 21 | + end |
| 22 | + |
| 23 | + @socket = @server.accept |
| 24 | + end |
| 25 | + |
| 26 | + def spawn_app(env, args) |
| 27 | + @pid = |
| 28 | + Process.spawn( |
| 29 | + env, |
| 30 | + *args |
| 31 | + ) |
| 32 | + |
| 33 | + log "(spawn #{@pid})" |
| 34 | + end |
| 35 | + |
| 36 | + def await_boot |
| 37 | + @pid = socket.gets.to_i |
| 38 | + start_wait_thread(pid, socket) unless pid.zero? |
| 39 | + end |
| 40 | + |
| 41 | + def start_wait_thread(pid, child) |
| 42 | + Process.detach(pid) |
| 43 | + |
| 44 | + Thread.new { |
| 45 | + begin |
| 46 | + Process.kill(0, pid) while sleep(1) |
| 47 | + rescue Errno::ESRCH |
| 48 | + end |
| 49 | + |
| 50 | + log "child #{pid} shutdown" |
| 51 | + |
| 52 | + # socket.close |
| 53 | + # @server.close |
| 54 | + on_done.call(self) if on_done |
| 55 | + } |
| 56 | + end |
| 57 | + |
| 58 | + def log(message) |
| 59 | + @spring_env.log "[worker:#{uuid}] #{message}" |
| 60 | + end |
| 61 | + end |
| 62 | + |
| 63 | + class WorkerPool |
| 64 | + def initialize(app_env, *app_args) |
| 65 | + @app_env = app_env |
| 66 | + @app_args = app_args |
| 67 | + @spring_env = Env.new |
| 68 | + |
| 69 | + @workers = [] |
| 70 | + @workers_in_use = [] |
| 71 | + @spawning_workers = [] |
| 72 | + |
| 73 | + @check_mutex = Mutex.new |
| 74 | + @workers_mutex = Mutex.new |
| 75 | + |
| 76 | + run |
| 77 | + end |
| 78 | + |
| 79 | + def add_worker |
| 80 | + worker = Worker.new(@app_env, @app_args) |
| 81 | + worker.on_done = method(:worker_done) |
| 82 | + @workers_mutex.synchronize { @spawning_workers << worker } |
| 83 | + Thread.new do |
| 84 | + worker.await_boot |
| 85 | + log "+ worker #{worker.pid} (#{worker.uuid})" |
| 86 | + @workers_mutex.synchronize do |
| 87 | + @spawning_workers.delete(worker) |
| 88 | + @workers << worker |
| 89 | + end |
| 90 | + end |
| 91 | + end |
| 92 | + |
| 93 | + def worker_done(worker) |
| 94 | + log "- worker #{worker.pid} (#{worker.uuid})" |
| 95 | + @workers_mutex.synchronize do |
| 96 | + @workers_in_use.delete(worker) |
| 97 | + end |
| 98 | + end |
| 99 | + |
| 100 | + def get_worker(spawn_new = true) |
| 101 | + add_worker if spawn_new && all_size == 0 |
| 102 | + |
| 103 | + worker = nil |
| 104 | + while worker.nil? && all_size > 0 |
| 105 | + @workers_mutex.synchronize do |
| 106 | + worker = @workers.shift |
| 107 | + @workers_in_use << worker if worker |
| 108 | + end |
| 109 | + break if worker |
| 110 | + sleep 1 |
| 111 | + end |
| 112 | + |
| 113 | + Thread.new { check_min_free_workers } if spawn_new |
| 114 | + |
| 115 | + worker |
| 116 | + end |
| 117 | + |
| 118 | + def check_min_free_workers |
| 119 | + if @check_mutex.try_lock |
| 120 | + while all_size < Spring.pool_min_free_workers |
| 121 | + unless Spring.pool_spawn_parallel |
| 122 | + sleep 0.1 until @workers_mutex.synchronize { @spawning_workers.empty? } |
| 123 | + end |
| 124 | + add_worker |
| 125 | + end |
| 126 | + @check_mutex.unlock |
| 127 | + end |
| 128 | + end |
| 129 | + |
| 130 | + def all_size |
| 131 | + @workers_mutex.synchronize { @workers.size + @spawning_workers.size } |
| 132 | + end |
| 133 | + |
| 134 | + def stop! |
| 135 | + if spawning_worker_pids.include?(nil) |
| 136 | + log "Waiting for workers to quit..." |
| 137 | + sleep 0.1 while spawning_worker_pids.include?(nil) |
| 138 | + end |
| 139 | + |
| 140 | + @workers_mutex.synchronize do |
| 141 | + (@spawning_workers + @workers_in_use + @workers).each do |worker| |
| 142 | + kill_worker(worker) |
| 143 | + end |
| 144 | + end |
| 145 | + end |
| 146 | + private |
| 147 | + def kill_worker(worker) |
| 148 | + log "- worker #{worker.pid} (#{worker.uuid})." |
| 149 | + system("kill -9 #{worker.pid} > /dev/null 2>&1") |
| 150 | + rescue |
| 151 | + end |
| 152 | + |
| 153 | + def spawning_worker_pids |
| 154 | + @spawning_workers.map { |worker| worker.pid } |
| 155 | + end |
| 156 | + |
| 157 | + def run |
| 158 | + check_min_free_workers |
| 159 | + end |
| 160 | + |
| 161 | + def log(message) |
| 162 | + @spring_env.log "[worker:pool] #{message}" |
| 163 | + end |
| 164 | + end |
| 165 | + |
| 166 | + def initialize(app_env) |
| 167 | + @app_env = app_env |
| 168 | + @spring_env = Env.new |
| 169 | + @pool = |
| 170 | + WorkerPool.new( |
| 171 | + { |
| 172 | + "RAILS_ENV" => app_env, |
| 173 | + "RACK_ENV" => app_env, |
| 174 | + "SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV), |
| 175 | + "SPRING_PRELOAD" => "1", |
| 176 | + }, |
| 177 | + Spring.ruby_bin, |
| 178 | + "-I", File.expand_path("../..", __FILE__), |
| 179 | + "-e", "require 'spring/application/boot'" |
| 180 | + ) |
| 181 | + end |
| 182 | + |
| 183 | + # Returns the name of the screen running the command, or nil if the application process died. |
| 184 | + def run(client) |
| 185 | + pid = nil |
| 186 | + with_child do |child| |
| 187 | + child.socket.send_io(client) |
| 188 | + IO.select([child.socket]) |
| 189 | + child.socket.gets or raise Errno::EPIPE |
| 190 | + IO.select([child.socket]) |
| 191 | + pid = child.socket.gets.to_i |
| 192 | + end |
| 193 | + |
| 194 | + unless pid.zero? |
| 195 | + log "got worker pid #{pid}" |
| 196 | + pid |
| 197 | + end |
| 198 | + rescue Errno::ECONNRESET, Errno::EPIPE => e |
| 199 | + log "#{e} while reading from child; returning no pid" |
| 200 | + nil |
| 201 | + ensure |
| 202 | + client.close |
| 203 | + end |
| 204 | + |
| 205 | + def stop |
| 206 | + log "stopping" |
| 207 | + |
| 208 | + @pool.stop! |
| 209 | + rescue Errno::ESRCH, Errno::ECHILD |
| 210 | + # Don't care |
| 211 | + end |
| 212 | + |
| 213 | + protected |
| 214 | + |
| 215 | + attr_reader :app_env, :spring_env |
| 216 | + |
| 217 | + def log(message) |
| 218 | + spring_env.log "[application_manager:#{app_env}] #{message}" |
| 219 | + end |
| 220 | + |
| 221 | + def with_child |
| 222 | + yield(@pool.get_worker) |
| 223 | + end |
| 224 | + end |
| 225 | + end |
| 226 | +end |
0 commit comments