pitchfork 0.13.0 → 0.14.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: 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
+ }