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 +4 -4
- data/Cargo.lock +24 -24
- data/README.md +77 -6
- data/docs/guide.md +282 -0
- data/ext/shuttlebay/Cargo.toml +4 -4
- data/ext/shuttlebay/src/codec.rs +140 -29
- data/ext/shuttlebay/src/interned.rs +93 -0
- data/ext/shuttlebay/src/lib.rs +16 -0
- data/lib/shuttlebay/cache.rb +61 -0
- data/lib/shuttlebay/engine.rb +25 -0
- data/lib/shuttlebay/handler.rb +8 -6
- data/lib/shuttlebay/logger.rb +5 -0
- data/lib/shuttlebay/railtie.rb +42 -1
- data/lib/shuttlebay/runtime.rb +45 -3
- data/lib/shuttlebay/server.rb +80 -29
- data/lib/shuttlebay/version.rb +1 -1
- data/lib/shuttlebay/worker.rb +2 -2
- data/lib/shuttlebay.rb +1 -0
- metadata +21 -4
data/lib/shuttlebay/server.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
3
4
|
require 'socket'
|
|
4
5
|
|
|
5
6
|
module Shuttlebay
|
|
@@ -11,9 +12,10 @@ module Shuttlebay
|
|
|
11
12
|
STABLE_AFTER = 30.0
|
|
12
13
|
SIGNALS = { 'TERM' => 'T', 'INT' => 'I', 'QUIT' => 'Q', 'CHLD' => 'C' }.freeze
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
Group = Struct.new(:ship, :app_path, :socket_path, :workers, :threads, :app, :listener)
|
|
16
|
+
Slot = Struct.new(:group, :index, :pid, :started_at, :backoff, :respawn_at)
|
|
15
17
|
|
|
16
|
-
attr_reader :app_path, :socket_path, :workers, :threads, :ship
|
|
18
|
+
attr_reader :app_path, :socket_path, :workers, :threads, :ship, :forks
|
|
17
19
|
|
|
18
20
|
def self.from_env(argv, env = ENV)
|
|
19
21
|
socket_path = env.fetch('MS_SOCKET_PATH') do
|
|
@@ -24,34 +26,48 @@ module Shuttlebay
|
|
|
24
26
|
socket_path: socket_path,
|
|
25
27
|
workers: Integer(env.fetch('MS_BAY_WORKERS', '0'), 10),
|
|
26
28
|
threads: Integer(env.fetch('MS_BAY_THREADS', '3'), 10),
|
|
27
|
-
ship: env.fetch('MS_SHIP', 'rack')
|
|
29
|
+
ship: env.fetch('MS_SHIP', 'rack'),
|
|
30
|
+
forks: parse_forks(env.fetch('MS_BAY_FORKS', '[]'))
|
|
28
31
|
)
|
|
29
32
|
end
|
|
30
33
|
|
|
31
|
-
def
|
|
34
|
+
def self.parse_forks(json)
|
|
35
|
+
JSON.parse(json).map do |fork|
|
|
36
|
+
Group.new(fork.fetch('ship'), fork.fetch('app'), fork.fetch('socket'),
|
|
37
|
+
Integer(fork.fetch('workers')).clamp(1..), Integer(fork.fetch('threads')))
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(app_path:, socket_path:, workers:, threads:, ship:, forks: [], logger: Shuttlebay.logger)
|
|
32
42
|
raise ArgumentError, "workers must be >= 0 (got #{workers})" if workers.negative?
|
|
33
43
|
raise ArgumentError, "threads must be >= 1 (got #{threads})" if threads < 1
|
|
44
|
+
forks.each { |fork| raise ArgumentError, "#{fork.ship}: threads must be >= 1" if fork.threads < 1 }
|
|
34
45
|
|
|
35
46
|
@app_path = app_path
|
|
36
47
|
@socket_path = socket_path
|
|
37
48
|
@workers = workers
|
|
38
49
|
@threads = threads
|
|
39
50
|
@ship = ship
|
|
51
|
+
@forks = forks
|
|
40
52
|
@logger = logger
|
|
41
53
|
@master_pid = Process.pid
|
|
42
54
|
end
|
|
43
55
|
|
|
44
56
|
def run
|
|
45
57
|
Shuttlebay.running = true
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
primary = Group.new(ship, app_path, socket_path, workers.clamp(1..), threads, load_app(app_path))
|
|
59
|
+
forked = load_forks
|
|
60
|
+
Process.warmup if Shuttlebay.warmup?
|
|
61
|
+
[primary, *forked].each do |group|
|
|
62
|
+
group.listener = bind(group.socket_path)
|
|
63
|
+
@logger.info('app loaded, socket bound', ship: group.ship, app: group.app_path, socket: group.socket_path,
|
|
64
|
+
workers: group.workers, threads: group.threads)
|
|
65
|
+
end
|
|
50
66
|
|
|
51
|
-
if workers.zero?
|
|
52
|
-
worker(
|
|
67
|
+
if workers.zero? && forked.empty?
|
|
68
|
+
worker(primary, index: nil).run
|
|
53
69
|
else
|
|
54
|
-
supervise(
|
|
70
|
+
supervise([primary, *forked])
|
|
55
71
|
end
|
|
56
72
|
ensure
|
|
57
73
|
cleanup if Process.pid == @master_pid
|
|
@@ -59,7 +75,7 @@ module Shuttlebay
|
|
|
59
75
|
|
|
60
76
|
private
|
|
61
77
|
|
|
62
|
-
def load_app
|
|
78
|
+
def load_app(app_path)
|
|
63
79
|
require 'rack'
|
|
64
80
|
require 'rack/builder'
|
|
65
81
|
path = File.expand_path(app_path)
|
|
@@ -68,26 +84,45 @@ module Shuttlebay
|
|
|
68
84
|
Rack::Builder.parse_file(path)
|
|
69
85
|
end
|
|
70
86
|
|
|
71
|
-
def
|
|
87
|
+
def load_forks
|
|
88
|
+
forks.filter_map do |group|
|
|
89
|
+
group.app = load_app(group.app_path)
|
|
90
|
+
group
|
|
91
|
+
rescue StandardError, ScriptError => e
|
|
92
|
+
@logger.error('fork app failed to load, skipped', ship: group.ship, app: group.app_path,
|
|
93
|
+
**Logger.error_fields(e))
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def bind(socket_path)
|
|
72
99
|
if File.exist?(socket_path)
|
|
73
100
|
raise ArgumentError, "#{socket_path} exists and is not a socket" unless File.socket?(socket_path)
|
|
74
101
|
|
|
75
102
|
File.unlink(socket_path)
|
|
76
103
|
end
|
|
77
|
-
UNIXServer.new(socket_path).tap
|
|
104
|
+
UNIXServer.new(socket_path).tap do |server|
|
|
105
|
+
server.listen(1024)
|
|
106
|
+
bound << [server, socket_path]
|
|
107
|
+
end
|
|
78
108
|
end
|
|
79
109
|
|
|
80
|
-
def
|
|
81
|
-
|
|
82
|
-
index: index, master_pid: index && @master_pid, logger: @logger)
|
|
110
|
+
def bound
|
|
111
|
+
@bound ||= []
|
|
83
112
|
end
|
|
84
113
|
|
|
85
|
-
def
|
|
114
|
+
def worker(group, index:)
|
|
115
|
+
Worker.new(app: group.app, listener: group.listener, threads: group.threads, workers: group.workers,
|
|
116
|
+
ship: group.ship, index: index, master_pid: index && @master_pid, logger: @logger)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def supervise(groups)
|
|
86
120
|
wake_r, wake_w = IO.pipe
|
|
87
121
|
previous = trap_signals(SIGNALS, wake_w)
|
|
88
|
-
@app = app
|
|
89
122
|
Shuttlebay.run_hooks(:before_fork)
|
|
90
|
-
@slots =
|
|
123
|
+
@slots = groups.flat_map do |group|
|
|
124
|
+
Array.new(group.workers) { |index| Slot.new(group, index, nil, nil, RESPAWN_MIN, monotonic) }
|
|
125
|
+
end
|
|
91
126
|
@stopping = false
|
|
92
127
|
|
|
93
128
|
until @stopping && @slots.none?(&:pid)
|
|
@@ -120,14 +155,28 @@ module Shuttlebay
|
|
|
120
155
|
@slots.each do |slot|
|
|
121
156
|
next if slot.pid || slot.respawn_at > now
|
|
122
157
|
|
|
123
|
-
slot.pid = spawn_worker(slot
|
|
158
|
+
slot.pid = spawn_worker(slot)
|
|
124
159
|
slot.started_at = now
|
|
125
160
|
end
|
|
126
161
|
end
|
|
127
162
|
|
|
128
|
-
def spawn_worker(
|
|
129
|
-
|
|
130
|
-
|
|
163
|
+
def spawn_worker(slot)
|
|
164
|
+
fork do
|
|
165
|
+
become(slot.group)
|
|
166
|
+
run_worker_process(slot.index, SIGNALS) { worker(slot.group, index: slot.index) }
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# In a freshly forked worker of `group`: it serves that bay alone, so the
|
|
171
|
+
# other bays' listening sockets are closed, and a fork's worker logs (and
|
|
172
|
+
# sees in MS_SHIP) the fork's name rather than its parent's.
|
|
173
|
+
def become(group)
|
|
174
|
+
bound.each { |server, path| server.close unless path == group.socket_path }
|
|
175
|
+
return if group.ship == ship
|
|
176
|
+
|
|
177
|
+
ENV['MS_SHIP'] = group.ship
|
|
178
|
+
@logger = @logger.respond_to?(:for_ship) ? @logger.for_ship(group.ship) : @logger
|
|
179
|
+
Shuttlebay.logger = @logger
|
|
131
180
|
end
|
|
132
181
|
|
|
133
182
|
def reap
|
|
@@ -153,7 +202,7 @@ module Shuttlebay
|
|
|
153
202
|
slot.respawn_at = monotonic + delay
|
|
154
203
|
return if @stopping
|
|
155
204
|
|
|
156
|
-
@logger.warn('worker exited, respawning', index: slot.index, pid: pid,
|
|
205
|
+
@logger.warn('worker exited, respawning', ship: slot.group.ship, index: slot.index, pid: pid,
|
|
157
206
|
status: status && (status.exitstatus || status.termsig),
|
|
158
207
|
uptime: uptime.round(3), respawn_in: delay)
|
|
159
208
|
end
|
|
@@ -190,10 +239,12 @@ module Shuttlebay
|
|
|
190
239
|
end
|
|
191
240
|
|
|
192
241
|
def cleanup
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
242
|
+
bound.each do |server, path|
|
|
243
|
+
server.close
|
|
244
|
+
File.unlink(path) if File.socket?(path)
|
|
245
|
+
rescue SystemCallError => e
|
|
246
|
+
@logger.warn('socket cleanup failed', socket: path, error: e.message)
|
|
247
|
+
end
|
|
197
248
|
end
|
|
198
249
|
end
|
|
199
250
|
end
|
data/lib/shuttlebay/version.rb
CHANGED
data/lib/shuttlebay/worker.rb
CHANGED
|
@@ -41,7 +41,7 @@ module Shuttlebay
|
|
|
41
41
|
def run(trap_signals: true)
|
|
42
42
|
@wake_r, @wake_w = IO.pipe
|
|
43
43
|
previous = trap_signals(SIGNALS, @wake_w) if trap_signals
|
|
44
|
-
Shuttlebay.run_hooks(:worker_boot, @index) if forked?
|
|
44
|
+
Shuttlebay.run_hooks(:worker_boot, @index, @ship) if forked?
|
|
45
45
|
pool = spawn_pool
|
|
46
46
|
@logger.info('worker serving', index: @index, threads: @threads, reactors: @reactors)
|
|
47
47
|
|
|
@@ -158,7 +158,7 @@ module Shuttlebay
|
|
|
158
158
|
end
|
|
159
159
|
|
|
160
160
|
def run_shutdown_hooks
|
|
161
|
-
Shuttlebay.run_hooks(:worker_shutdown, @index)
|
|
161
|
+
Shuttlebay.run_hooks(:worker_shutdown, @index, @ship)
|
|
162
162
|
rescue StandardError => e
|
|
163
163
|
@logger.error('on_worker_shutdown hook failed', **Logger.error_fields(e))
|
|
164
164
|
end
|
data/lib/shuttlebay.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: shuttlebay
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -66,19 +66,33 @@ dependencies:
|
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '3.40'
|
|
68
68
|
- !ruby/object:Gem::Dependency
|
|
69
|
-
name:
|
|
69
|
+
name: guardship
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 0.2.0
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 0.2.0
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: padded_cell
|
|
70
84
|
requirement: !ruby/object:Gem::Requirement
|
|
71
85
|
requirements:
|
|
72
86
|
- - "~>"
|
|
73
87
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '
|
|
88
|
+
version: '0.2'
|
|
75
89
|
type: :development
|
|
76
90
|
prerelease: false
|
|
77
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
92
|
requirements:
|
|
79
93
|
- - "~>"
|
|
80
94
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: '
|
|
95
|
+
version: '0.2'
|
|
82
96
|
- !ruby/object:Gem::Dependency
|
|
83
97
|
name: railties
|
|
84
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -137,16 +151,19 @@ files:
|
|
|
137
151
|
- Cargo.toml
|
|
138
152
|
- LICENSE.txt
|
|
139
153
|
- README.md
|
|
154
|
+
- docs/guide.md
|
|
140
155
|
- exe/shuttlebay
|
|
141
156
|
- ext/shuttlebay/Cargo.toml
|
|
142
157
|
- ext/shuttlebay/extconf.rb
|
|
143
158
|
- ext/shuttlebay/src/codec.rs
|
|
144
159
|
- ext/shuttlebay/src/headers.rs
|
|
160
|
+
- ext/shuttlebay/src/interned.rs
|
|
145
161
|
- ext/shuttlebay/src/lib.rs
|
|
146
162
|
- ext/shuttlebay/src/link.rs
|
|
147
163
|
- lib/rackup/handler/shuttlebay.rb
|
|
148
164
|
- lib/shuttlebay.rb
|
|
149
165
|
- lib/shuttlebay/attached.rb
|
|
166
|
+
- lib/shuttlebay/cache.rb
|
|
150
167
|
- lib/shuttlebay/capybara.rb
|
|
151
168
|
- lib/shuttlebay/engine.rb
|
|
152
169
|
- lib/shuttlebay/handler.rb
|