shuttlebay 0.1.4 → 0.2.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: 4038d15bb64a686fa2abc8662520807bbdedc3ac6d3796688ccb57c4acef8aaa
4
- data.tar.gz: 1bfb6f3eb2456f5b3b16117a45d5daa97f9c58e08420bf53c888d023b3f28210
3
+ metadata.gz: 65929907b2e26eb80f96fbb87c41903626e320d301d8cc0e0bd36ddbe29228ae
4
+ data.tar.gz: c4ce7ea0de33c5acb37315645b895a25654f51699c283533aa07bb75a01ce408
5
5
  SHA512:
6
- metadata.gz: 472a1f07635596621b47dfd053f6ffacb4d579def811317a90160cff2d9c52c547ee562f6b2071f2e559fe1228e9a18ecccb3fcf8457d2cb3fcdc6707f67bf1b
7
- data.tar.gz: ffba9bc614e3b26f51db20e397d44cb640ca32a6ca58bda9e7c33f26eb4d9627991ee99e981cab37f9c047d2162980505f3aa1448695e229fd80a2f3b7ba3d37
6
+ metadata.gz: 3258cb81038a1687dfccf846fceb21fd517ff4bf89f383fcf5668de0b558da2834d8be5be9c0f61f8b23e4eb01cec508f6637407665f7ebb520b0335be0c36e1
7
+ data.tar.gz: 9986d6fa8615b58f4e08b7f205e2cdee43e5dbae170f7287cf6ef842dd3cfe991900b78ee724f57d74cfcd528502a66853b8b786864878c721012ebf3185c68f
data/Cargo.lock CHANGED
@@ -69,9 +69,9 @@ dependencies = [
69
69
 
70
70
  [[package]]
71
71
  name = "either"
72
- version = "1.18.0"
72
+ version = "1.19.0"
73
73
  source = "registry+https://github.com/rust-lang/crates.io-index"
74
- checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
74
+ checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
75
75
 
76
76
  [[package]]
77
77
  name = "glob"
@@ -102,9 +102,9 @@ checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
102
102
 
103
103
  [[package]]
104
104
  name = "libc"
105
- version = "0.2.190"
105
+ version = "0.2.191"
106
106
  source = "registry+https://github.com/rust-lang/crates.io-index"
107
- checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
107
+ checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
108
108
 
109
109
  [[package]]
110
110
  name = "libloading"
@@ -118,9 +118,9 @@ dependencies = [
118
118
 
119
119
  [[package]]
120
120
  name = "magnus"
121
- version = "0.9.2"
121
+ version = "0.9.3"
122
122
  source = "registry+https://github.com/rust-lang/crates.io-index"
123
- checksum = "b9068710e00d21be762edca59b9c4c8946dbdb29820d4f76ad559afea07982c9"
123
+ checksum = "40ef424404d166cbb71c8b8d19c9c589c748c4547c1afa1928c5504299f6c713"
124
124
  dependencies = [
125
125
  "bytes",
126
126
  "magnus-macros",
@@ -154,9 +154,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
154
154
 
155
155
  [[package]]
156
156
  name = "mothership-docking-protocol"
157
- version = "0.2.0"
157
+ version = "0.3.0"
158
158
  source = "registry+https://github.com/rust-lang/crates.io-index"
159
- checksum = "aaec5966e84e1a3c65b5846bd89c0c0621de6c75362f9c669636449fdf5a4b99"
159
+ checksum = "e43cb62ea01813b2fe88036c2e65c724a0020f97139023f4b8aea5e78626461b"
160
160
  dependencies = [
161
161
  "serde",
162
162
  "serde_json",
@@ -289,14 +289,14 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
289
289
  dependencies = [
290
290
  "proc-macro2",
291
291
  "quote",
292
- "syn 3.0.6",
292
+ "syn 3.0.9",
293
293
  ]
294
294
 
295
295
  [[package]]
296
296
  name = "serde_json"
297
- version = "1.0.151"
297
+ version = "1.0.154"
298
298
  source = "registry+https://github.com/rust-lang/crates.io-index"
299
- checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
299
+ checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
300
300
  dependencies = [
301
301
  "itoa",
302
302
  "memchr",
@@ -319,7 +319,7 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
319
319
 
320
320
  [[package]]
321
321
  name = "shuttlebay"
322
- version = "0.1.0"
322
+ version = "0.1.4"
323
323
  dependencies = [
324
324
  "magnus",
325
325
  "mothership-docking-protocol",
@@ -330,9 +330,9 @@ dependencies = [
330
330
 
331
331
  [[package]]
332
332
  name = "state-machines"
333
- version = "0.30.0"
333
+ version = "0.32.1"
334
334
  source = "registry+https://github.com/rust-lang/crates.io-index"
335
- checksum = "f2bc4296143483a9608a37ba4cf9955b42b8e2154dc58917df4f9cbe8120ebb4"
335
+ checksum = "f9b6f45e43f20a6eba0d7c03779b53775e0d9b40a452b89e55ea7aae87fa7a14"
336
336
  dependencies = [
337
337
  "state-machines-core",
338
338
  "state-machines-macro",
@@ -340,19 +340,19 @@ dependencies = [
340
340
 
341
341
  [[package]]
342
342
  name = "state-machines-core"
343
- version = "0.11.0"
343
+ version = "0.13.1"
344
344
  source = "registry+https://github.com/rust-lang/crates.io-index"
345
- checksum = "54b3da777f8a61919bdb3c6e9e445f2995f851b967e5d73b5949290dccfdcddd"
345
+ checksum = "f7a38d0afcf79a49438b4342ea90beec6490664a0725b2932f6e816d83bc46fe"
346
346
 
347
347
  [[package]]
348
348
  name = "state-machines-macro"
349
- version = "0.30.0"
349
+ version = "0.32.1"
350
350
  source = "registry+https://github.com/rust-lang/crates.io-index"
351
- checksum = "edf80d0c48518c7fb7fbb1dd4dda13a8dc189c1f15ac435545b351d5f2c276b1"
351
+ checksum = "604a644fc18fffd8845eb7f48e332d79a24750ac9d463a39d9b3cb10c44fcc28"
352
352
  dependencies = [
353
353
  "proc-macro2",
354
354
  "quote",
355
- "syn 2.0.119",
355
+ "syn 3.0.9",
356
356
  ]
357
357
 
358
358
  [[package]]
@@ -368,9 +368,9 @@ dependencies = [
368
368
 
369
369
  [[package]]
370
370
  name = "syn"
371
- version = "3.0.6"
371
+ version = "3.0.9"
372
372
  source = "registry+https://github.com/rust-lang/crates.io-index"
373
- checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
373
+ checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
374
374
  dependencies = [
375
375
  "proc-macro2",
376
376
  "quote",
@@ -394,14 +394,14 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
394
394
  dependencies = [
395
395
  "proc-macro2",
396
396
  "quote",
397
- "syn 3.0.6",
397
+ "syn 3.0.9",
398
398
  ]
399
399
 
400
400
  [[package]]
401
401
  name = "unicode-ident"
402
- version = "1.0.26"
402
+ version = "1.0.27"
403
403
  source = "registry+https://github.com/rust-lang/crates.io-index"
404
- checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
404
+ checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
405
405
 
406
406
  [[package]]
407
407
  name = "windows-link"
data/README.md CHANGED
@@ -15,6 +15,10 @@ flowchart TD
15
15
  master --> w1["worker 1<br/>threads"]
16
16
  ```
17
17
 
18
+ **New here?** [docs/guide.md](docs/guide.md) walks a Rails app from install to
19
+ production: the manifest, coming from Puma, moving `Rack::Attack` rules,
20
+ caching, TLS and HTTP/2, Docker and Kamal, several hosts.
21
+
18
22
  ## Install
19
23
 
20
24
  ```ruby
@@ -57,9 +61,59 @@ bundle exec mothership
57
61
  | Real client IP | `RemoteIp` + proxy config | `REMOTE_ADDR` resolved (Forwarded / PROXY protocol) |
58
62
  | Load balancing to workers | kernel accept / `wait_for_less_busy_worker` | idle-thread checkout; queueing in Mothership |
59
63
  | Request body buffering | App server | Mothership reads the whole body before claiming a thread |
64
+ | `ETag` / `If-None-Match` → 304 | `Rack::ETag`, `Rack::ConditionalGet` | Mothership hashes complete bodies and answers 304 |
65
+ | IP allowlists, rate limits | `Rack::Attack` | `ip_allowlist`, `rate_limits`, `trusted_proxies` in the manifest |
66
+ | Page / API caching | CDN, `Rack::Cache` | `[mothership.response_cache]`, purged by tag |
67
+
68
+ When Rails is loaded, the bundled Railtie removes the middleware Mothership
69
+ makes redundant:
70
+
71
+ - `Rack::Sendfile`: the engine offloads `to_path` bodies itself.
72
+ - `ActionDispatch::Static`: when Mothership serves the app's `public/` as a
73
+ static dir (`MS_STATIC_ROOTS`). `config.public_file_server.headers` it
74
+ does not send (per `MS_STATIC_HEADERS`) are logged at boot: move them to
75
+ the static dir's `headers` in the manifest.
76
+ - `Rack::ETag` and `Rack::ConditionalGet`: when Mothership validates
77
+ conditionals (`MS_CONDITIONAL_GET=1`), so bodies are not hashed twice.
78
+
79
+ Other Rack apps (Sinatra, Roda, Hanami) ask the same in `config.ru`;
80
+ `Shuttlebay.offloaded?` is false under any other server:
81
+
82
+ ```ruby
83
+ use Rack::ETag unless Shuttlebay.offloaded?(:conditional_get)
84
+ use Rack::ConditionalGet unless Shuttlebay.offloaded?(:conditional_get)
85
+ use Rack::Static, urls: [""], root: "public", cascade: true unless Shuttlebay.offloaded?(:static, "public")
86
+ use Rack::Sendfile unless Shuttlebay.offloaded?(:sendfile)
87
+ run App
88
+ ```
89
+
90
+ Rate limiting and allowlists in `Rack::Attack` are app configuration: drop
91
+ them yourself once the manifest enforces the same rules.
92
+
93
+ ## Caching at the edge
94
+
95
+ Mothership's response cache keeps entries until their data changes when the
96
+ app tags responses with what they show and purges the tags on change:
97
+
98
+ ```ruby
99
+ class BooksController < ApplicationController
100
+ def show
101
+ @book = Book.find(params[:id])
102
+ Shuttlebay::Cache.tag(response.headers, "book-#{@book.id}", "books")
103
+ expires_in 1.hour, public: true # or a manifest rule
104
+ end
105
+ end
106
+
107
+ class Book < ApplicationRecord
108
+ after_commit { Shuttlebay::Cache.purge(tags: ["book-#{id}", "books"]) }
109
+ end
110
+ ```
60
111
 
61
- When Rails is loaded, the bundled Railtie removes `Rack::Sendfile`, since the
62
- engine offloads `to_path` bodies itself.
112
+ `purge` also takes `urls:` (path and query), `prefixes:`, `all: true`, and
113
+ `soft: true` (mark stale and revalidate instead of dropping). It posts to
114
+ `MS_ADMIN_URL`, returns the number purged, is a no-op returning `nil` where
115
+ there is no Mothership, and raises `Shuttlebay::Error` when Mothership refuses
116
+ (e.g. no cache configured) or a network error when it is unreachable.
63
117
 
64
118
  ## Hooks
65
119
 
@@ -67,7 +121,7 @@ engine offloads `to_path` bodies itself.
67
121
 
68
122
  ```ruby
69
123
  Shuttlebay.before_fork { ActiveRecord::Base.connection_pool.disconnect! }
70
- Shuttlebay.on_worker_boot { |index| Rails.logger.info("worker #{index} up") }
124
+ Shuttlebay.on_worker_boot { |index, ship| Rails.logger.info("#{ship} worker #{index} up") }
71
125
  Shuttlebay.on_worker_shutdown { |index| Rails.logger.info("worker #{index} drained") }
72
126
  Shuttlebay.out_of_band { GC.start } # runs when a worker has no request in flight
73
127
  run Rails.application
@@ -152,18 +206,35 @@ For HTTPS, run Mothership with a `tls` bind and the app as a docking upstream
152
206
  - **Large uploads:** bodies over 1 MiB are spooled to an unlinked temp file
153
207
  (`rack.input` is a `Tempfile`). Memory stays bounded however large uploads
154
208
  are allowed to be.
209
+ - **Warmup:** the master calls `Process.warmup` once the app is loaded, so
210
+ workers fork from a compacted heap. `Shuttlebay.warmup = false` in
211
+ `config.ru` skips it.
212
+ - **Forks:** a `fork_from` bay in the manifest is served by this master: it
213
+ loads that bay's app too and forks its workers, sharing the preloaded heap.
214
+ A fork app that fails to load is logged and skipped. Each worker serves one
215
+ bay: it holds only that bay's socket, logs and sees (`MS_SHIP`) its name,
216
+ and hooks get it as their second argument (`|index, ship|`; hooks taking
217
+ only the index keep working).
155
218
  - **Signals:** on `TERM`, workers stop accepting, drop idle links, and finish
156
219
  in-flight requests within `MS_BAY_DRAIN_TIMEOUT` (Mothership passes the
157
220
  bay's `request_timeout`). A dead worker is re-forked with backoff.
158
221
  - **Deploys:** `kill -USR1` the mothership process to roll the bay to new code
159
222
  with no dropped requests (phased restart).
160
- - **TLS:** terminated by Mothership on a `tls` bind; the app sees
161
- `rack.url_scheme = "https"`.
223
+ - **TLS:** terminated by Mothership on a `tls` bind (guardship 0.2.1 and
224
+ later ship with TLS); the app sees `rack.url_scheme = "https"`.
225
+ - **HTTP/2:** browsers get it from Mothership's `tls` binds (ALPN), or from
226
+ the CDN or proxy in front; cleartext binds also take HTTP/2 with prior
227
+ knowledge. The app is unaffected: every request reaches it the same way,
228
+ as a Rack env, whatever the client spoke.
162
229
  - **Logs:** JSON lines on stdout, which Mothership passes through.
163
230
 
164
231
  ## Environment (set by Mothership)
165
232
 
166
- `MS_SOCKET_PATH`, `MS_SHIP`, `MS_BAY_WORKERS`, `MS_BAY_THREADS`, `MS_BAY_DRAIN_TIMEOUT`.
233
+ `MS_SOCKET_PATH`, `MS_SHIP`, `MS_BAY_WORKERS`, `MS_BAY_THREADS`, `MS_BAY_DRAIN_TIMEOUT`,
234
+ `MS_STATIC_ROOTS` (directories Mothership serves at `/`, one per line),
235
+ `MS_STATIC_HEADERS` (the headers their files carry, JSON by path),
236
+ `MS_CONDITIONAL_GET=1` (Mothership answers conditionals),
237
+ `MS_ADMIN_URL` (admin endpoint for cache purges).
167
238
 
168
239
  ## Development
169
240
 
data/docs/guide.md ADDED
@@ -0,0 +1,282 @@
1
+ # Running a Rails app on Shuttlebay
2
+
3
+ From `rails new` (or an app on Puma) to production behind Mothership. Any
4
+ Rack app works the same way; the Rails-specific parts are marked.
5
+
6
+ Mothership is the HTTP server: it terminates connections, serves `public/`,
7
+ compresses, limits, caches and routes. Shuttlebay runs your app on threads
8
+ that Mothership hands finished requests to. There is no Puma, no Thruster
9
+ and no nginx left in the request path.
10
+
11
+ ## 1. Install
12
+
13
+ ```ruby
14
+ # Gemfile
15
+ gem "shuttlebay"
16
+ gem "guardship" # the mothership binary, prebuilt for macOS, Linux, FreeBSD 15+
17
+ ```
18
+
19
+ ```bash
20
+ bundle install
21
+ bundle exec mothership --help
22
+ ```
23
+
24
+ Keep Puma in the Gemfile if you like: nothing below removes it, and
25
+ `rails server` without `-u` still uses it.
26
+
27
+ ## 2. Development and system tests
28
+
29
+ ```bash
30
+ bin/rails server -u shuttlebay
31
+ ```
32
+
33
+ ```ruby
34
+ # test/application_system_test_case.rb
35
+ require "shuttlebay/capybara"
36
+ Capybara.server = :shuttlebay
37
+ ```
38
+
39
+ Both start a Mothership attached to the app, so what you test is the stack
40
+ you deploy.
41
+
42
+ ## 3. The manifest
43
+
44
+ Mothership reads `ship-manifest.toml` from the directory it starts in:
45
+
46
+ ```toml
47
+ [mothership]
48
+ compression = true
49
+
50
+ [mothership.bind]
51
+ http = "0.0.0.0:3000"
52
+
53
+ [[mothership.static_dirs]]
54
+ path = "./public"
55
+ prefix = "/"
56
+ headers = { "cache-control" = "public, max-age=31536000" }
57
+
58
+ [[bays.http]]
59
+ name = "web"
60
+ command = "bundle"
61
+ args = ["exec", "shuttlebay", "config.ru"]
62
+ env = { RAILS_ENV = "production" }
63
+ workers = "auto"
64
+ threads = 5
65
+ routes = [{ bind = "http", pattern = "/.*" }]
66
+ ```
67
+
68
+ ```bash
69
+ bundle exec mothership clearance # validate the manifest
70
+ bundle exec mothership chart # what is routed where
71
+ bundle exec mothership # run
72
+ ```
73
+
74
+ - `workers = "auto"` forks one worker per CPU the process may use, as many as
75
+ memory affords (512 MiB each); a 1 CPU or 1 GiB container serves from one
76
+ process. A number, or `"$WEB_CONCURRENCY"`, works too.
77
+ - `threads` is the threads per worker, each with its own docking link to
78
+ Mothership. A request goes only to an idle thread; when all are busy it
79
+ waits in Mothership (`queue_timeout`, 30 s, then `503`).
80
+ - The static dir serves `public/` before any request reaches Rails, `/about`
81
+ from `about.html` and precompressed `.br`/`.gz` files included. The
82
+ Railtie sees that and drops `ActionDispatch::Static`. Give it the headers
83
+ your `config.public_file_server.headers` sets: the Railtie logs a warning at
84
+ boot for any it does not see there.
85
+
86
+ ## 4. Coming from Puma
87
+
88
+ | `config/puma.rb` | With Shuttlebay |
89
+ |---|---|
90
+ | `workers ENV.fetch("WEB_CONCURRENCY")` | `workers = "$WEB_CONCURRENCY"` (or `"auto"`) |
91
+ | `threads n, n` / `RAILS_MAX_THREADS` | `threads = "$RAILS_MAX_THREADS"` |
92
+ | `port` / `bind` | `[mothership.bind]` |
93
+ | `preload_app!` | always: `config.ru` loads in the master, then workers fork |
94
+ | `before_fork`, `on_worker_boot`, `on_worker_shutdown`, `out_of_band` | `Shuttlebay.before_fork { }` and friends, in `config.ru` |
95
+ | `worker_timeout` | the bay's `request_timeout` (`504`, then the stuck worker is recycled) |
96
+ | `pumactl phased-restart` / `plugin :tmp_restart` | `kill -USR1` the mothership process |
97
+ | `wait_for_less_busy_worker` | built in: requests only go to idle threads |
98
+
99
+ Hooks go in `config.ru`, before `run`:
100
+
101
+ ```ruby
102
+ # config.ru
103
+ require_relative "config/environment"
104
+
105
+ Shuttlebay.on_worker_boot { |index| Rails.logger.info("worker #{index} up") }
106
+ Shuttlebay.out_of_band { GC.start } # when a worker has no request in flight
107
+
108
+ run Rails.application
109
+ Rails.application.load_server
110
+ ```
111
+
112
+ `config/puma.rb` can stay for `rails server`; Shuttlebay never reads it.
113
+
114
+ ## 5. Let Mothership do the middleware's work
115
+
116
+ Under Mothership the Railtie removes, on its own, what Mothership already does:
117
+ `Rack::Sendfile`, `ActionDispatch::Static` (when the static dir serves
118
+ `public/`) and `Rack::ETag` / `Rack::ConditionalGet` (Mothership tags pages and
119
+ answers revalidations itself, off the GVL). Nothing changes under Puma.
120
+
121
+ `Rack::Attack` rules are your configuration, so you move them yourself:
122
+
123
+ ```ruby
124
+ # config/initializers/rack_attack.rb, before
125
+ Rack::Attack.throttle("api", limit: 60, period: 1.minute) do |req|
126
+ req.ip if req.path.start_with?("/api/")
127
+ end
128
+ ```
129
+
130
+ ```toml
131
+ # ship-manifest.toml, after
132
+ [[mothership.rate_limits]]
133
+ pattern = "^/api/"
134
+ limit = 60
135
+ period = "1m"
136
+ ```
137
+
138
+ Mothership keeps the counts in memory, with no cache-store write per request,
139
+ and answers `429` with `Retry-After`. Allowlists move to
140
+ `[[mothership.ip_allowlist]]`; see the Mothership README for both.
141
+
142
+ ## 6. Behind a CDN or a proxy
143
+
144
+ Behind Cloudflare, Kamal proxy or a load balancer, tell Mothership which
145
+ proxies to see through, so allowlists and rate limits apply to the visitor,
146
+ not to the proxy:
147
+
148
+ ```toml
149
+ [mothership]
150
+ trusted_proxies = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] # + your CDN's ranges
151
+ ```
152
+
153
+ The app's `REMOTE_ADDR` is the address that connected to Mothership (or the
154
+ PROXY-protocol one, with `proxy_protocol = true` on the bind), and the
155
+ forwarding headers pass through. Rails resolves `request.remote_ip` from them
156
+ as it always has: keep the configuration you need for that today (for
157
+ Cloudflare, `cloudflare-rails`).
158
+
159
+ ## 7. Caching
160
+
161
+ Mothership can cache whole responses in memory, the way a CDN does, purged by
162
+ tag when the data changes:
163
+
164
+ ```toml
165
+ [mothership]
166
+ metrics_port = 9090 # also serves the purge endpoint, on 127.0.0.1
167
+
168
+ [mothership.response_cache]
169
+ status_header = true # Cache-Status on responses
170
+
171
+ [[mothership.response_cache.rules]]
172
+ pattern = "^/api/"
173
+ ttl = "5m"
174
+ stale = "1m"
175
+ bypass_cookies = ["_myapp_session"] # signed-in users skip the cache
176
+ ```
177
+
178
+ ```ruby
179
+ class BooksController < ApplicationController
180
+ def show
181
+ @book = Book.find(params[:id])
182
+ Shuttlebay::Cache.tag(response.headers, "book-#{@book.id}")
183
+ end
184
+ end
185
+
186
+ class Book < ApplicationRecord
187
+ after_commit { Shuttlebay::Cache.purge(tags: ["book-#{id}"]) }
188
+ end
189
+ ```
190
+
191
+ A successful `POST`/`PUT`/`PATCH`/`DELETE` through Mothership already
192
+ invalidates its own URL. Requests with `Authorization`, or with a cookie a rule
193
+ does not let through, never touch a rule's cache. The README's caching section
194
+ covers the rest.
195
+
196
+ ## 8. TLS and HTTP/2
197
+
198
+ Behind a CDN or Kamal proxy, they terminate TLS and speak HTTP/2 to browsers;
199
+ Mothership listens on plain HTTP behind them. To terminate TLS in Mothership
200
+ itself (guardship 0.2.1 and later):
201
+
202
+ ```toml
203
+ [mothership.bind]
204
+ http = "0.0.0.0:80"
205
+ https = { addr = "0.0.0.0:443", tls = { cert = "/etc/letsencrypt/live/app/fullchain.pem", key = "/etc/letsencrypt/live/app/privkey.pem" } }
206
+ ```
207
+
208
+ Browsers then get HTTP/2 (negotiated over ALPN), the app sees
209
+ `rack.url_scheme = "https"`, and renewed certificates are picked up without a
210
+ restart.
211
+
212
+ ## 9. Docker and Kamal
213
+
214
+ Mothership replaces `./bin/thrust ./bin/rails server` in the Dockerfile Rails
215
+ generates:
216
+
217
+ ```dockerfile
218
+ EXPOSE 80
219
+ CMD ["bundle", "exec", "mothership"]
220
+ ```
221
+
222
+ with the bind on port 80 (`http = "0.0.0.0:80"`), where kamal-proxy expects
223
+ the app. Rails' `bin/docker-entrypoint` prepares the database only when the
224
+ command is `./bin/rails server`, so run that as a prelaunch job instead:
225
+
226
+ ```toml
227
+ [[mothership.prelaunch]]
228
+ name = "db-prepare"
229
+ command = "bin/rails"
230
+ args = ["db:prepare"]
231
+ ```
232
+
233
+ ```yaml
234
+ # config/deploy.yml
235
+ proxy:
236
+ host: app.example.com
237
+ ssl: true
238
+ healthcheck:
239
+ path: /up
240
+ ```
241
+
242
+ Mothership runs fine as PID 1: on Linux it reaps and forwards signals itself
243
+ (`docker run --init` is still the safer default elsewhere). Kamal deploys by
244
+ starting a new container and moving traffic to it once `/up` answers, so
245
+ phased restarts (`kill -USR1`) are for servers you deploy to in place.
246
+
247
+ On several hosts:
248
+
249
+ - Run prelaunch jobs (migrations) once: tag one host and elect it.
250
+
251
+ ```yaml
252
+ servers:
253
+ web:
254
+ - 10.0.0.5: flagship
255
+ - 10.0.0.6
256
+ env:
257
+ tags:
258
+ flagship:
259
+ MS_FLAGSHIP: "true"
260
+ ```
261
+
262
+ ```toml
263
+ [mothership.flagship]
264
+ enabled = true
265
+ election = "static"
266
+ static_flagship = "$MS_FLAGSHIP"
267
+ ```
268
+
269
+ - With a response cache, let the hosts relay purges and invalidations to one
270
+ another (`[mothership.response_cache.peers]` in the Mothership README).
271
+
272
+ ## 10. Watching it run
273
+
274
+ - `bundle exec mothership health` prints the fleet's health as JSON (needs
275
+ `metrics_port`); `/metrics` serves Prometheus metrics, `mothership_http_bay_*`
276
+ among them.
277
+ - Both Mothership and Shuttlebay log JSON lines to stdout.
278
+ - `503 Starting`: the bay is still booting (`boot_timeout`, 120 s).
279
+ - `503` once running: every thread was busy for `queue_timeout`; add
280
+ `threads` (I/O-bound apps) or `workers` (CPU-bound ones).
281
+ - `504`: no response head within `request_timeout` (60 s); the stuck worker
282
+ is recycled.
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "shuttlebay"
3
- version = "0.1.4" # x-release-please-version
3
+ version = "0.2.0" # x-release-please-version
4
4
  edition.workspace = true
5
5
  rust-version.workspace = true
6
6
  license.workspace = true
@@ -25,7 +25,7 @@ expect_used = "deny"
25
25
 
26
26
  [dependencies]
27
27
  magnus = { version = "0.9", features = ["bytes"] }
28
- rb-sys = "0.9.124"
29
- mothership-docking-protocol = "0.2.0"
28
+ rb-sys = "0.9.130"
29
+ mothership-docking-protocol = "0.3.0"
30
30
  serde_json = "1.0"
31
- state-machines = "0.30"
31
+ state-machines = "0.32"