pitchfork 0.7.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05372d35dd4784eb22e204b614b1c7add13fd0e298495543c5395e2f03b42073
4
- data.tar.gz: 14f241efeb95774f6de6e9fc8926815eb7f10c25965a09a0700f77dae75dbb92
3
+ metadata.gz: eb4e22969b9f2c38717f0cfa7a3c966995814156a17589bc06bdc609b7ad6e32
4
+ data.tar.gz: dbf7833c26ef94962abbd71d66c63e40dd0f7f405ee82951723ad3b4cbfa864f
5
5
  SHA512:
6
- metadata.gz: f26ebeeb3bc9f7533d25cc41b29a317fb8a80ce108a1859657adfa80b9eb8f88812cfe85dfecf0fbd8093b6e91f45c6ce51ab7b9a15864d96b9d0f5f77835786
7
- data.tar.gz: 8d1f09bb24226f2c89b2db2d549c28508a9f2ea716a1612fcc3367d27445657b9d9da281e623af696dc803acd649847a724366306caf672dbc523a3e7495579b
6
+ metadata.gz: 0f0c029a01bc999d90421fe0ed77f76c6f1e56a9f3ec8eaa4ab6722f40eb0ee7f9aa0f004044e4c97e93a593cdba0d7a6bab01ef6b075440c76012f05e5cccd4
7
+ data.tar.gz: 4ae4d319ffd2acbf99ad29156622510c951ca095c20074f8415376d6e217be1b959e68e3245590cce62de66f3f8af8f0aae469fec842865dd4dcfe1b060e3db9
@@ -0,0 +1,28 @@
1
+ // For format details, see https://aka.ms/devcontainer.json. For config options, see the
2
+ // README at: https://github.com/devcontainers/templates/tree/main/src/ruby
3
+ {
4
+ "name": "Ruby",
5
+ // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
6
+ "build": {
7
+ // Path is relative to the devcontainer.json file.
8
+ "dockerfile": "../Dockerfile"
9
+ },
10
+ "features": {
11
+ "ghcr.io/devcontainers/features/github-cli:1": {}
12
+ },
13
+
14
+ // Features to add to the dev container. More info: https://containers.dev/features.
15
+ // "features": {},
16
+
17
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
18
+ // "forwardPorts": [],
19
+
20
+ // Use 'postCreateCommand' to run commands after the container is created.
21
+ "postCreateCommand": "bundle install"
22
+
23
+ // Configure tool-specific properties.
24
+ // "customizations": {},
25
+
26
+ // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
27
+ // "remoteUser": "root"
28
+ }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Unreleased
2
2
 
3
+ # 0.9.0
4
+
5
+ - Implement `spawn_timeout` to protect against bugs causing workers to get stuck before they reach ready state.
6
+
7
+ # 0.8.0
8
+
9
+ - Add an `after_monitor_ready` callback, called in the monitor process at end of boot.
10
+ - Implement `Pitchfork.prevent_fork` for use in background threads that synchronize native locks with the GVL released.
11
+
3
12
  # 0.7.0
4
13
 
5
14
  - Set nicer `proctile` to better see the state of the process tree at a glance.
data/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM ruby:3.2
1
+ FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bookworm
2
2
  RUN apt-get update -y && apt-get install -y ragel socat netcat-traditional smem apache2-utils
3
3
  WORKDIR /app
4
4
  CMD [ "bash" ]
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pitchfork (0.7.0)
4
+ pitchfork (0.9.0)
5
5
  rack (>= 2.0)
6
6
  raindrops (~> 0.7)
7
7
 
@@ -10,7 +10,7 @@ GEM
10
10
  specs:
11
11
  minitest (5.15.0)
12
12
  nio4r (2.5.9)
13
- puma (6.3.0)
13
+ puma (6.3.1)
14
14
  nio4r (~> 2.0)
15
15
  rack (3.0.8)
16
16
  raindrops (0.20.1)
data/README.md CHANGED
@@ -9,13 +9,6 @@ advantage of features in Unix/Unix-like kernels. Slow clients should
9
9
  only be served by placing a reverse proxy capable of fully buffering
10
10
  both the request and response in between `pitchfork` and slow clients.
11
11
 
12
- ## Disclaimer
13
-
14
- Until this notice is removed from the README, `pitchfork` should be
15
- considered experimental. As such it is not encouraged to run it in
16
- production just yet unless you feel capable of debugging yourself
17
- any issue that may arise.
18
-
19
12
  ## Features
20
13
 
21
14
  * Designed for Rack, Linux, fast clients, and ease-of-debugging. We
@@ -40,9 +33,23 @@ any issue that may arise.
40
33
  or ports yourself. `pitchfork` can spawn and manage any number of
41
34
  worker processes you choose to scale to your backend.
42
35
 
36
+ * Adaptative timeout: request timeout can be extended dynamically on a
37
+ per request basis, which allows to keep a strict overall timeout for
38
+ most endpoints, but allow a few endpoints to take longer.
39
+
43
40
  * Load balancing is done entirely by the operating system kernel.
44
41
  Requests never pile up behind a busy worker process.
45
42
 
43
+ ## When to Use
44
+
45
+ Pitchfork isn't inherently better than other Ruby application servers, it mostly
46
+ focus on different tradeoffs.
47
+
48
+ If you are fine with your current server, it's best to stick with it.
49
+
50
+ If there is a problem you are trying to solve, please read the
51
+ [migration guide](docs/WHY_MIGRATE.md) first.
52
+
46
53
  ## Requirements
47
54
 
48
55
  Ruby(MRI) Version 2.5 and above.
@@ -238,6 +238,19 @@ exit or be SIGKILL-ed due to timeouts.
238
238
  See https://nginx.org/en/docs/http/ngx_http_upstream_module.html
239
239
  for more details on nginx upstream configuration.
240
240
 
241
+ ### `spawn_timeout`
242
+
243
+ ```ruby
244
+ timeout 5
245
+ ```
246
+
247
+ Sets the timeout for a newly spawned worker to be ready after being spawned.
248
+
249
+ This timeout is a safeguard against various low-level fork safety bugs that could cause
250
+ a process to dead-lock.
251
+
252
+ The default of `10` seconds is quite generous and likely doesn't need to be adjusted.
253
+
241
254
  ### `logger`
242
255
 
243
256
  ```ruby
@@ -253,12 +266,23 @@ The default Logger will log its output to STDERR.
253
266
  Because pitchfork several callbacks around the lifecycle of workers.
254
267
  It is often necessary to use these callbacks to close inherited connection after fork.
255
268
 
256
- Note that when reforking is available, the `pitchfork` master process won't load your application
257
- at all. As such for hooks executed in the master, you may need to explicitly load the parts of your
269
+ Note that when reforking is available, the `pitchfork` monitor process won't load your application
270
+ at all. As such for hooks executed in the monitor, you may need to explicitly load the parts of your
258
271
  application that are used in hooks.
259
272
 
260
273
  `pitchfork` also don't attempt to rescue hook errors. Raising from a worker hook will crash the worker,
261
- and raising from a master hook will bring the whole cluster down.
274
+ and raising from a monitor hook will bring the whole cluster down.
275
+
276
+ ### `after_monitor_ready`
277
+
278
+ Called by the monitor process after it's done booting the application and
279
+ spawning the original workers.
280
+
281
+ ```ruby
282
+ after_monitor_ready do |server|
283
+ server.logger.info("Monitor pid=#{Process.pid} ready")
284
+ end
285
+ ```
262
286
 
263
287
  ### `after_mold_fork`
264
288
 
@@ -338,7 +362,7 @@ By default the cleanup timeout is 2 seconds.
338
362
 
339
363
  ### `after_worker_hard_timeout`
340
364
 
341
- Called in the master process when a worker hard timeout is elapsed:
365
+ Called in the monitor process when a worker hard timeout is elapsed:
342
366
 
343
367
  ```ruby
344
368
  after_worker_timeout do |server, worker|
@@ -353,7 +377,7 @@ soft timeout from working.
353
377
 
354
378
  ### `after_worker_exit`
355
379
 
356
- Called in the master process after a worker exits.
380
+ Called in the monitor process after a worker exits.
357
381
 
358
382
  ```ruby
359
383
  after_worker_exit do |server, worker, status|
@@ -0,0 +1,93 @@
1
+ # Why migrate to Pitchfork?
2
+
3
+ First and foremost, if you don't have any specific problem with your current server, then don't.
4
+
5
+ Pitchfork isn't a silver bullet, it's a very opinionated software that focus on very specific tradeoffs,
6
+ that are different from other servers.
7
+
8
+ ## Coming from Unicorn
9
+
10
+ ### Why Migrate?
11
+
12
+ #### Adaptative timeout
13
+
14
+ Pitchfork allows to extend the request timeout on a per request basis,
15
+ this can be helpful when trying to reduce the global request timeout
16
+ to a saner value. You can enforce a stricter value, and extend it
17
+ in the minority of offending endpoints.
18
+
19
+ #### Memory Usage - Reforking
20
+
21
+ If you are unsatisfied with Unicorn memory usage, but threaded Puma isn't an option
22
+ for you, then Pitchfork may be an option if you are able to enable reforking.
23
+
24
+ However be warned that making an application fork safe can be non-trivial,
25
+ and mistakes can lead to critical bugs.
26
+
27
+ #### Rack 3
28
+
29
+ As of Unicorn `6.1.0`, Rack 3 isn't yet supported by Unicorn.
30
+
31
+ Pitchfork is compatible with Rack 3.
32
+
33
+ ### Why Not Migrate?
34
+
35
+ #### Reduced Features
36
+
37
+ While Pitchfork started as a fork of Unicorn, many features such as daemonization,
38
+ pid file management, hot reload have been stripped.
39
+
40
+ Pitchfork only kept features that makes sense in a containerized world.
41
+
42
+ ## Coming from Puma
43
+
44
+ Generally speaking, compared to (threaded) Puma, Pitchfork *may* offer better latency and isolation at the expense of throughput.
45
+
46
+ ### Why Migrate?
47
+
48
+ #### Latency
49
+
50
+ If you suspect your application is subject to contention on the GVL or some other in-process shared resources,
51
+ then Pitchfork may offer improved latency.
52
+
53
+ It is however heavily recommended to first confirm this suspicion with profiling
54
+ tools such as [gvltools](https://github.com/Shopify/gvltools).
55
+
56
+ If you application isn't subject to in-process contention, Pitchfork is unlikely to improve latency.
57
+
58
+ #### Out of Band Garbage Collection
59
+
60
+ Another advantage of only processing a single request per process is that
61
+ [it allows to periodically trigger garbage collection when the worker isn't processing any request](https://shopify.engineering/adventures-in-garbage-collection).
62
+
63
+ This can significantly improve tail latency at the expense of throughput.
64
+
65
+ #### Resiliency and Isolation
66
+
67
+ Since Pitchfork workers have their own address space and only process one request at a time
68
+ it makes it much harder for one faulty request to impact another.
69
+
70
+ Even if a bug causes Ruby to crash, only the request that triggered the bug will be impacted.
71
+
72
+ If a bug causes Ruby to hang, the monitor process will SIGKILL the worker and the capacity will be
73
+ reclaimed.
74
+
75
+ This makes Pitchfork more resilient to some classes of bugs.
76
+
77
+ #### Thread Safety
78
+
79
+ Pitchfork doesn't require applications to be thread-safe. That is probably the worst reason
80
+ to migrate though.
81
+
82
+ ### Why Not Migrate?
83
+
84
+ #### Memory Usage
85
+
86
+ Without reforking enabled Pitchfork will without a doubt use more memory than threaded Puma.
87
+
88
+ With reforking enabled, results will vary based on the application profile and the number of Puma threads,
89
+ but should be in the same ballpark, sometimes better, but likely worse, this depends on many variables and
90
+ can't really be predicted.
91
+
92
+ However be warned that [making an application fork safe](FORK_SAFETY.md) can be non-trivial,
93
+ and mistakes can lead to critical bugs.
@@ -33,6 +33,7 @@ module Pitchfork
33
33
  DEFAULTS = {
34
34
  :soft_timeout => 20,
35
35
  :cleanup_timeout => 2,
36
+ :spawn_timeout => 10,
36
37
  :timeout => 22,
37
38
  :logger => default_logger,
38
39
  :worker_processes => 1,
@@ -60,6 +61,9 @@ module Pitchfork
60
61
  :after_worker_ready => lambda { |server, worker|
61
62
  server.logger.info("worker=#{worker.nr} gen=#{worker.generation} ready")
62
63
  },
64
+ :after_monitor_ready => lambda { |server|
65
+ server.logger.info("Monitor pid=#{Process.pid} ready")
66
+ },
63
67
  :after_worker_timeout => nil,
64
68
  :after_worker_hard_timeout => nil,
65
69
  :after_request_complete => nil,
@@ -141,6 +145,10 @@ module Pitchfork
141
145
  set_hook(:after_worker_ready, block_given? ? block : args[0])
142
146
  end
143
147
 
148
+ def after_monitor_ready(*args, &block)
149
+ set_hook(:after_monitor_ready, block_given? ? block : args[0], 1)
150
+ end
151
+
144
152
  def after_worker_timeout(*args, &block)
145
153
  set_hook(:after_worker_timeout, block_given? ? block : args[0], 3)
146
154
  end
@@ -167,6 +175,10 @@ module Pitchfork
167
175
  set_int(:timeout, soft_timeout + cleanup_timeout, 5)
168
176
  end
169
177
 
178
+ def spawn_timeout(seconds)
179
+ set_int(:spawn_timeout, seconds, 1)
180
+ end
181
+
170
182
  def worker_processes(nr)
171
183
  set_int(:worker_processes, nr, 1)
172
184
  end
@@ -74,13 +74,13 @@ module Pitchfork
74
74
  end
75
75
 
76
76
  # :stopdoc:
77
- attr_accessor :app, :timeout, :soft_timeout, :cleanup_timeout, :worker_processes,
77
+ attr_accessor :app, :timeout, :soft_timeout, :cleanup_timeout, :spawn_timeout, :worker_processes,
78
78
  :after_worker_fork, :after_mold_fork,
79
79
  :listener_opts, :children,
80
80
  :orig_app, :config, :ready_pipe,
81
81
  :default_middleware, :early_hints
82
82
  attr_writer :after_worker_exit, :before_worker_exit, :after_worker_ready, :after_request_complete,
83
- :refork_condition, :after_worker_timeout, :after_worker_hard_timeout
83
+ :refork_condition, :after_worker_timeout, :after_worker_hard_timeout, :after_monitor_ready
84
84
 
85
85
  attr_reader :logger
86
86
  include Pitchfork::SocketHelper
@@ -212,6 +212,8 @@ module Pitchfork
212
212
  wait_for_pending_workers
213
213
  end
214
214
 
215
+ @after_monitor_ready&.call(self)
216
+
215
217
  self
216
218
  end
217
219
 
@@ -554,6 +556,10 @@ module Pitchfork
554
556
  def spawn_worker(worker, detach:)
555
557
  logger.info("worker=#{worker.nr} gen=#{worker.generation} spawning...")
556
558
 
559
+ # We set the deadline before spawning the child so that if for some
560
+ # reason it gets stuck before reaching the worker loop,
561
+ # the monitor process will kill it.
562
+ worker.update_deadline(@spawn_timeout)
557
563
  Pitchfork.fork_sibling do
558
564
  worker.pid = Process.pid
559
565
 
@@ -6,14 +6,35 @@ module Pitchfork
6
6
  module Info
7
7
  @workers_count = 0
8
8
  @fork_safe = true
9
- @kept_ios = ObjectSpace::WeakMap.new
9
+
10
+ class WeakSet # :nodoc
11
+ def initialize
12
+ @map = ObjectSpace::WeakMap.new
13
+ end
14
+
15
+ if RUBY_VERSION < "2.7"
16
+ def <<(object)
17
+ @map[object] = object
18
+ end
19
+ else
20
+ def <<(object)
21
+ @map[object] = true
22
+ end
23
+ end
24
+
25
+ def each(&block)
26
+ @map.each_key(&block)
27
+ end
28
+ end
29
+
30
+ @kept_ios = WeakSet.new
10
31
 
11
32
  class << self
12
33
  attr_accessor :workers_count
13
34
 
14
35
  def keep_io(io)
15
36
  raise ArgumentError, "#{io.inspect} doesn't respond to :to_io" unless io.respond_to?(:to_io)
16
- @kept_ios[io] = io
37
+ @kept_ios << io
17
38
  io
18
39
  end
19
40
 
@@ -22,20 +43,14 @@ module Pitchfork
22
43
  end
23
44
 
24
45
  def close_all_ios!
25
- ignored_ios = [$stdin, $stdout, $stderr]
46
+ ignored_ios = [$stdin, $stdout, $stderr, STDIN, STDOUT, STDERR].uniq.compact
26
47
 
27
- @kept_ios.each_value do |io_like|
48
+ @kept_ios.each do |io_like|
28
49
  ignored_ios << (io_like.is_a?(IO) ? io_like : io_like.to_io)
29
50
  end
30
51
 
31
52
  ObjectSpace.each_object(IO) do |io|
32
- closed = begin
33
- io.closed?
34
- rescue IOError
35
- true
36
- end
37
-
38
- if !closed && io.autoclose? && !ignored_ios.include?(io)
53
+ if io_open?(io) && io_autoclosed?(io) && !ignored_ios.include?(io)
39
54
  if io.is_a?(TCPSocket)
40
55
  # If we inherited a TCP Socket, calling #close directly could send FIN or RST.
41
56
  # So we first reopen /dev/null to avoid that.
@@ -73,6 +88,20 @@ module Pitchfork
73
88
  def shutting_down?
74
89
  SharedMemory.shutting_down?
75
90
  end
91
+
92
+ private
93
+
94
+ def io_open?(io)
95
+ !io.closed?
96
+ rescue IOError
97
+ false
98
+ end
99
+
100
+ def io_autoclosed?(io)
101
+ io.autoclose?
102
+ rescue IOError
103
+ false
104
+ end
76
105
  end
77
106
  end
78
107
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pitchfork
4
- VERSION = "0.7.0"
4
+ VERSION = "0.9.0"
5
5
  module Const
6
6
  UNICORN_VERSION = '6.1.0'
7
7
  end
data/lib/pitchfork.rb CHANGED
@@ -36,157 +36,190 @@ module Pitchfork
36
36
 
37
37
  # :stopdoc:
38
38
 
39
- # This returns a lambda to pass in as the app, this does not "build" the
40
- # app The returned lambda will be called when it is
41
- # time to build the app.
42
- def self.builder(ru, op)
43
- # allow Configurator to parse cli switches embedded in the ru file
44
- op = Pitchfork::Configurator::RACKUP.merge!(:file => ru, :optparse => op)
45
- if ru =~ /\.ru$/ && !defined?(Rack::Builder)
46
- abort "rack and Rack::Builder must be available for processing #{ru}"
39
+ FORK_LOCK = Monitor.new
40
+ @socket_type = :SOCK_SEQPACKET
41
+
42
+ class << self
43
+ # :startdoc:
44
+
45
+ # Prevent Pitchfork from forking new children for the duration of the block.
46
+ #
47
+ # If you have background threads calling code that synchronize native locks,
48
+ # while the GVL is released, forking while they are held could leak to
49
+ # corrupted children.
50
+ #
51
+ # One example of this is `getaddrinfo(3)`, so opening a connection from a
52
+ # background thread has a chance to produce stuck children.
53
+ #
54
+ # To avoid this you can wrap such code in `Pitchfork.prevent_fork`:
55
+ #
56
+ # def heartbeat_thread
57
+ # @heartbeat_thread ||= Thread.new do
58
+ # loop do
59
+ # Pitchfork.prevent_fork do
60
+ # heartbeat
61
+ # end
62
+ # sleep 10
63
+ # end
64
+ # end
65
+ # end
66
+ #
67
+ def prevent_fork(&block)
68
+ FORK_LOCK.synchronize(&block)
47
69
  end
48
70
 
49
- # always called after config file parsing, may be called after forking
50
- lambda do |_, server|
51
- inner_app = case ru
52
- when /\.ru$/
53
- raw = File.read(ru)
54
- raw.sub!(/^__END__\n.*/, '')
55
- eval("Rack::Builder.new {(\n#{raw}\n)}.to_app", TOPLEVEL_BINDING, ru)
56
- else
57
- require ru
58
- Object.const_get(File.basename(ru, '.rb').capitalize)
71
+ # :stopdoc:
72
+
73
+ # This returns a lambda to pass in as the app, this does not "build" the
74
+ # app The returned lambda will be called when it is
75
+ # time to build the app.
76
+ def builder(ru, op)
77
+ # allow Configurator to parse cli switches embedded in the ru file
78
+ op = Pitchfork::Configurator::RACKUP.merge!(:file => ru, :optparse => op)
79
+ if ru =~ /\.ru$/ && !defined?(Rack::Builder)
80
+ abort "rack and Rack::Builder must be available for processing #{ru}"
59
81
  end
60
82
 
61
- Rack::Builder.new do
62
- use(Rack::ContentLength)
63
- use(Pitchfork::Chunked)
64
- use(Rack::Lint) if ENV["RACK_ENV"] == "development"
65
- use(Rack::TempfileReaper)
66
- run inner_app
67
- end.to_app
83
+ # always called after config file parsing, may be called after forking
84
+ lambda do |_, server|
85
+ inner_app = case ru
86
+ when /\.ru$/
87
+ raw = File.read(ru)
88
+ raw.sub!(/^__END__\n.*/, '')
89
+ eval("Rack::Builder.new {(\n#{raw}\n)}.to_app", TOPLEVEL_BINDING, ru)
90
+ else
91
+ require ru
92
+ Object.const_get(File.basename(ru, '.rb').capitalize)
93
+ end
94
+
95
+ Rack::Builder.new do
96
+ use(Rack::ContentLength)
97
+ use(Pitchfork::Chunked)
98
+ use(Rack::Lint) if ENV["RACK_ENV"] == "development"
99
+ use(Rack::TempfileReaper)
100
+ run inner_app
101
+ end.to_app
102
+ end
68
103
  end
69
- end
70
104
 
71
- # returns an array of strings representing TCP listen socket addresses
72
- # and Unix domain socket paths. This is useful for use with
73
- # Raindrops::Middleware under Linux: https://yhbt.net/raindrops/
74
- def self.listener_names
75
- Pitchfork::HttpServer::LISTENERS.map do |io|
76
- Pitchfork::SocketHelper.sock_name(io)
105
+ # returns an array of strings representing TCP listen socket addresses
106
+ # and Unix domain socket paths. This is useful for use with
107
+ # Raindrops::Middleware under Linux: https://yhbt.net/raindrops/
108
+ def listener_names
109
+ Pitchfork::HttpServer::LISTENERS.map do |io|
110
+ Pitchfork::SocketHelper.sock_name(io)
111
+ end
77
112
  end
78
- end
79
113
 
80
- def self.log_error(logger, prefix, exc)
81
- message = exc.message
82
- message = message.dump if /[[:cntrl:]]/ =~ message
83
- logger.error "#{prefix}: #{message} (#{exc.class})"
84
- exc.backtrace.each { |line| logger.error(line) }
85
- end
114
+ def log_error(logger, prefix, exc)
115
+ message = exc.message
116
+ message = message.dump if /[[:cntrl:]]/ =~ message
117
+ logger.error "#{prefix}: #{message} (#{exc.class})"
118
+ exc.backtrace.each { |line| logger.error(line) }
119
+ end
86
120
 
87
- F_SETPIPE_SZ = 1031 if RUBY_PLATFORM =~ /linux/
88
-
89
- def self.pipe # :nodoc:
90
- IO.pipe.each do |io|
91
- # shrink pipes to minimize impact on /proc/sys/fs/pipe-user-pages-soft
92
- # limits.
93
- if defined?(F_SETPIPE_SZ)
94
- begin
95
- io.fcntl(F_SETPIPE_SZ, Raindrops::PAGE_SIZE)
96
- rescue Errno::EINVAL
97
- # old kernel
98
- rescue Errno::EPERM
99
- # resizes fail if Linux is close to the pipe limit for the user
100
- # or if the user does not have permissions to resize
121
+ F_SETPIPE_SZ = 1031 if RUBY_PLATFORM =~ /linux/
122
+
123
+ def pipe # :nodoc:
124
+ IO.pipe.each do |io|
125
+ # shrink pipes to minimize impact on /proc/sys/fs/pipe-user-pages-soft
126
+ # limits.
127
+ if defined?(F_SETPIPE_SZ)
128
+ begin
129
+ io.fcntl(F_SETPIPE_SZ, Raindrops::PAGE_SIZE)
130
+ rescue Errno::EINVAL
131
+ # old kernel
132
+ rescue Errno::EPERM
133
+ # resizes fail if Linux is close to the pipe limit for the user
134
+ # or if the user does not have permissions to resize
135
+ end
101
136
  end
102
137
  end
103
138
  end
104
- end
105
139
 
106
- @socket_type = :SOCK_SEQPACKET
107
- def self.socketpair
108
- pair = UNIXSocket.socketpair(@socket_type).map { |s| MessageSocket.new(s) }
109
- pair[0].close_write
110
- pair[1].close_read
111
- pair
112
- rescue Errno::EPROTONOSUPPORT
113
- if @socket_type == :SOCK_SEQPACKET
114
- # macOS and very old linuxes don't support SOCK_SEQPACKET (SCTP).
115
- # In such case we can fallback to SOCK_STREAM (TCP)
116
- warn("SEQPACKET (SCTP) isn't supported, falling back to STREAM")
117
- @socket_type = :SOCK_STREAM
118
- retry
119
- else
120
- raise
140
+ def socketpair
141
+ pair = UNIXSocket.socketpair(@socket_type).map { |s| MessageSocket.new(s) }
142
+ pair[0].close_write
143
+ pair[1].close_read
144
+ pair
145
+ rescue Errno::EPROTONOSUPPORT
146
+ if @socket_type == :SOCK_SEQPACKET
147
+ # macOS and very old linuxes don't support SOCK_SEQPACKET (SCTP).
148
+ # In such case we can fallback to SOCK_STREAM (TCP)
149
+ warn("SEQPACKET (SCTP) isn't supported, falling back to STREAM")
150
+ @socket_type = :SOCK_STREAM
151
+ retry
152
+ else
153
+ raise
154
+ end
121
155
  end
122
- end
123
156
 
124
- def self.clean_fork(setpgid: true, &block)
125
- if pid = Process.fork
126
- if setpgid
127
- Process.setpgid(pid, pid) # Make into a group leader
157
+ def clean_fork(setpgid: true, &block)
158
+ if pid = FORK_LOCK.synchronize { Process.fork }
159
+ if setpgid
160
+ Process.setpgid(pid, pid) # Make into a group leader
161
+ end
162
+ return pid
128
163
  end
129
- return pid
130
- end
131
164
 
132
- begin
133
- # Pitchfork recursively refork the worker processes.
134
- # Because of this we need to unwind the stack before resuming execution
135
- # in the child, otherwise on each generation the available stack space would
136
- # get smaller and smaller until it's basically 0.
137
- #
138
- # The very first version of this method used to call fork from a new
139
- # thread, however this can cause issues with some native gems that rely on
140
- # pthread_atfork(3) or pthread_mutex_lock(3), as the new main thread would
141
- # now be different.
142
- #
143
- # A second version used to fork from a new fiber, but fibers have a much smaller
144
- # stack space (https://bugs.ruby-lang.org/issues/3187), so it would break large applications.
145
- #
146
- # The latest version now use `throw` to unwind the stack after the fork, it however
147
- # restrict it to be called only inside `handle_clean_fork`.
148
- if Thread.current[:pitchfork_handle_clean_fork]
149
- throw self, block
150
- else
151
- while block
152
- block = catch(self) do
153
- Thread.current[:pitchfork_handle_clean_fork] = true
154
- block.call
155
- nil
165
+ begin
166
+ # Pitchfork recursively refork the worker processes.
167
+ # Because of this we need to unwind the stack before resuming execution
168
+ # in the child, otherwise on each generation the available stack space would
169
+ # get smaller and smaller until it's basically 0.
170
+ #
171
+ # The very first version of this method used to call fork from a new
172
+ # thread, however this can cause issues with some native gems that rely on
173
+ # pthread_atfork(3) or pthread_mutex_lock(3), as the new main thread would
174
+ # now be different.
175
+ #
176
+ # A second version used to fork from a new fiber, but fibers have a much smaller
177
+ # stack space (https://bugs.ruby-lang.org/issues/3187), so it would break large applications.
178
+ #
179
+ # The latest version now use `throw` to unwind the stack after the fork, it however
180
+ # restrict it to be called only inside `handle_clean_fork`.
181
+ if Thread.current[:pitchfork_handle_clean_fork]
182
+ throw self, block
183
+ else
184
+ while block
185
+ block = catch(self) do
186
+ Thread.current[:pitchfork_handle_clean_fork] = true
187
+ block.call
188
+ nil
189
+ end
156
190
  end
157
191
  end
192
+ rescue
193
+ abort
194
+ else
195
+ exit
158
196
  end
159
- rescue
160
- abort
161
- else
162
- exit
163
197
  end
164
- end
165
198
 
166
- def self.fork_sibling(&block)
167
- if REFORKING_AVAILABLE
168
- # We double fork so that the new worker is re-attached back
169
- # to the master.
170
- # This requires either PR_SET_CHILD_SUBREAPER which is exclusive to Linux 3.4
171
- # or the master to be PID 1.
172
- if middle_pid = Process.fork # parent
173
- # We need to wait(2) so that the middle process doesn't end up a zombie.
174
- Process.wait(middle_pid)
175
- else # first child
176
- clean_fork(&block) # detach into a grand child
177
- exit
199
+ def fork_sibling(&block)
200
+ if REFORKING_AVAILABLE
201
+ # We double fork so that the new worker is re-attached back
202
+ # to the master.
203
+ # This requires either PR_SET_CHILD_SUBREAPER which is exclusive to Linux 3.4
204
+ # or the master to be PID 1.
205
+ if middle_pid = FORK_LOCK.synchronize { Process.fork } # parent
206
+ # We need to wait(2) so that the middle process doesn't end up a zombie.
207
+ Process.wait(middle_pid)
208
+ else # first child
209
+ clean_fork(&block) # detach into a grand child
210
+ exit
211
+ end
212
+ else
213
+ clean_fork(&block)
178
214
  end
179
- else
180
- clean_fork(&block)
181
- end
182
215
 
183
- nil # it's tricky to return the PID
184
- end
216
+ nil # it's tricky to return the PID
217
+ end
185
218
 
186
- def self.time_now(int = false)
187
- Process.clock_gettime(Process::CLOCK_MONOTONIC, int ? :second : :float_second)
219
+ def time_now(int = false)
220
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, int ? :second : :float_second)
221
+ end
188
222
  end
189
- # :startdoc:
190
223
  end
191
224
  # :enddoc:
192
225
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pitchfork
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-08-18 00:00:00.000000000 Z
11
+ date: 2023-09-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: raindrops
@@ -49,6 +49,7 @@ extensions:
49
49
  - ext/pitchfork_http/extconf.rb
50
50
  extra_rdoc_files: []
51
51
  files:
52
+ - ".devcontainer/devcontainer.json"
52
53
  - ".git-blame-ignore-revs"
53
54
  - ".gitattributes"
54
55
  - ".github/workflows/ci.yml"
@@ -71,6 +72,7 @@ files:
71
72
  - docs/REFORKING.md
72
73
  - docs/SIGNALS.md
73
74
  - docs/TUNING.md
75
+ - docs/WHY_MIGRATE.md
74
76
  - examples/constant_caches.ru
75
77
  - examples/echo.ru
76
78
  - examples/hello.ru