Skip to content

Commit b84916b

Browse files
Add support for startup_timeout.
1 parent 03b6acd commit b84916b

7 files changed

Lines changed: 74 additions & 5 deletions

File tree

async-service.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ Gem::Specification.new do |spec|
2727
spec.required_ruby_version = ">= 3.2"
2828

2929
spec.add_dependency "async"
30-
spec.add_dependency "async-container", "~> 0.16"
30+
spec.add_dependency "async-container", "~> 0.28"
3131
spec.add_dependency "string-format", "~> 0.2"
3232
end

guides/best-practices/readme.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ class WebService < Async::Service::ManagedService
208208
# Managed::Service automatically handles:
209209
# - Container setup with proper options.
210210
# - Health checking with process title updates.
211+
# - Status messages during startup to prevent premature timeouts.
211212
# - Preloading of scripts before startup.
212213

213214
private def format_title(evaluator, server)
@@ -222,6 +223,28 @@ class WebService < Async::Service::ManagedService
222223
end
223224
```
224225

226+
### Configure Timeouts for Slow-Starting Services
227+
228+
If your service takes a long time to start up (e.g., loading large datasets, connecting to external services), configure appropriate timeouts:
229+
230+
```ruby
231+
module WebEnvironment
232+
include Async::Service::ManagedEnvironment
233+
234+
# Allow 2 minutes for startup.
235+
def startup_timeout
236+
120
237+
end
238+
239+
# Require health checks every 30 seconds.
240+
def health_check_timeout
241+
30
242+
end
243+
end
244+
```
245+
246+
The `startup_timeout` ensures processes that hang during startup are detected, while `health_check_timeout` monitors processes after they've become ready. `ManagedService` automatically sends `status!` messages during startup to keep the health check clock resetting until the service is ready.
247+
225248
### Implement Meaningful Process Titles
226249

227250
Use the `format_title` method to provide dynamic process information:

guides/service-architecture/readme.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,20 +262,25 @@ end
262262

263263
### Health Checking
264264

265-
For services using `Async::Service::ManagedService`, health checking is handled automatically. For services extending `GenericService`, you can set up health checking manually:
265+
For services using `Async::Service::ManagedService`, health checking is handled automatically. The service sends `status!` messages during startup to prevent premature health check timeouts, then transitions to sending `ready!` messages once the service is actually ready.
266+
267+
For services extending `GenericService`, you can set up health checking manually:
266268

267269
```ruby
268270
def setup(container)
269271
container_options = @evaluator.container_options
270272
health_check_timeout = container_options[:health_check_timeout]
271273

272274
container.run(**container_options) do |instance|
275+
# Send status updates during startup:
276+
instance.status!("Preparing...")
273277
# Prepare your service.
274278

275279
Async do
276280
# Start your service.
281+
instance.status!("Starting...")
277282

278-
# Set up health checking, if a timeout was specified:
283+
# Once ready, set up health checking:
279284
health_checker(instance, health_check_timeout) do
280285
instance.name = "#{self.name}: #{current_status}"
281286
end
@@ -284,6 +289,30 @@ def setup(container)
284289
end
285290
```
286291

292+
#### Startup Timeout vs Health Check Timeout
293+
294+
The container supports two separate timeout mechanisms:
295+
296+
- **`startup_timeout`**: Maximum time a process can take to become ready (call `ready!`) before being terminated. This detects processes that hang during startup and never become ready.
297+
- **`health_check_timeout`**: Maximum time between health check messages after the process has become ready. This detects processes that stop responding after they've started.
298+
299+
Both timeouts use a single clock that starts when the process starts. The clock resets when the process becomes ready, transitioning from startup timeout monitoring to health check timeout monitoring.
300+
301+
You can configure these timeouts via `container_options`:
302+
303+
```ruby
304+
module WebEnvironment
305+
include Async::Service::ManagedEnvironment
306+
307+
def container_options
308+
super.merge(
309+
startup_timeout: 60, # Allow up to 60 seconds for startup.
310+
health_check_timeout: 30 # Require health checks every 30 seconds after ready.
311+
)
312+
end
313+
end
314+
```
315+
287316
Note: `Async::Service::ManagedService` automatically handles health checking, container options, and process title formatting, so you typically don't need to set this up manually.
288317

289318
## How They Work Together

lib/async/service/managed_environment.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ def count
1616
nil
1717
end
1818

19+
# The timeout duration for the startup. Set to `nil` to disable the startup timeout.
20+
#
21+
# @returns [Numeric | nil] The startup timeout in seconds.
22+
def startup_timeout
23+
nil
24+
end
25+
1926
# The timeout duration for the health check. Set to `nil` to disable the health check.
2027
#
2128
# @returns [Numeric | nil] The health check timeout in seconds.
@@ -30,6 +37,7 @@ def container_options
3037
{
3138
restart: true,
3239
count: self.count,
40+
startup_timeout: self.startup_timeout,
3341
health_check_timeout: self.health_check_timeout,
3442
}.compact
3543
end

lib/async/service/managed_service.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,12 @@ def setup(container)
7474
Async do
7575
evaluator = self.environment.evaluator
7676

77+
instance.status!("Preparing...")
7778
evaluator.prepare!(instance)
78-
7979
emit_prepared(instance, start_time)
8080

81+
instance.status!("Running...")
8182
server = run(instance, evaluator)
82-
8383
emit_running(instance, start_time)
8484

8585
health_checker(instance) do

releases.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- `ManagedService` now sends `status!` messages during startup to prevent premature health check timeouts for slow-starting services.
6+
- Support for `startup_timeout` option via `container_options` to detect processes that hang during startup and never become ready.
7+
38
## v0.16.0
49

510
- Renamed `Async::Service::Generic` -\> `Async::Service::GenericService`, added compatibilty alias.

test/async/service/managed_service.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ def mock_instance.ready!
7777
@ready_called = true
7878
end
7979

80+
def mock_instance.status!(text)
81+
# status! is called during startup but doesn't need to be tracked
82+
end
83+
8084
def mock_instance.ready_called?
8185
@ready_called || false
8286
end

0 commit comments

Comments
 (0)