puma 5.0.0-java → 5.1.0-java
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.
Potentially problematic release.
This version of puma might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/History.md +1190 -574
- data/README.md +28 -20
- data/bin/puma-wild +3 -9
- data/docs/compile_options.md +19 -0
- data/docs/deployment.md +5 -6
- data/docs/fork_worker.md +2 -0
- data/docs/jungle/README.md +0 -4
- data/docs/jungle/rc.d/puma +2 -2
- data/docs/nginx.md +1 -1
- data/docs/restart.md +46 -23
- data/docs/systemd.md +25 -3
- data/ext/puma_http11/ext_help.h +1 -1
- data/ext/puma_http11/extconf.rb +4 -5
- data/ext/puma_http11/http11_parser.c +64 -64
- data/ext/puma_http11/mini_ssl.c +39 -37
- data/ext/puma_http11/puma_http11.c +25 -12
- data/lib/puma.rb +7 -4
- data/lib/puma/app/status.rb +44 -46
- data/lib/puma/binder.rb +48 -1
- data/lib/puma/cli.rb +4 -0
- data/lib/puma/client.rb +31 -80
- data/lib/puma/cluster.rb +39 -202
- data/lib/puma/cluster/worker.rb +176 -0
- data/lib/puma/cluster/worker_handle.rb +86 -0
- data/lib/puma/configuration.rb +20 -8
- data/lib/puma/const.rb +11 -3
- data/lib/puma/control_cli.rb +71 -70
- data/lib/puma/dsl.rb +67 -19
- data/lib/puma/error_logger.rb +2 -2
- data/lib/puma/events.rb +21 -3
- data/lib/puma/json.rb +96 -0
- data/lib/puma/launcher.rb +61 -12
- data/lib/puma/minissl.rb +8 -0
- data/lib/puma/puma_http11.jar +0 -0
- data/lib/puma/queue_close.rb +26 -0
- data/lib/puma/reactor.rb +79 -373
- data/lib/puma/request.rb +451 -0
- data/lib/puma/runner.rb +15 -21
- data/lib/puma/server.rb +193 -508
- data/lib/puma/single.rb +3 -2
- data/lib/puma/state_file.rb +5 -3
- data/lib/puma/systemd.rb +46 -0
- data/lib/puma/thread_pool.rb +22 -2
- data/lib/puma/util.rb +12 -0
- metadata +9 -6
- data/docs/jungle/upstart/README.md +0 -61
- data/docs/jungle/upstart/puma-manager.conf +0 -31
- data/docs/jungle/upstart/puma.conf +0 -69
- data/lib/puma/accept_nonblock.rb +0 -29
data/lib/puma/cli.rb
CHANGED
@@ -104,6 +104,10 @@ module Puma
|
|
104
104
|
user_config.bind arg
|
105
105
|
end
|
106
106
|
|
107
|
+
o.on "--bind-to-activated-sockets [only]", "Bind to all activated sockets" do |arg|
|
108
|
+
user_config.bind_to_activated_sockets(arg || true)
|
109
|
+
end
|
110
|
+
|
107
111
|
o.on "-C", "--config PATH", "Load PATH as a config file" do |arg|
|
108
112
|
file_config.load arg
|
109
113
|
end
|
data/lib/puma/client.rb
CHANGED
@@ -85,6 +85,13 @@ module Puma
|
|
85
85
|
|
86
86
|
def_delegators :@io, :closed?
|
87
87
|
|
88
|
+
# Test to see if io meets a bare minimum of functioning, @to_io needs to be
|
89
|
+
# used for MiniSSL::Socket
|
90
|
+
def io_ok?
|
91
|
+
@to_io.is_a?(::BasicSocket) && !closed?
|
92
|
+
end
|
93
|
+
|
94
|
+
# @!attribute [r] inspect
|
88
95
|
def inspect
|
89
96
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
90
97
|
end
|
@@ -96,12 +103,18 @@ module Puma
|
|
96
103
|
env[HIJACK_IO] ||= @io
|
97
104
|
end
|
98
105
|
|
106
|
+
# @!attribute [r] in_data_phase
|
99
107
|
def in_data_phase
|
100
108
|
!@read_header
|
101
109
|
end
|
102
110
|
|
103
111
|
def set_timeout(val)
|
104
|
-
@timeout_at =
|
112
|
+
@timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
|
113
|
+
end
|
114
|
+
|
115
|
+
# Number of seconds until the timeout elapses.
|
116
|
+
def timeout
|
117
|
+
[@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
105
118
|
end
|
106
119
|
|
107
120
|
def reset(fast_check=true)
|
@@ -155,7 +168,9 @@ module Puma
|
|
155
168
|
data = @io.read_nonblock(CHUNK_SIZE)
|
156
169
|
rescue IO::WaitReadable
|
157
170
|
return false
|
158
|
-
rescue
|
171
|
+
rescue EOFError
|
172
|
+
# Swallow error, don't log
|
173
|
+
rescue SystemCallError, IOError
|
159
174
|
raise ConnectionError, "Connection error detected during read"
|
160
175
|
end
|
161
176
|
|
@@ -184,79 +199,20 @@ module Puma
|
|
184
199
|
false
|
185
200
|
end
|
186
201
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
data = @io.sysread_nonblock(CHUNK_SIZE)
|
193
|
-
rescue OpenSSL::SSL::SSLError => e
|
194
|
-
return false if e.kind_of? IO::WaitReadable
|
195
|
-
raise e
|
196
|
-
end
|
197
|
-
|
198
|
-
# No data means a closed socket
|
199
|
-
unless data
|
200
|
-
@buffer = nil
|
201
|
-
set_ready
|
202
|
-
raise EOFError
|
203
|
-
end
|
204
|
-
|
205
|
-
if @buffer
|
206
|
-
@buffer << data
|
207
|
-
else
|
208
|
-
@buffer = data
|
209
|
-
end
|
210
|
-
|
211
|
-
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
212
|
-
|
213
|
-
if @parser.finished?
|
214
|
-
return setup_body
|
215
|
-
elsif @parsed_bytes >= MAX_HEADER
|
216
|
-
raise HttpParserError,
|
217
|
-
"HEADER is longer than allowed, aborting client early."
|
218
|
-
end
|
219
|
-
|
220
|
-
false
|
221
|
-
end
|
222
|
-
|
223
|
-
def eagerly_finish
|
224
|
-
return true if @ready
|
225
|
-
|
226
|
-
if @io.kind_of? OpenSSL::SSL::SSLSocket
|
227
|
-
return true if jruby_start_try_to_finish
|
228
|
-
end
|
229
|
-
|
230
|
-
return false unless IO.select([@to_io], nil, nil, 0)
|
231
|
-
try_to_finish
|
232
|
-
end
|
233
|
-
|
234
|
-
else
|
235
|
-
|
236
|
-
def eagerly_finish
|
237
|
-
return true if @ready
|
238
|
-
return false unless IO.select([@to_io], nil, nil, 0)
|
239
|
-
try_to_finish
|
240
|
-
end
|
241
|
-
|
242
|
-
# For documentation, see https://github.com/puma/puma/issues/1754
|
243
|
-
send(:alias_method, :jruby_eagerly_finish, :eagerly_finish)
|
244
|
-
end # IS_JRUBY
|
202
|
+
def eagerly_finish
|
203
|
+
return true if @ready
|
204
|
+
return false unless IO.select([@to_io], nil, nil, 0)
|
205
|
+
try_to_finish
|
206
|
+
end
|
245
207
|
|
246
208
|
def finish(timeout)
|
247
|
-
return
|
248
|
-
until try_to_finish
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
unless can_read
|
255
|
-
write_error(408) if in_data_phase
|
256
|
-
raise ConnectionError
|
257
|
-
end
|
258
|
-
end
|
259
|
-
true
|
209
|
+
return if @ready
|
210
|
+
IO.select([@to_io], nil, nil, timeout) || timeout! until try_to_finish
|
211
|
+
end
|
212
|
+
|
213
|
+
def timeout!
|
214
|
+
write_error(408) if in_data_phase
|
215
|
+
raise ConnectionError
|
260
216
|
end
|
261
217
|
|
262
218
|
def write_error(status_code)
|
@@ -283,13 +239,8 @@ module Puma
|
|
283
239
|
# @version 5.0.0
|
284
240
|
#
|
285
241
|
def can_close?
|
286
|
-
# Allow connection to close if
|
287
|
-
|
288
|
-
#
|
289
|
-
# From RFC 2616 section 8.1.4:
|
290
|
-
# Servers SHOULD always respond to at least one request per connection,
|
291
|
-
# if at all possible.
|
292
|
-
@requests_served > 0 && @parsed_bytes == 0
|
242
|
+
# Allow connection to close if we're not in the middle of parsing a request.
|
243
|
+
@parsed_bytes == 0
|
293
244
|
end
|
294
245
|
|
295
246
|
private
|
data/lib/puma/cluster.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
require 'puma/runner'
|
4
4
|
require 'puma/util'
|
5
5
|
require 'puma/plugin'
|
6
|
+
require 'puma/cluster/worker_handle'
|
7
|
+
require 'puma/cluster/worker'
|
6
8
|
|
7
9
|
require 'time'
|
8
10
|
|
@@ -11,10 +13,6 @@ module Puma
|
|
11
13
|
# to boot and serve a Ruby application when puma "workers" are needed
|
12
14
|
# i.e. when using multi-processes. For example `$ puma -w 5`
|
13
15
|
#
|
14
|
-
# At the core of this class is running an instance of `Puma::Server` which
|
15
|
-
# gets created via the `start_server` method from the `Puma::Runner` class
|
16
|
-
# that this inherits from.
|
17
|
-
#
|
18
16
|
# An instance of this class will spawn the number of processes passed in
|
19
17
|
# via the `spawn_workers` method call. Each worker will have it's own
|
20
18
|
# instance of a `Puma::Server`.
|
@@ -61,79 +59,6 @@ module Puma
|
|
61
59
|
@workers.each { |x| x.hup }
|
62
60
|
end
|
63
61
|
|
64
|
-
class Worker
|
65
|
-
def initialize(idx, pid, phase, options)
|
66
|
-
@index = idx
|
67
|
-
@pid = pid
|
68
|
-
@phase = phase
|
69
|
-
@stage = :started
|
70
|
-
@signal = "TERM"
|
71
|
-
@options = options
|
72
|
-
@first_term_sent = nil
|
73
|
-
@started_at = Time.now
|
74
|
-
@last_checkin = Time.now
|
75
|
-
@last_status = {}
|
76
|
-
@term = false
|
77
|
-
end
|
78
|
-
|
79
|
-
attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
|
80
|
-
|
81
|
-
# @version 5.0.0
|
82
|
-
attr_writer :pid, :phase
|
83
|
-
|
84
|
-
def booted?
|
85
|
-
@stage == :booted
|
86
|
-
end
|
87
|
-
|
88
|
-
def boot!
|
89
|
-
@last_checkin = Time.now
|
90
|
-
@stage = :booted
|
91
|
-
end
|
92
|
-
|
93
|
-
def term?
|
94
|
-
@term
|
95
|
-
end
|
96
|
-
|
97
|
-
def ping!(status)
|
98
|
-
@last_checkin = Time.now
|
99
|
-
require 'json'
|
100
|
-
@last_status = JSON.parse(status, symbolize_names: true)
|
101
|
-
end
|
102
|
-
|
103
|
-
# @see Puma::Cluster#check_workers
|
104
|
-
# @version 5.0.0
|
105
|
-
def ping_timeout
|
106
|
-
@last_checkin +
|
107
|
-
(booted? ?
|
108
|
-
@options[:worker_timeout] :
|
109
|
-
@options[:worker_boot_timeout]
|
110
|
-
)
|
111
|
-
end
|
112
|
-
|
113
|
-
def term
|
114
|
-
begin
|
115
|
-
if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
|
116
|
-
@signal = "KILL"
|
117
|
-
else
|
118
|
-
@term ||= true
|
119
|
-
@first_term_sent ||= Time.now
|
120
|
-
end
|
121
|
-
Process.kill @signal, @pid if @pid
|
122
|
-
rescue Errno::ESRCH
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
def kill
|
127
|
-
@signal = 'KILL'
|
128
|
-
term
|
129
|
-
end
|
130
|
-
|
131
|
-
def hup
|
132
|
-
Process.kill "HUP", @pid
|
133
|
-
rescue Errno::ESRCH
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
62
|
def spawn_workers
|
138
63
|
diff = @options[:workers] - @workers.size
|
139
64
|
return if diff < 1
|
@@ -154,7 +79,7 @@ module Puma
|
|
154
79
|
end
|
155
80
|
|
156
81
|
debug "Spawned worker: #{pid}"
|
157
|
-
@workers <<
|
82
|
+
@workers << WorkerHandle.new(idx, pid, @phase, @options)
|
158
83
|
end
|
159
84
|
|
160
85
|
if @options[:fork_worker] &&
|
@@ -189,11 +114,12 @@ module Puma
|
|
189
114
|
debug "Workers to cull: #{workers_to_cull.inspect}"
|
190
115
|
|
191
116
|
workers_to_cull.each do |worker|
|
192
|
-
log "- Worker #{worker.index} (
|
117
|
+
log "- Worker #{worker.index} (PID: #{worker.pid}) terminating"
|
193
118
|
worker.term
|
194
119
|
end
|
195
120
|
end
|
196
121
|
|
122
|
+
# @!attribute [r] next_worker_index
|
197
123
|
def next_worker_index
|
198
124
|
all_positions = 0...@options[:workers]
|
199
125
|
occupied_positions = @workers.map { |w| w.index }
|
@@ -248,113 +174,25 @@ module Puma
|
|
248
174
|
end
|
249
175
|
|
250
176
|
def worker(index, master)
|
251
|
-
title = "puma: cluster worker #{index}: #{master}"
|
252
|
-
title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
|
253
|
-
$0 = title
|
254
|
-
|
255
|
-
Signal.trap "SIGINT", "IGNORE"
|
256
|
-
Signal.trap "SIGCHLD", "DEFAULT"
|
257
|
-
|
258
|
-
fork_worker = @options[:fork_worker] && index == 0
|
259
|
-
|
260
177
|
@workers = []
|
261
|
-
if !@options[:fork_worker] || fork_worker
|
262
|
-
@master_read.close
|
263
|
-
@suicide_pipe.close
|
264
|
-
@fork_writer.close
|
265
|
-
end
|
266
|
-
|
267
|
-
Thread.new do
|
268
|
-
Puma.set_thread_name "worker check pipe"
|
269
|
-
IO.select [@check_pipe]
|
270
|
-
log "! Detected parent died, dying"
|
271
|
-
exit! 1
|
272
|
-
end
|
273
178
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
if File.exist?("Gemfile")
|
278
|
-
log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
|
279
|
-
elsif File.exist?("gems.rb")
|
280
|
-
log "+ Gemfile in context: #{File.expand_path("gems.rb")}"
|
281
|
-
end
|
282
|
-
end
|
283
|
-
|
284
|
-
# Invoke any worker boot hooks so they can get
|
285
|
-
# things in shape before booting the app.
|
286
|
-
@launcher.config.run_hooks :before_worker_boot, index, @launcher.events
|
287
|
-
|
288
|
-
server = @server ||= start_server
|
289
|
-
restart_server = Queue.new << true << false
|
290
|
-
|
291
|
-
if fork_worker
|
292
|
-
restart_server.clear
|
293
|
-
worker_pids = []
|
294
|
-
Signal.trap "SIGCHLD" do
|
295
|
-
wakeup! if worker_pids.reject! do |p|
|
296
|
-
Process.wait(p, Process::WNOHANG) rescue true
|
297
|
-
end
|
298
|
-
end
|
299
|
-
|
300
|
-
Thread.new do
|
301
|
-
Puma.set_thread_name "worker fork pipe"
|
302
|
-
while (idx = @fork_pipe.gets)
|
303
|
-
idx = idx.to_i
|
304
|
-
if idx == -1 # stop server
|
305
|
-
if restart_server.length > 0
|
306
|
-
restart_server.clear
|
307
|
-
server.begin_restart(true)
|
308
|
-
@launcher.config.run_hooks :before_refork, nil, @launcher.events
|
309
|
-
nakayoshi_gc
|
310
|
-
end
|
311
|
-
elsif idx == 0 # restart server
|
312
|
-
restart_server << true << false
|
313
|
-
else # fork worker
|
314
|
-
worker_pids << pid = spawn_worker(idx, master)
|
315
|
-
@worker_write << "f#{pid}:#{idx}\n" rescue nil
|
316
|
-
end
|
317
|
-
end
|
318
|
-
end
|
319
|
-
end
|
320
|
-
|
321
|
-
Signal.trap "SIGTERM" do
|
322
|
-
@worker_write << "e#{Process.pid}\n" rescue nil
|
323
|
-
server.stop
|
324
|
-
restart_server << false
|
325
|
-
end
|
326
|
-
|
327
|
-
begin
|
328
|
-
@worker_write << "b#{Process.pid}:#{index}\n"
|
329
|
-
rescue SystemCallError, IOError
|
330
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
331
|
-
STDERR.puts "Master seems to have exited, exiting."
|
332
|
-
return
|
333
|
-
end
|
334
|
-
|
335
|
-
Thread.new(@worker_write) do |io|
|
336
|
-
Puma.set_thread_name "stat payload"
|
179
|
+
@master_read.close
|
180
|
+
@suicide_pipe.close
|
181
|
+
@fork_writer.close
|
337
182
|
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
io << "p#{Process.pid}#{server.stats.to_json}\n"
|
343
|
-
rescue IOError
|
344
|
-
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
345
|
-
break
|
346
|
-
end
|
347
|
-
end
|
183
|
+
pipes = { check_pipe: @check_pipe, worker_write: @worker_write }
|
184
|
+
if @options[:fork_worker]
|
185
|
+
pipes[:fork_pipe] = @fork_pipe
|
186
|
+
pipes[:wakeup] = @wakeup
|
348
187
|
end
|
349
188
|
|
350
|
-
server
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
@worker_write.close
|
189
|
+
server = start_server if preload?
|
190
|
+
new_worker = Worker.new index: index,
|
191
|
+
master: master,
|
192
|
+
launcher: @launcher,
|
193
|
+
pipes: pipes,
|
194
|
+
server: server
|
195
|
+
new_worker.run
|
358
196
|
end
|
359
197
|
|
360
198
|
def restart
|
@@ -396,6 +234,7 @@ module Puma
|
|
396
234
|
|
397
235
|
# Inside of a child process, this will return all zeroes, as @workers is only populated in
|
398
236
|
# the master process.
|
237
|
+
# @!attribute [r] stats
|
399
238
|
def stats
|
400
239
|
old_worker_count = @workers.count { |w| w.phase != @phase }
|
401
240
|
worker_status = @workers.map do |w|
|
@@ -490,15 +329,19 @@ module Puma
|
|
490
329
|
|
491
330
|
output_header "cluster"
|
492
331
|
|
493
|
-
|
332
|
+
# This is aligned with the output from Runner, see Runner#output_header
|
333
|
+
log "* Workers: #{@options[:workers]}"
|
494
334
|
|
495
|
-
|
335
|
+
# Threads explicitly marked as fork safe will be ignored.
|
336
|
+
# Used in Rails, but may be used by anyone.
|
337
|
+
before = Thread.list.reject { |t| t.thread_variable_get(:fork_safe) }
|
496
338
|
|
497
339
|
if preload?
|
340
|
+
log "* Restarts: (\u2714) hot (\u2716) phased"
|
498
341
|
log "* Preloading application"
|
499
342
|
load_and_bind
|
500
343
|
|
501
|
-
after = Thread.list
|
344
|
+
after = Thread.list.reject { |t| t.thread_variable_get(:fork_safe) }
|
502
345
|
|
503
346
|
if after.size > before.size
|
504
347
|
threads = (after - before)
|
@@ -512,7 +355,7 @@ module Puma
|
|
512
355
|
end
|
513
356
|
end
|
514
357
|
else
|
515
|
-
log "*
|
358
|
+
log "* Restarts: (\u2714) hot (\u2714) phased"
|
516
359
|
|
517
360
|
unless @launcher.config.app_configured?
|
518
361
|
error "No application configured, nothing to run"
|
@@ -550,7 +393,7 @@ module Puma
|
|
550
393
|
@master_read, @worker_write = read, @wakeup
|
551
394
|
|
552
395
|
@launcher.config.run_hooks :before_fork, nil, @launcher.events
|
553
|
-
nakayoshi_gc
|
396
|
+
Puma::Util.nakayoshi_gc @events if @options[:nakayoshi_fork]
|
554
397
|
|
555
398
|
spawn_workers
|
556
399
|
|
@@ -558,9 +401,9 @@ module Puma
|
|
558
401
|
stop
|
559
402
|
end
|
560
403
|
|
561
|
-
@launcher.events.fire_on_booted!
|
562
|
-
|
563
404
|
begin
|
405
|
+
booted = false
|
406
|
+
|
564
407
|
while @status == :run
|
565
408
|
begin
|
566
409
|
if @phased_restart
|
@@ -591,7 +434,7 @@ module Puma
|
|
591
434
|
case req
|
592
435
|
when "b"
|
593
436
|
w.boot!
|
594
|
-
log "- Worker #{w.index} (
|
437
|
+
log "- Worker #{w.index} (PID: #{pid}) booted, phase: #{w.phase}"
|
595
438
|
@next_check = Time.now
|
596
439
|
when "e"
|
597
440
|
# external term, see worker method, Signal.trap "SIGTERM"
|
@@ -601,6 +444,10 @@ module Puma
|
|
601
444
|
when "p"
|
602
445
|
w.ping!(result.sub(/^\d+/,'').chomp)
|
603
446
|
@launcher.events.fire(:ping!, w)
|
447
|
+
if !booted && @workers.none? {|worker| worker.last_status.empty?}
|
448
|
+
@launcher.events.fire_on_booted!
|
449
|
+
booted = true
|
450
|
+
end
|
604
451
|
end
|
605
452
|
else
|
606
453
|
log "! Out-of-sync worker list, no #{pid} worker"
|
@@ -638,7 +485,9 @@ module Puma
|
|
638
485
|
rescue Errno::ECHILD
|
639
486
|
begin
|
640
487
|
Process.kill(0, w.pid)
|
641
|
-
|
488
|
+
# child still alive but has another parent (e.g., using fork_worker)
|
489
|
+
w.term if w.term?
|
490
|
+
false
|
642
491
|
rescue Errno::ESRCH, Errno::EPERM
|
643
492
|
true # child is already terminated
|
644
493
|
end
|
@@ -655,17 +504,5 @@ module Puma
|
|
655
504
|
end
|
656
505
|
end
|
657
506
|
end
|
658
|
-
|
659
|
-
# @version 5.0.0
|
660
|
-
def nakayoshi_gc
|
661
|
-
return unless @options[:nakayoshi_fork]
|
662
|
-
log "! Promoting existing objects to old generation..."
|
663
|
-
4.times { GC.start(full_mark: false) }
|
664
|
-
if GC.respond_to?(:compact)
|
665
|
-
log "! Compacting..."
|
666
|
-
GC.compact
|
667
|
-
end
|
668
|
-
log "! Friendly fork preparation complete."
|
669
|
-
end
|
670
507
|
end
|
671
508
|
end
|