pitchfork 0.13.0 → 0.14.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd251198665823484f036ddf2cf11c9415c3c07c04c8e6e809c0e74b05c1055f
4
- data.tar.gz: fc9475e4c4605b8bdf6cb7854946577413bbe980b5ac531a1638b43be4574d03
3
+ metadata.gz: 9d3eb42c934de40ea4ae4e25e5d334af536a03f9818dfec8798319744d1629a5
4
+ data.tar.gz: 5ea480e75dabff8298cb419a9ca1ba159af04985f4e02df8fcb51c6eff65b996
5
5
  SHA512:
6
- metadata.gz: ba08c99f11e707e37af4265ac9f1dd7f15c62243b2c65ffc6dd2a3189db51dc3512f5b59234b16c98aff396fc88585d0f2c255ab7af9921653c9fdb4efbe7e61
7
- data.tar.gz: '06790f57c21601cc73bcf34a01d89278c5bf6fb34b846a0534ab5d6982432c334ee101d50154a465e503fb92378c0e2809a5b94704e129aa4ee0cbfb5e88f058'
6
+ metadata.gz: c934f51445bc2b2904f9ecb84edfb0d454e87494fdb3a58210bf5e9b3e69887a6817be1d38f217696432ab40006448f97d22aa0eca866be4e09c6e80ca0499db
7
+ data.tar.gz: c2dfdf19c3a08aa91199b6289fef08add17281d60eaa3eefd361fb1de74aae0083ae2f10123293782d92b3c8a152b00c21676143c21b03487fecef2c8994d982
@@ -4,13 +4,17 @@ on: [push, pull_request]
4
4
 
5
5
  jobs:
6
6
  ruby:
7
- name: Ruby ${{ matrix.ruby }}
7
+ name: Ruby ${{ matrix.ruby }} ${{ matrix.rubyopt }}
8
8
  timeout-minutes: 15
9
9
  strategy:
10
10
  fail-fast: false
11
11
  matrix:
12
12
  os: ["ubuntu-latest"]
13
13
  ruby: ["ruby-head", "3.3", "3.2", "3.1", "3.0", "2.7", "2.6"]
14
+ rubyopt: [""]
15
+ include:
16
+ - ruby: "3.3"
17
+ rubyopt: "--enable-frozen-string-literal"
14
18
  runs-on: ubuntu-latest
15
19
  steps:
16
20
  - name: Check out code
@@ -26,4 +30,4 @@ jobs:
26
30
  run: sudo apt-get install -y ragel socat netcat
27
31
 
28
32
  - name: Tests ${{ matrix.rubyopt }}
29
- run: bundle exec rake
33
+ run: RUBYOPT="${{ matrix.rubyopt }}" bundle exec rake
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Unreleased
2
2
 
3
+ # 0.14.0
4
+
5
+ - Remove the dependency on `raindrops`.
6
+ - Add `X-Request-Id` header in the workers proctitle if present.
7
+ - Added experimental service workers.
8
+
3
9
  # 0.13.0
4
10
 
5
11
  - Fix compatibility with `--enable-frozen-string-literal` in preparation for Ruby 3.4.
data/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bookworm
1
+ FROM mcr.microsoft.com/devcontainers/ruby:3.3-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,9 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pitchfork (0.13.0)
4
+ pitchfork (0.14.0)
5
5
  rack (>= 2.0)
6
- raindrops (~> 0.7)
7
6
 
8
7
  GEM
9
8
  remote: https://rubygems.org/
@@ -12,8 +11,7 @@ GEM
12
11
  nio4r (2.7.0)
13
12
  puma (6.4.2)
14
13
  nio4r (~> 2.0)
15
- rack (3.0.10)
16
- raindrops (0.20.1)
14
+ rack (3.0.11)
17
15
  rake (13.0.6)
18
16
  rake-compiler (1.2.1)
19
17
  rake
@@ -299,8 +299,13 @@ end
299
299
  ```ruby
300
300
  after_mold_fork do |server, mold|
301
301
  Database.disconnect!
302
+
303
+ # Ruby < 3.3
302
304
  3.times { GC.start } # promote surviving objects to oldgen
303
305
  GC.compact
306
+
307
+ # Ruby >= 3.3
308
+ Process.warmup
304
309
  end
305
310
  ```
306
311
 
@@ -417,6 +422,39 @@ after_request_complete do |server, worker, env|
417
422
  end
418
423
  ```
419
424
 
425
+ ### `before_service_worker_ready` (experimental)
426
+
427
+ Experimental and may change at any point.
428
+
429
+ If defined, Pitchfork will spawn one extra worker, called a service worker
430
+ which doesn't accept incoming requests, but allows to perform service tasks
431
+ such as warming node local caches or emitting metrics.
432
+
433
+ Service workers are never promoted to molds, so it is safe to use threads and
434
+ other fork unsafe APIs.
435
+
436
+ This callback MUST not block. It should start one or multiple background threads
437
+ to perform tasks at regular intervals.
438
+
439
+ ```ruby
440
+ before_service_worker_ready do |server, service_worker|
441
+ Thread.new do
442
+ loop do
443
+ MyApp.emit_utilization_metrics
444
+ sleep 1
445
+ end
446
+ end
447
+ end
448
+ ```
449
+
450
+ ### `before_service_worker_exit` (experimental)
451
+
452
+ Experimental and may change at any point.
453
+
454
+ Optional.
455
+
456
+ Called whenever the service worker is exiting. This allow to do a clean shutdown.
457
+
420
458
  ## Reforking
421
459
 
422
460
  ### `refork_after`
@@ -0,0 +1,34 @@
1
+ # Unicorn Migration Guide
2
+
3
+ While Pitchfork started out as a patch on top of Unicorn, many Unicorn features
4
+ that don't make sense in containerized environments were removed to simplify the codebase.
5
+
6
+ This guide is intended to cover the most common changes you need to make to your configuration
7
+ in order to make the switch.
8
+
9
+ > [!NOTE]
10
+ > This document doesn't contain every incompatibility with Unicorn. If you encounter
11
+ additional incompatibilities, please open an Issue or a Pull Request to add your findings.
12
+
13
+ * The configurations `user`, `working_directory`, `stderr_path`, `stdout_path`, and `pid`
14
+ have been removed without replacement. Pitchfork is designed for modern deployment strategies
15
+ like Docker and Systemd, as such the responsibility for this functionality is delegated to
16
+ these systems.
17
+
18
+ * The configuration `preload_app` has been removed without replacement. Pitchfork will always behave
19
+ as if it is set to `true`.
20
+
21
+ * The Signal `USR2` has been repurposed for reforking. Remove `ExecReload` from your Sytemd unit
22
+ file, if it contains it. Reloading is not a supported feature of Pitchfork.
23
+
24
+ * The configuration `after_fork` has been split between `after_worker_fork` and `after_mold_fork`.
25
+
26
+ * If you use `unicorn-worker-killer` or similar gems, you will need to implement this functionally yourself since
27
+ there is no `pitchfork-worker-killer`. Changes to Pitchfork internals make this a pretty painless
28
+ ordeal, you can check out the following GitHub issue to get started: https://github.com/Shopify/pitchfork/issues/92
29
+
30
+ ## Reforking
31
+
32
+ [Reforking](REFORKING.md) is Pitchfork's main selling point. Give [Refork Safety](FORK_SAFETY.md#refork-safety) a read to understand
33
+ if your application may be compatible. [Enabling reforking](CONFIGURATION.md#refork_after) will give you memory savings above what Unicorn
34
+ is able to offer with its forking model.
data/docs/WHY_MIGRATE.md CHANGED
@@ -39,6 +39,11 @@ pid file management, hot reload have been stripped.
39
39
 
40
40
  Pitchfork only kept features that makes sense in a containerized world.
41
41
 
42
+ ### Migration Guide
43
+
44
+ If the above points convinced you to make the switch, take a look at the [migration guide](MIGRATING_FROM_UNICORN.md).
45
+ It will go over the most common changes you will need to make to use Pitchfork.
46
+
42
47
  ## Coming from Puma
43
48
 
44
49
  Generally speaking, compared to (threaded) Puma, Pitchfork *may* offer better latency and isolation at the expense of throughput.
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ # Minimal sample configuration file for Pitchfork
3
+
4
+ # listen 2007 # by default Pitchfork listens on port 8080
5
+ worker_processes 4 # this should be >= nr_cpus
6
+ refork_after [50, 100, 1000]
7
+
8
+ service_thread = nil
9
+ service_shutdown = false
10
+
11
+ before_service_worker_ready do |server, service|
12
+ service_thread = Thread.new do
13
+ server.logger.info "Service: start"
14
+ count = 1
15
+ until service_shutdown
16
+ server.logger.info "Service: ping count=#{count}"
17
+ count += 1
18
+ sleep 1
19
+ end
20
+ end
21
+ end
22
+
23
+ before_service_worker_exit do |server, service|
24
+ server.logger.info "Service: shutting down"
25
+ service_shutdown = true
26
+ service_thread&.join(2)
27
+ end
data/exe/pitchfork CHANGED
@@ -64,7 +64,7 @@ op = OptionParser.new("", 24, ' ') do |opts|
64
64
  warn "-s/--server only exists for compatibility with rackup"
65
65
  end
66
66
 
67
- # Unicorn-specific stuff
67
+ # Pitchfork-specific stuff
68
68
  opts.on("-l", "--listen {HOST:PORT|PATH}",
69
69
  "listen on HOST:PORT or PATH",
70
70
  "this may be specified multiple times",
@@ -72,11 +72,11 @@ op = OptionParser.new("", 24, ' ') do |opts|
72
72
  options[:listeners] << address
73
73
  end
74
74
 
75
- opts.on("-c", "--config-file FILE", "Unicorn-specific config file") do |f|
75
+ opts.on("-c", "--config-file FILE", "Pitchfork-specific config file") do |f|
76
76
  options[:config_file] = f
77
77
  end
78
78
 
79
- # I'm avoiding Unicorn-specific config options on the command-line.
79
+ # I'm avoiding Pitchfork-specific config options on the command-line.
80
80
  # IMNSHO, config options on the command-line are redundant given
81
81
  # config files and make things unnecessarily complicated with multiple
82
82
  # places to look for a config option.
@@ -89,7 +89,7 @@ op = OptionParser.new("", 24, ' ') do |opts|
89
89
  end
90
90
 
91
91
  opts.on_tail("-v", "--version", "Show version") do
92
- puts "#{cmd} v#{Pitchfork::Const::UNICORN_VERSION}"
92
+ puts "#{cmd} v#{Pitchfork::VERSION} (based on Unicorn v#{Pitchfork::Const::UNICORN_VERSION})"
93
93
  exit
94
94
  end
95
95
 
@@ -2,9 +2,11 @@
2
2
  # frozen_string_literal: true
3
3
  require 'mkmf'
4
4
 
5
+ append_cflags("-fvisibility=hidden")
5
6
  have_const("PR_SET_CHILD_SUBREAPER", "sys/prctl.h")
6
7
  have_func("rb_enc_interned_str", "ruby.h") # Ruby 3.0+
7
8
  have_func("rb_io_descriptor", "ruby.h") # Ruby 3.1+
9
+ have_func("getpagesize", "unistd.h")
8
10
 
9
11
  if RUBY_VERSION.start_with?('3.0.')
10
12
  # https://bugs.ruby-lang.org/issues/18772
@@ -0,0 +1,223 @@
1
+ /* Note: A large part of this code has been borrowed/stolen/adapted from raindrops. */
2
+
3
+ #include <ruby.h>
4
+ #include <unistd.h>
5
+ #include <sys/mman.h>
6
+ #include <errno.h>
7
+ #include <stddef.h>
8
+ #include <string.h>
9
+ #include <assert.h>
10
+
11
+ #define PAGE_MASK (~(page_size - 1))
12
+ #define PAGE_ALIGN(addr) (((addr) + page_size - 1) & PAGE_MASK)
13
+
14
+ static size_t slot_size = 128;
15
+
16
+ static void init_slot_size(void)
17
+ {
18
+ long tmp = 2;
19
+
20
+ #ifdef _SC_NPROCESSORS_CONF
21
+ tmp = sysconf(_SC_NPROCESSORS_CONF);
22
+ #endif
23
+ /* no point in padding on single CPU machines */
24
+ if (tmp == 1) {
25
+ slot_size = sizeof(unsigned long);
26
+ }
27
+ #ifdef _SC_LEVEL1_DCACHE_LINESIZE
28
+ if (tmp != 1) {
29
+ tmp = sysconf(_SC_LEVEL1_DCACHE_LINESIZE);
30
+ if (tmp > 0) {
31
+ slot_size = (size_t)tmp;
32
+ }
33
+ }
34
+ #endif
35
+ }
36
+
37
+ static size_t page_size = (size_t)-1;
38
+
39
+ static void init_page_size(void)
40
+ {
41
+ #if defined(_SC_PAGE_SIZE)
42
+ page_size = (size_t)sysconf(_SC_PAGE_SIZE);
43
+ #elif defined(_SC_PAGESIZE)
44
+ page_size = (size_t)sysconf(_SC_PAGESIZE);
45
+ #elif defined(HAVE_GETPAGESIZE)
46
+ page_size = (size_t)getpagesize();
47
+ #elif defined(PAGE_SIZE)
48
+ page_size = (size_t)PAGE_SIZE;
49
+ #elif defined(PAGESIZE)
50
+ page_size = (size_t)PAGESIZE;
51
+ #else
52
+ # error unable to detect page size for mmap()
53
+ #endif
54
+ if ((page_size == (size_t)-1) || (page_size < slot_size)) {
55
+ rb_raise(rb_eRuntimeError, "system page size invalid: %llu", (unsigned long long)page_size);
56
+ }
57
+ }
58
+
59
+ /* each slot is a counter */
60
+ struct slot {
61
+ unsigned long counter;
62
+ } __attribute__((packed));
63
+
64
+ /* allow mmap-ed regions to store more than one counter */
65
+ struct memory_page {
66
+ size_t size;
67
+ size_t capa;
68
+ struct slot *slots;
69
+ };
70
+
71
+ static void memory_page_free(void *ptr)
72
+ {
73
+ struct memory_page *page = (struct memory_page *)ptr;
74
+
75
+ if (page->slots != MAP_FAILED) {
76
+ int rv = munmap(page->slots, slot_size * page->capa);
77
+ if (rv != 0) {
78
+ rb_bug("Pitchfork::MemoryPage munmap failed in gc: %s", strerror(errno));
79
+ }
80
+ }
81
+
82
+ xfree(ptr);
83
+ }
84
+
85
+ static size_t memory_page_memsize(const void *ptr)
86
+ {
87
+ const struct memory_page *page = (const struct memory_page *)ptr;
88
+ size_t memsize = sizeof(struct memory_page);
89
+ if (page->slots != MAP_FAILED) {
90
+ memsize += slot_size * page->capa;
91
+ }
92
+ return memsize;
93
+ }
94
+
95
+ static const rb_data_type_t memory_page_type = {
96
+ .wrap_struct_name = "Pitchfork::MemoryPage",
97
+ .function = {
98
+ .dmark = NULL,
99
+ .dfree = memory_page_free,
100
+ .dsize = memory_page_memsize,
101
+ },
102
+ .flags = RUBY_TYPED_WB_PROTECTED,
103
+ };
104
+
105
+ static VALUE memory_page_alloc(VALUE klass)
106
+ {
107
+ struct memory_page *page;
108
+ VALUE obj = TypedData_Make_Struct(klass, struct memory_page, &memory_page_type, page);
109
+
110
+ page->slots = MAP_FAILED;
111
+ return obj;
112
+ }
113
+
114
+ static struct memory_page *memory_page_get(VALUE self)
115
+ {
116
+ struct memory_page *page;
117
+
118
+ TypedData_Get_Struct(self, struct memory_page, &memory_page_type, page);
119
+
120
+ if (page->slots == MAP_FAILED) {
121
+ rb_raise(rb_eStandardError, "invalid or freed Pitchfork::MemoryPage");
122
+ }
123
+
124
+ return page;
125
+ }
126
+
127
+ static unsigned long *memory_page_address(VALUE self, VALUE index)
128
+ {
129
+ struct memory_page *page = memory_page_get(self);
130
+ unsigned long off = FIX2ULONG(index) * slot_size;
131
+
132
+ if (off >= slot_size * page->size) {
133
+ rb_raise(rb_eArgError, "offset overrun");
134
+ }
135
+
136
+ return (unsigned long *)((unsigned long)page->slots + off);
137
+ }
138
+
139
+
140
+ static VALUE memory_page_aref(VALUE self, VALUE index)
141
+ {
142
+ return ULONG2NUM(*memory_page_address(self, index));
143
+ }
144
+
145
+ static VALUE memory_page_aset(VALUE self, VALUE index, VALUE value)
146
+ {
147
+ unsigned long *addr = memory_page_address(self, index);
148
+ *addr = NUM2ULONG(value);
149
+ return value;
150
+ }
151
+
152
+ static VALUE memory_page_initialize(VALUE self, VALUE size)
153
+ {
154
+ struct memory_page *page;
155
+ TypedData_Get_Struct(self, struct memory_page, &memory_page_type, page);
156
+
157
+ int tries = 1;
158
+
159
+ if (page->slots != MAP_FAILED) {
160
+ rb_raise(rb_eRuntimeError, "already initialized");
161
+ }
162
+
163
+ page->size = NUM2SIZET(size);
164
+ if (page->size < 1) {
165
+ rb_raise(rb_eArgError, "size must be >= 1");
166
+ }
167
+
168
+ size_t tmp = PAGE_ALIGN(slot_size * page->size);
169
+ page->capa = tmp / slot_size;
170
+ assert(PAGE_ALIGN(slot_size * page->capa) == tmp && "not aligned");
171
+
172
+ retry:
173
+ page->slots = mmap(NULL, tmp, PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0);
174
+
175
+ if (page->slots == MAP_FAILED) {
176
+ int err = errno;
177
+
178
+ if ((err == EAGAIN || err == ENOMEM) && tries-- > 0) {
179
+ rb_gc();
180
+ goto retry;
181
+ }
182
+ rb_sys_fail("mmap");
183
+ }
184
+
185
+ memset(page->slots, 0, tmp);
186
+
187
+ return self;
188
+ }
189
+
190
+ void init_pitchfork_memory_page(VALUE mPitchfork)
191
+ {
192
+ init_slot_size();
193
+ init_page_size();
194
+
195
+ VALUE rb_cMemoryPage = rb_define_class_under(mPitchfork, "MemoryPage", rb_cObject);
196
+
197
+ /*
198
+ * The size of one page of memory for a mmap()-ed MemoryPage region.
199
+ * Typically 4096 bytes under Linux.
200
+ */
201
+ rb_define_const(rb_cMemoryPage, "PAGE_SIZE", SIZET2NUM(page_size));
202
+
203
+ /*
204
+ * The size (in bytes) of a slot in a MemoryPage object.
205
+ * This is the size of a word on single CPU systems and
206
+ * the size of the L1 cache line size if detectable.
207
+ *
208
+ * Defaults to 128 bytes if undetectable.
209
+ */
210
+ rb_define_const(rb_cMemoryPage, "SLOT_SIZE", SIZET2NUM(slot_size));
211
+
212
+ rb_define_const(rb_cMemoryPage, "SLOTS", SIZET2NUM(page_size / slot_size));
213
+
214
+ /*
215
+ * The maximum value a slot counter can hold
216
+ */
217
+ rb_define_const(rb_cMemoryPage, "SLOT_MAX", ULONG2NUM((unsigned long)-1));
218
+
219
+ rb_define_alloc_func(rb_cMemoryPage, memory_page_alloc);
220
+ rb_define_private_method(rb_cMemoryPage, "initialize", memory_page_initialize, 1);
221
+ rb_define_method(rb_cMemoryPage, "[]", memory_page_aref, 1);
222
+ rb_define_method(rb_cMemoryPage, "[]=", memory_page_aset, 2);
223
+ }