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.
@@ -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
- Slot = Struct.new(:index, :pid, :started_at, :backoff, :respawn_at)
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 initialize(app_path:, socket_path:, workers:, threads:, ship:, logger: Shuttlebay.logger)
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
- app = load_app
47
- @listener = bind
48
- @logger.info('app loaded, socket bound', app: app_path, socket: socket_path,
49
- workers: workers, threads: threads)
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(app, index: nil).run
67
+ if workers.zero? && forked.empty?
68
+ worker(primary, index: nil).run
53
69
  else
54
- supervise(app)
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 bind
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 { |server| server.listen(1024) }
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 worker(app, index:)
81
- Worker.new(app: app, listener: @listener, threads: threads, workers: workers, ship: ship,
82
- index: index, master_pid: index && @master_pid, logger: @logger)
110
+ def bound
111
+ @bound ||= []
83
112
  end
84
113
 
85
- def supervise(app)
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 = Array.new(workers) { |index| Slot.new(index, nil, nil, RESPAWN_MIN, monotonic) }
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.index)
158
+ slot.pid = spawn_worker(slot)
124
159
  slot.started_at = now
125
160
  end
126
161
  end
127
162
 
128
- def spawn_worker(index)
129
- app = @app
130
- fork { run_worker_process(index, SIGNALS) { worker(app, index: index) } }
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
- @listener&.close
194
- File.unlink(socket_path) if socket_path && File.socket?(socket_path)
195
- rescue SystemCallError => e
196
- @logger.warn('socket cleanup failed', socket: socket_path, error: e.message)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shuttlebay
4
- VERSION = '0.1.4'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -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
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'shuttlebay/runtime'
4
+ require_relative 'shuttlebay/cache'
4
5
  require_relative 'shuttlebay/railtie' if defined?(Rails::Railtie)
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.1.4
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: minitest
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: '6.0'
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: '6.0'
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