async-service 0.18.1 → 0.19.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
- checksums.yaml.gz.sig +0 -0
- data/context/best-practices.md +5 -5
- data/context/deployment.md +2 -2
- data/context/getting-started.md +6 -6
- data/context/service-architecture.md +10 -10
- data/lib/async/service/configuration.rb +1 -1
- data/lib/async/service/controller.rb +3 -3
- data/lib/async/service/generic.rb +57 -7
- data/lib/async/service/generic_service.rb +7 -57
- data/lib/async/service/health_checker.rb +7 -39
- data/lib/async/service/managed/environment.rb +71 -0
- data/lib/async/service/managed/health_checker.rb +49 -0
- data/lib/async/service/managed/service.rb +117 -0
- data/lib/async/service/managed_environment.rb +7 -61
- data/lib/async/service/managed_service.rb +6 -105
- data/lib/async/service/version.rb +3 -1
- data/lib/async/service.rb +0 -7
- data/readme.md +7 -4
- data/releases.md +7 -0
- data.tar.gz.sig +0 -0
- metadata +5 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b31f83d5dbdb36dfdf3e6be399d8ed560caedb465854a537a7c40741d121632
|
|
4
|
+
data.tar.gz: edae5fada2d9ebcd50b89a495df5901cd78a1c6eee94b270986592cf3ca79cfb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a0fe39a4f3a4bba63ad3bfefcafaf068191e3db7e710d0a3a7eaf9a5f92601035431aa088a39163a70426ad31dd03302c609f3a25996593290df33aac2b46a9
|
|
7
|
+
data.tar.gz: 37fc1659dd3e2cc747dc01fa85915694b7952eda09d361c1e72977f07804471221be796a0339fc61a98180acdc8042cca7fe0f195afdb5337a2f6ffae3ac8522
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/context/best-practices.md
CHANGED
|
@@ -61,7 +61,7 @@ Place environments in `lib/my_library/environment/`:
|
|
|
61
61
|
module MyLibrary
|
|
62
62
|
module Environment
|
|
63
63
|
module WebEnvironment
|
|
64
|
-
include Async::Service::
|
|
64
|
+
include Async::Service::Managed::Environment
|
|
65
65
|
|
|
66
66
|
def service_class
|
|
67
67
|
MyLibrary::Service::WebService
|
|
@@ -87,7 +87,7 @@ Place services in `lib/my_library/service/`:
|
|
|
87
87
|
# lib/my_library/service/web_service.rb
|
|
88
88
|
module MyLibrary
|
|
89
89
|
module Service
|
|
90
|
-
class WebService < Async::Service::
|
|
90
|
+
class WebService < Async::Service::Managed::Service
|
|
91
91
|
private def format_title(evaluator, server)
|
|
92
92
|
if server&.respond_to?(:connection_count)
|
|
93
93
|
"#{self.name} [#{evaluator.host}:#{evaluator.port}] (#{server.connection_count} connections)"
|
|
@@ -114,7 +114,7 @@ end
|
|
|
114
114
|
|
|
115
115
|
### Use `Managed::Environment` for Services
|
|
116
116
|
|
|
117
|
-
Include {ruby Async::Service::
|
|
117
|
+
Include {ruby Async::Service::Managed::Environment} for services that need robust lifecycle management using {ruby Async::Service::Managed::Service}:
|
|
118
118
|
|
|
119
119
|
```ruby
|
|
120
120
|
module WebEnvironment
|
|
@@ -201,7 +201,7 @@ end
|
|
|
201
201
|
|
|
202
202
|
### Use `Managed::Service` as Base Class
|
|
203
203
|
|
|
204
|
-
Prefer `Async::Service::
|
|
204
|
+
Prefer `Async::Service::Managed::Service` over `Generic` for most services:
|
|
205
205
|
|
|
206
206
|
```ruby
|
|
207
207
|
class WebService < Async::Service::ManagedService
|
|
@@ -243,7 +243,7 @@ module WebEnvironment
|
|
|
243
243
|
end
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
-
The `startup_timeout` ensures processes that hang during startup are detected, while `health_check_timeout` monitors processes after they've become ready. `
|
|
246
|
+
The `startup_timeout` ensures processes that hang during startup are detected, while `health_check_timeout` monitors processes after they've become ready. `Managed::Service` automatically sends `status!` messages during startup to keep the health check clock resetting until the service is ready.
|
|
247
247
|
|
|
248
248
|
### Implement Meaningful Process Titles
|
|
249
249
|
|
data/context/deployment.md
CHANGED
|
@@ -14,7 +14,7 @@ require "async/http"
|
|
|
14
14
|
require "async/service/managed_service"
|
|
15
15
|
require "async/service/managed_environment"
|
|
16
16
|
|
|
17
|
-
class WebService < Async::Service::
|
|
17
|
+
class WebService < Async::Service::Managed::Service
|
|
18
18
|
def start
|
|
19
19
|
super
|
|
20
20
|
@endpoint = @evaluator.endpoint
|
|
@@ -40,7 +40,7 @@ class WebService < Async::Service::ManagedService
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
module WebEnvironment
|
|
43
|
-
include Async::Service::
|
|
43
|
+
include Async::Service::Managed::Environment
|
|
44
44
|
|
|
45
45
|
def endpoint
|
|
46
46
|
Async::HTTP::Endpoint.parse("http://0.0.0.0:3000")
|
data/context/getting-started.md
CHANGED
|
@@ -14,13 +14,13 @@ $ bundle add async-service
|
|
|
14
14
|
|
|
15
15
|
`async-service` has several core concepts:
|
|
16
16
|
|
|
17
|
-
- A {ruby Async::Service::
|
|
17
|
+
- A {ruby Async::Service::Generic} which represents the base class for implementing services.
|
|
18
18
|
- A {ruby Async::Service::Configuration} which manages service configurations and environments.
|
|
19
19
|
- A {ruby Async::Service::Controller} which handles starting, stopping, and managing services.
|
|
20
20
|
|
|
21
21
|
## Usage
|
|
22
22
|
|
|
23
|
-
Services are long-running processes that can be managed as a group. Each service extends `Async::Service::
|
|
23
|
+
Services are long-running processes that can be managed as a group. Each service extends `Async::Service::Generic` and implements a `setup` method that defines how the service runs.
|
|
24
24
|
|
|
25
25
|
### Basic Service
|
|
26
26
|
|
|
@@ -31,7 +31,7 @@ Create a simple service that runs continuously:
|
|
|
31
31
|
|
|
32
32
|
require "async/service"
|
|
33
33
|
|
|
34
|
-
class HelloService < Async::Service::
|
|
34
|
+
class HelloService < Async::Service::Generic
|
|
35
35
|
def setup(container)
|
|
36
36
|
super
|
|
37
37
|
|
|
@@ -73,7 +73,7 @@ end
|
|
|
73
73
|
In your service implementation, you can access these values through the environment and evaluator:
|
|
74
74
|
|
|
75
75
|
```ruby
|
|
76
|
-
class WebServerService < Async::Service::
|
|
76
|
+
class WebServerService < Async::Service::Generic
|
|
77
77
|
def setup(container)
|
|
78
78
|
super
|
|
79
79
|
|
|
@@ -103,7 +103,7 @@ You can define multiple services in a single configuration file:
|
|
|
103
103
|
|
|
104
104
|
require "async/service"
|
|
105
105
|
|
|
106
|
-
class WebService < Async::Service::
|
|
106
|
+
class WebService < Async::Service::Generic
|
|
107
107
|
def setup(container)
|
|
108
108
|
super
|
|
109
109
|
|
|
@@ -115,7 +115,7 @@ class WebService < Async::Service::GenericService
|
|
|
115
115
|
end
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
class WorkerService < Async::Service::
|
|
118
|
+
class WorkerService < Async::Service::Generic
|
|
119
119
|
def setup(container)
|
|
120
120
|
super
|
|
121
121
|
|
|
@@ -82,14 +82,14 @@ end
|
|
|
82
82
|
|
|
83
83
|
## Service
|
|
84
84
|
|
|
85
|
-
The {ruby Async::Service::
|
|
85
|
+
The {ruby Async::Service::Generic} represents the service implementation layer. It handles the actual business logic of your services, provides access to configuration through environment evaluators, and manages the service lifecycle including startup, execution, and shutdown phases.
|
|
86
86
|
|
|
87
87
|
### Business Logic
|
|
88
88
|
|
|
89
89
|
Services contain the actual implementation of what your service does:
|
|
90
90
|
|
|
91
91
|
```ruby
|
|
92
|
-
class WebService < Async::Service::
|
|
92
|
+
class WebService < Async::Service::Generic
|
|
93
93
|
def setup(container)
|
|
94
94
|
super
|
|
95
95
|
|
|
@@ -148,7 +148,7 @@ By evaluating the log_path in the child process, you ensure that each instance h
|
|
|
148
148
|
Services define their startup, running, and shutdown behavior:
|
|
149
149
|
|
|
150
150
|
```ruby
|
|
151
|
-
class MyService < Async::Service::
|
|
151
|
+
class MyService < Async::Service::Generic
|
|
152
152
|
def start
|
|
153
153
|
super
|
|
154
154
|
# Service-specific startup logic including pre-loading libraries and binding to network interfaces before forking.
|
|
@@ -262,9 +262,9 @@ end
|
|
|
262
262
|
|
|
263
263
|
### Health Checking
|
|
264
264
|
|
|
265
|
-
For services using `Async::Service::
|
|
265
|
+
For services using `Async::Service::Managed::Service`, 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
266
|
|
|
267
|
-
For services extending `
|
|
267
|
+
For services extending `Generic`, you can set up health checking manually:
|
|
268
268
|
|
|
269
269
|
```ruby
|
|
270
270
|
def setup(container)
|
|
@@ -302,7 +302,7 @@ You can configure these timeouts via `container_options`:
|
|
|
302
302
|
|
|
303
303
|
```ruby
|
|
304
304
|
module WebEnvironment
|
|
305
|
-
include Async::Service::
|
|
305
|
+
include Async::Service::Managed::Environment
|
|
306
306
|
|
|
307
307
|
def container_options
|
|
308
308
|
super.merge(
|
|
@@ -313,7 +313,7 @@ module WebEnvironment
|
|
|
313
313
|
end
|
|
314
314
|
```
|
|
315
315
|
|
|
316
|
-
Note: `Async::Service::
|
|
316
|
+
Note: `Async::Service::Managed::Service` automatically handles health checking, container options, and process title formatting, so you typically don't need to set this up manually.
|
|
317
317
|
|
|
318
318
|
## How They Work Together
|
|
319
319
|
|
|
@@ -334,7 +334,7 @@ end
|
|
|
334
334
|
|
|
335
335
|
```ruby
|
|
336
336
|
# Services are defined using environments:
|
|
337
|
-
class WebService < Async::Service::
|
|
337
|
+
class WebService < Async::Service::Generic
|
|
338
338
|
def setup(container)
|
|
339
339
|
super
|
|
340
340
|
|
|
@@ -469,7 +469,7 @@ Create reusable configuration modules:
|
|
|
469
469
|
|
|
470
470
|
```ruby
|
|
471
471
|
module ManagedEnvironment
|
|
472
|
-
include Async::Service::
|
|
472
|
+
include Async::Service::Managed::Environment
|
|
473
473
|
|
|
474
474
|
def count
|
|
475
475
|
4
|
|
@@ -493,7 +493,7 @@ end
|
|
|
493
493
|
Build service hierarchies:
|
|
494
494
|
|
|
495
495
|
```ruby
|
|
496
|
-
class BaseWebService < Async::Service::
|
|
496
|
+
class BaseWebService < Async::Service::Generic
|
|
497
497
|
def setup(container)
|
|
498
498
|
super
|
|
499
499
|
# Common web service setup
|
|
@@ -42,7 +42,7 @@ module Async
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# Create a controller for the given services.
|
|
45
|
-
# @parameter services [Array(
|
|
45
|
+
# @parameter services [Array(Generic)] The services to control.
|
|
46
46
|
# @parameter options [Hash] Additional options for the controller.
|
|
47
47
|
# @returns [Controller] A new controller instance.
|
|
48
48
|
def self.for(*services, **options)
|
|
@@ -50,7 +50,7 @@ module Async
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# Initialize a new controller with services.
|
|
53
|
-
# @parameter services [Array(
|
|
53
|
+
# @parameter services [Array(Generic)] The services to manage.
|
|
54
54
|
# @parameter options [Hash] Options passed to the parent controller.
|
|
55
55
|
def initialize(services, **options)
|
|
56
56
|
super(**options)
|
|
@@ -59,7 +59,7 @@ module Async
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
# All the services associated with this controller.
|
|
62
|
-
# @attribute [Array(Async::Service::
|
|
62
|
+
# @attribute [Array(Async::Service::Generic)]
|
|
63
63
|
attr :services
|
|
64
64
|
|
|
65
65
|
# Start all named services.
|
|
@@ -1,15 +1,65 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright,
|
|
5
|
-
|
|
6
|
-
# Compatibility shim for Async::Service::Generic
|
|
7
|
-
# Use Async::Service::GenericService instead
|
|
8
|
-
require_relative "generic_service"
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
9
5
|
|
|
10
6
|
module Async
|
|
11
7
|
module Service
|
|
12
|
-
#
|
|
13
|
-
|
|
8
|
+
# Captures the stateful behaviour of a specific service.
|
|
9
|
+
# Specifies the interfaces required by derived classes.
|
|
10
|
+
#
|
|
11
|
+
# Designed to be invoked within an {Async::Controller::Container}.
|
|
12
|
+
class Generic
|
|
13
|
+
# Convert the given environment into a service if possible.
|
|
14
|
+
# @parameter environment [Environment] The environment to use to construct the service.
|
|
15
|
+
# @returns [Generic | Nil] The constructed service if the environment specifies a service class.
|
|
16
|
+
def self.wrap(environment)
|
|
17
|
+
evaluator = environment.evaluator
|
|
18
|
+
|
|
19
|
+
if evaluator.key?(:service_class)
|
|
20
|
+
if service_class = evaluator.service_class
|
|
21
|
+
return service_class.new(environment, evaluator)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Initialize the service from the given environment.
|
|
27
|
+
# @parameter environment [Environment]
|
|
28
|
+
def initialize(environment, evaluator = environment.evaluator)
|
|
29
|
+
@environment = environment
|
|
30
|
+
@evaluator = evaluator
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @attribute [Environment] The environment which is used to configure the service.
|
|
34
|
+
attr :environment
|
|
35
|
+
|
|
36
|
+
# Convert the service evaluator to a hash.
|
|
37
|
+
# @returns [Hash] A hash representation of the evaluator.
|
|
38
|
+
def to_h
|
|
39
|
+
@evaluator.to_h
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The name of the service - used for informational purposes like logging.
|
|
43
|
+
# e.g. `myapp.com`.
|
|
44
|
+
def name
|
|
45
|
+
@evaluator.name
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Start the service. Called before the container setup.
|
|
49
|
+
def start
|
|
50
|
+
Console.debug(self){"Starting service #{self.name}..."}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Setup the service into the specified container.
|
|
54
|
+
# @parameter container [Async::Container::Generic]
|
|
55
|
+
def setup(container)
|
|
56
|
+
Console.debug(self){"Setting up service #{self.name}..."}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Stop the service. Called after the container is stopped.
|
|
60
|
+
def stop(graceful = true)
|
|
61
|
+
Console.debug(self){"Stopping service #{self.name}..."}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
14
64
|
end
|
|
15
65
|
end
|
|
@@ -1,65 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2025
|
|
4
|
+
# Copyright, 2024-2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
# Compatibility shim for Async::Service::GenericService
|
|
7
|
+
# Use Async::Service::Generic instead
|
|
8
|
+
require_relative "generic"
|
|
5
9
|
|
|
6
10
|
module Async
|
|
7
11
|
module Service
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
#
|
|
11
|
-
# Designed to be invoked within an {Async::Controller::Container}.
|
|
12
|
-
class GenericService
|
|
13
|
-
# Convert the given environment into a service if possible.
|
|
14
|
-
# @parameter environment [Environment] The environment to use to construct the service.
|
|
15
|
-
# @returns [GenericService | Nil] The constructed service if the environment specifies a service class.
|
|
16
|
-
def self.wrap(environment)
|
|
17
|
-
evaluator = environment.evaluator
|
|
18
|
-
|
|
19
|
-
if evaluator.key?(:service_class)
|
|
20
|
-
if service_class = evaluator.service_class
|
|
21
|
-
return service_class.new(environment, evaluator)
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Initialize the service from the given environment.
|
|
27
|
-
# @parameter environment [Environment]
|
|
28
|
-
def initialize(environment, evaluator = environment.evaluator)
|
|
29
|
-
@environment = environment
|
|
30
|
-
@evaluator = evaluator
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# @attribute [Environment] The environment which is used to configure the service.
|
|
34
|
-
attr :environment
|
|
35
|
-
|
|
36
|
-
# Convert the service evaluator to a hash.
|
|
37
|
-
# @returns [Hash] A hash representation of the evaluator.
|
|
38
|
-
def to_h
|
|
39
|
-
@evaluator.to_h
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# The name of the service - used for informational purposes like logging.
|
|
43
|
-
# e.g. `myapp.com`.
|
|
44
|
-
def name
|
|
45
|
-
@evaluator.name
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Start the service. Called before the container setup.
|
|
49
|
-
def start
|
|
50
|
-
Console.debug(self){"Starting service #{self.name}..."}
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Setup the service into the specified container.
|
|
54
|
-
# @parameter container [Async::Container::Generic]
|
|
55
|
-
def setup(container)
|
|
56
|
-
Console.debug(self){"Setting up service #{self.name}..."}
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Stop the service. Called after the container is stopped.
|
|
60
|
-
def stop(graceful = true)
|
|
61
|
-
Console.debug(self){"Stopping service #{self.name}..."}
|
|
62
|
-
end
|
|
63
|
-
end
|
|
12
|
+
# @deprecated Use {Generic} instead.
|
|
13
|
+
GenericService = Generic
|
|
64
14
|
end
|
|
65
15
|
end
|
|
@@ -1,48 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2025, by Samuel Williams.
|
|
4
|
+
# Copyright, 2024-2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
# Compatibility shim for Async::Service::HealthChecker
|
|
7
|
+
# Use Async::Service::Managed::HealthChecker instead
|
|
8
|
+
require_relative "managed/health_checker"
|
|
5
9
|
|
|
6
10
|
module Async
|
|
7
11
|
module Service
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
# Start the health checker.
|
|
11
|
-
#
|
|
12
|
-
# If a timeout is specified, a transient child task will be scheduled, which will yield the instance if a block is given, then mark the instance as ready, and finally sleep for half the health check duration (so that we guarantee that the health check runs in time).
|
|
13
|
-
#
|
|
14
|
-
# If a timeout is not specified, the health checker will yield the instance immediately and then mark the instance as ready.
|
|
15
|
-
#
|
|
16
|
-
# @parameter instance [Object] The service instance to check.
|
|
17
|
-
# @parameter timeout [Numeric] The timeout duration for the health check.
|
|
18
|
-
# @parameter parent [Async::Task] The parent task to run the health checker in.
|
|
19
|
-
# @yields {|instance| ...} If a block is given, it will be called with the service instance at least once.
|
|
20
|
-
def health_checker(instance, timeout = @evaluator.health_check_timeout, parent: Async::Task.current, &block)
|
|
21
|
-
if timeout
|
|
22
|
-
parent.async(transient: true) do
|
|
23
|
-
while true
|
|
24
|
-
if block_given?
|
|
25
|
-
yield(instance)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# We deliberately create a fiber here, to confirm that fiber creation is working.
|
|
29
|
-
# If something has gone wrong with fiber allocation, we will crash here, and that's okay.
|
|
30
|
-
Fiber.new do
|
|
31
|
-
instance.healthy!
|
|
32
|
-
end.resume
|
|
33
|
-
|
|
34
|
-
sleep(timeout / 2.0)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
else
|
|
38
|
-
if block_given?
|
|
39
|
-
yield(instance)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
instance.healthy!
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
12
|
+
# @deprecated Use {Managed::HealthChecker} instead.
|
|
13
|
+
HealthChecker = Managed::HealthChecker
|
|
46
14
|
end
|
|
47
15
|
end
|
|
48
16
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Async
|
|
7
|
+
module Service
|
|
8
|
+
module Managed
|
|
9
|
+
# Default configuration for managed services.
|
|
10
|
+
#
|
|
11
|
+
# This is provided not because it is required, but to offer a sensible default for production services, and to expose a consistent interface for service configuration.
|
|
12
|
+
module Environment
|
|
13
|
+
# Number of instances to start. By default, when `nil`, uses `Etc.nprocessors`.
|
|
14
|
+
#
|
|
15
|
+
# @returns [Integer | nil] The number of instances to start, or `nil` to use the default.
|
|
16
|
+
def count
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The timeout duration for the startup. Set to `nil` to disable the startup timeout.
|
|
21
|
+
#
|
|
22
|
+
# @returns [Numeric | nil] The startup timeout in seconds.
|
|
23
|
+
def startup_timeout
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The timeout duration for the health check. Set to `nil` to disable the health check.
|
|
28
|
+
#
|
|
29
|
+
# @returns [Numeric | nil] The health check timeout in seconds.
|
|
30
|
+
def health_check_timeout
|
|
31
|
+
30
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Options to use when creating the container, including `restart`, `count`, and `health_check_timeout`.
|
|
35
|
+
#
|
|
36
|
+
# @returns [Hash] The options for the container.
|
|
37
|
+
def container_options
|
|
38
|
+
{
|
|
39
|
+
restart: true,
|
|
40
|
+
count: self.count,
|
|
41
|
+
startup_timeout: self.startup_timeout,
|
|
42
|
+
health_check_timeout: self.health_check_timeout,
|
|
43
|
+
}.compact
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Any scripts to preload before starting the service.
|
|
47
|
+
#
|
|
48
|
+
# @returns [Array(String)] The list of scripts to preload.
|
|
49
|
+
def preload
|
|
50
|
+
[]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# General tags for metrics, traces and logging.
|
|
54
|
+
#
|
|
55
|
+
# @returns [Array(String)] The tags for the service.
|
|
56
|
+
def tags
|
|
57
|
+
[]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Prepare the instance for running the service.
|
|
61
|
+
#
|
|
62
|
+
# This is called before {Async::Service::Managed::Service#run}.
|
|
63
|
+
#
|
|
64
|
+
# @parameter instance [Object] The container instance.
|
|
65
|
+
def prepare!(instance)
|
|
66
|
+
# No preparation required by default.
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Async
|
|
7
|
+
module Service
|
|
8
|
+
module Managed
|
|
9
|
+
# A health checker for managed services.
|
|
10
|
+
module HealthChecker
|
|
11
|
+
# Start the health checker.
|
|
12
|
+
#
|
|
13
|
+
# If a timeout is specified, a transient child task will be scheduled, which will yield the instance if a block is given, then mark the instance as ready, and finally sleep for half the health check duration (so that we guarantee that the health check runs in time).
|
|
14
|
+
#
|
|
15
|
+
# If a timeout is not specified, the health checker will yield the instance immediately and then mark the instance as ready.
|
|
16
|
+
#
|
|
17
|
+
# @parameter instance [Object] The service instance to check.
|
|
18
|
+
# @parameter timeout [Numeric] The timeout duration for the health check.
|
|
19
|
+
# @parameter parent [Async::Task] The parent task to run the health checker in.
|
|
20
|
+
# @yields {|instance| ...} If a block is given, it will be called with the service instance at least once.
|
|
21
|
+
def health_checker(instance, timeout = @evaluator.health_check_timeout, parent: Async::Task.current, &block)
|
|
22
|
+
if timeout
|
|
23
|
+
parent.async(transient: true) do
|
|
24
|
+
while true
|
|
25
|
+
if block_given?
|
|
26
|
+
yield(instance)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# We deliberately create a fiber here, to confirm that fiber creation is working.
|
|
30
|
+
# If something has gone wrong with fiber allocation, we will crash here, and that's okay.
|
|
31
|
+
Fiber.new do
|
|
32
|
+
instance.healthy!
|
|
33
|
+
end.resume
|
|
34
|
+
|
|
35
|
+
sleep(timeout / 2.0)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
else
|
|
39
|
+
if block_given?
|
|
40
|
+
yield(instance)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
instance.healthy!
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "../generic"
|
|
7
|
+
require_relative "health_checker"
|
|
8
|
+
|
|
9
|
+
module Async
|
|
10
|
+
module Service
|
|
11
|
+
# @namespace
|
|
12
|
+
module Managed
|
|
13
|
+
# A managed service with built-in health checking, restart policies, and process title formatting.
|
|
14
|
+
#
|
|
15
|
+
# This is the recommended base class for most services that need robust lifecycle management.
|
|
16
|
+
class Service < Generic
|
|
17
|
+
include HealthChecker
|
|
18
|
+
|
|
19
|
+
private def format_title(evaluator, server)
|
|
20
|
+
"#{evaluator.name} #{server.to_s}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Run the service logic.
|
|
24
|
+
#
|
|
25
|
+
# Override this method to implement your service. Return an object that represents the running service (e.g., a server, task, or worker pool) for health checking.
|
|
26
|
+
#
|
|
27
|
+
# @parameter instance [Object] The container instance.
|
|
28
|
+
# @parameter evaluator [Environment::Evaluator] The environment evaluator.
|
|
29
|
+
# @returns [Object] The service object (server, task, etc.)
|
|
30
|
+
def run(instance, evaluator)
|
|
31
|
+
Async do
|
|
32
|
+
sleep
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Preload any resources specified by the environment.
|
|
37
|
+
def preload!
|
|
38
|
+
if scripts = @evaluator.preload
|
|
39
|
+
root = @evaluator.root
|
|
40
|
+
scripts = Array(scripts)
|
|
41
|
+
|
|
42
|
+
scripts.each do |path|
|
|
43
|
+
Console.info(self){"Preloading #{path}..."}
|
|
44
|
+
full_path = File.expand_path(path, root)
|
|
45
|
+
require(full_path)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
rescue StandardError, LoadError => error
|
|
49
|
+
Console.warn(self, "Service preload failed!", error)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Start the service, including preloading resources.
|
|
53
|
+
def start
|
|
54
|
+
preload!
|
|
55
|
+
|
|
56
|
+
super
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Called after the service has been prepared but before it starts running.
|
|
60
|
+
#
|
|
61
|
+
# Override this method to emit metrics, logs, or perform other actions when the service preparation is complete.
|
|
62
|
+
#
|
|
63
|
+
# @parameter instance [Async::Container::Instance] The container instance.
|
|
64
|
+
# @parameter clock [Async::Clock] The monotonic start time from {Async::Clock.start}.
|
|
65
|
+
def emit_prepared(instance, clock)
|
|
66
|
+
# Override in subclasses as needed.
|
|
67
|
+
# Console.info(self, "Prepared...", duration: clock.total)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Called after the service has started running.
|
|
71
|
+
#
|
|
72
|
+
# Override this method to emit metrics, logs, or perform other actions when the service begins running.
|
|
73
|
+
#
|
|
74
|
+
# @parameter instance [Async::Container::Instance] The container instance.
|
|
75
|
+
# @parameter clock [Async::Clock] The monotonic start time from {Async::Clock.start}.
|
|
76
|
+
def emit_running(instance, clock)
|
|
77
|
+
# Override in subclasses as needed.
|
|
78
|
+
# Console.info(self, "Running...", duration: clock.total)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Set up the container with health checking and process title formatting.
|
|
82
|
+
# @parameter container [Async::Container] The container to configure.
|
|
83
|
+
def setup(container)
|
|
84
|
+
super
|
|
85
|
+
|
|
86
|
+
container_options = @evaluator.container_options
|
|
87
|
+
health_check_timeout = container_options[:health_check_timeout]
|
|
88
|
+
|
|
89
|
+
container.run(**container_options) do |instance|
|
|
90
|
+
clock = Async::Clock.start
|
|
91
|
+
|
|
92
|
+
Async do
|
|
93
|
+
evaluator = self.environment.evaluator
|
|
94
|
+
server = nil
|
|
95
|
+
|
|
96
|
+
health_checker(instance, health_check_timeout) do
|
|
97
|
+
if server
|
|
98
|
+
instance.name = format_title(evaluator, server)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
instance.status!("Preparing...")
|
|
103
|
+
evaluator.prepare!(instance)
|
|
104
|
+
emit_prepared(instance, clock)
|
|
105
|
+
|
|
106
|
+
instance.status!("Running...")
|
|
107
|
+
server = run(instance, evaluator)
|
|
108
|
+
emit_running(instance, clock)
|
|
109
|
+
|
|
110
|
+
instance.ready!
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -1,70 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2025
|
|
4
|
+
# Copyright, 2024-2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
# Compatibility shim for Async::Service::ManagedEnvironment
|
|
7
|
+
# Use Async::Service::Managed::Environment instead
|
|
8
|
+
require_relative "managed/environment"
|
|
5
9
|
|
|
6
10
|
module Async
|
|
7
11
|
module Service
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
# This is provided not because it is required, but to offer a sensible default for production services, and to expose a consistent interface for service configuration.
|
|
11
|
-
module ManagedEnvironment
|
|
12
|
-
# Number of instances to start. By default, when `nil`, uses `Etc.nprocessors`.
|
|
13
|
-
#
|
|
14
|
-
# @returns [Integer | nil] The number of instances to start, or `nil` to use the default.
|
|
15
|
-
def count
|
|
16
|
-
nil
|
|
17
|
-
end
|
|
18
|
-
|
|
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
|
-
|
|
26
|
-
# The timeout duration for the health check. Set to `nil` to disable the health check.
|
|
27
|
-
#
|
|
28
|
-
# @returns [Numeric | nil] The health check timeout in seconds.
|
|
29
|
-
def health_check_timeout
|
|
30
|
-
30
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Options to use when creating the container, including `restart`, `count`, and `health_check_timeout`.
|
|
34
|
-
#
|
|
35
|
-
# @returns [Hash] The options for the container.
|
|
36
|
-
def container_options
|
|
37
|
-
{
|
|
38
|
-
restart: true,
|
|
39
|
-
count: self.count,
|
|
40
|
-
startup_timeout: self.startup_timeout,
|
|
41
|
-
health_check_timeout: self.health_check_timeout,
|
|
42
|
-
}.compact
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Any scripts to preload before starting the service.
|
|
46
|
-
#
|
|
47
|
-
# @returns [Array(String)] The list of scripts to preload.
|
|
48
|
-
def preload
|
|
49
|
-
[]
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# General tags for metrics, traces and logging.
|
|
53
|
-
#
|
|
54
|
-
# @returns [Array(String)] The tags for the service.
|
|
55
|
-
def tags
|
|
56
|
-
[]
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Prepare the instance for running the service.
|
|
60
|
-
#
|
|
61
|
-
# This is called before {Async::Service::ManagedService#run}.
|
|
62
|
-
#
|
|
63
|
-
# @parameter instance [Object] The container instance.
|
|
64
|
-
def prepare!(instance)
|
|
65
|
-
# No preparation required by default.
|
|
66
|
-
end
|
|
67
|
-
end
|
|
12
|
+
# @deprecated Use {Managed::Environment} instead.
|
|
13
|
+
ManagedEnvironment = Managed::Environment
|
|
68
14
|
end
|
|
69
15
|
end
|
|
70
16
|
|
|
@@ -1,115 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2025
|
|
4
|
+
# Copyright, 2024-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
# Compatibility shim for Async::Service::ManagedService
|
|
7
|
+
# Use Async::Service::Managed::Service instead
|
|
8
|
+
require_relative "managed/service"
|
|
8
9
|
|
|
9
10
|
module Async
|
|
10
11
|
module Service
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
# This is the recommended base class for most services that need robust lifecycle management.
|
|
14
|
-
class ManagedService < GenericService
|
|
15
|
-
include HealthChecker
|
|
16
|
-
|
|
17
|
-
private def format_title(evaluator, server)
|
|
18
|
-
"#{evaluator.name} #{server.to_s}"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Run the service logic.
|
|
22
|
-
#
|
|
23
|
-
# Override this method to implement your service. Return an object that represents the running service (e.g., a server, task, or worker pool) for health checking.
|
|
24
|
-
#
|
|
25
|
-
# @parameter instance [Object] The container instance.
|
|
26
|
-
# @parameter evaluator [Environment::Evaluator] The environment evaluator.
|
|
27
|
-
# @returns [Object] The service object (server, task, etc.)
|
|
28
|
-
def run(instance, evaluator)
|
|
29
|
-
Async do
|
|
30
|
-
sleep
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Preload any resources specified by the environment.
|
|
35
|
-
def preload!
|
|
36
|
-
if scripts = @evaluator.preload
|
|
37
|
-
root = @evaluator.root
|
|
38
|
-
scripts = Array(scripts)
|
|
39
|
-
|
|
40
|
-
scripts.each do |path|
|
|
41
|
-
Console.info(self){"Preloading #{path}..."}
|
|
42
|
-
full_path = File.expand_path(path, root)
|
|
43
|
-
require(full_path)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
rescue StandardError, LoadError => error
|
|
47
|
-
Console.warn(self, "Service preload failed!", error)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Start the service, including preloading resources.
|
|
51
|
-
def start
|
|
52
|
-
preload!
|
|
53
|
-
|
|
54
|
-
super
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Called after the service has been prepared but before it starts running.
|
|
58
|
-
#
|
|
59
|
-
# Override this method to emit metrics, logs, or perform other actions when the service preparation is complete.
|
|
60
|
-
#
|
|
61
|
-
# @parameter instance [Async::Container::Instance] The container instance.
|
|
62
|
-
# @parameter clock [Async::Clock] The monotonic start time from {Async::Clock.start}.
|
|
63
|
-
def emit_prepared(instance, clock)
|
|
64
|
-
# Override in subclasses as needed.
|
|
65
|
-
# Console.info(self, "Prepared...", duration: clock.total)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Called after the service has started running.
|
|
69
|
-
#
|
|
70
|
-
# Override this method to emit metrics, logs, or perform other actions when the service begins running.
|
|
71
|
-
#
|
|
72
|
-
# @parameter instance [Async::Container::Instance] The container instance.
|
|
73
|
-
# @parameter clock [Async::Clock] The monotonic start time from {Async::Clock.start}.
|
|
74
|
-
def emit_running(instance, clock)
|
|
75
|
-
# Override in subclasses as needed.
|
|
76
|
-
# Console.info(self, "Running...", duration: clock.total)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Set up the container with health checking and process title formatting.
|
|
80
|
-
# @parameter container [Async::Container] The container to configure.
|
|
81
|
-
def setup(container)
|
|
82
|
-
super
|
|
83
|
-
|
|
84
|
-
container_options = @evaluator.container_options
|
|
85
|
-
health_check_timeout = container_options[:health_check_timeout]
|
|
86
|
-
|
|
87
|
-
container.run(**container_options) do |instance|
|
|
88
|
-
clock = Async::Clock.start
|
|
89
|
-
|
|
90
|
-
Async do
|
|
91
|
-
evaluator = self.environment.evaluator
|
|
92
|
-
server = nil
|
|
93
|
-
|
|
94
|
-
health_checker(instance, health_check_timeout) do
|
|
95
|
-
if server
|
|
96
|
-
instance.name = format_title(evaluator, server)
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
instance.status!("Preparing...")
|
|
101
|
-
evaluator.prepare!(instance)
|
|
102
|
-
emit_prepared(instance, clock)
|
|
103
|
-
|
|
104
|
-
instance.status!("Running...")
|
|
105
|
-
server = run(instance, evaluator)
|
|
106
|
-
emit_running(instance, clock)
|
|
107
|
-
|
|
108
|
-
instance.ready!
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|
|
12
|
+
# @deprecated Use {Managed::Service} instead.
|
|
13
|
+
ManagedService = Managed::Service
|
|
113
14
|
end
|
|
114
15
|
end
|
|
115
16
|
|
data/lib/async/service.rb
CHANGED
data/readme.md
CHANGED
|
@@ -29,6 +29,13 @@ Please see the [project documentation](https://socketry.github.io/async-service/
|
|
|
29
29
|
|
|
30
30
|
Please see the [project releases](https://socketry.github.io/async-service/releases/index) for all releases.
|
|
31
31
|
|
|
32
|
+
### v0.19.0
|
|
33
|
+
|
|
34
|
+
- Renamed `Async::Service::GenericService` -\> `Async::Service::Generic`, added compatibility alias for `GenericService`.
|
|
35
|
+
- Renamed `Async::Service::ManagedService` -\> `Async::Service::Managed::Service`, added compatibility alias for `ManagedService`.
|
|
36
|
+
- Renamed `Async::Service::ManagedEnvironment` -\> `Async::Service::Managed::Environment`, added compatibility alias for `ManagedEnvironment`.
|
|
37
|
+
- Renamed `Async::Service::HealthChecker` -\> `Async::Service::Managed::HealthChecker`, added compatibility alias for `HealthChecker`.
|
|
38
|
+
|
|
32
39
|
### v0.18.1
|
|
33
40
|
|
|
34
41
|
- Remove prepared and running log messages - not as useful as I imagined, and quite noisy.
|
|
@@ -74,10 +81,6 @@ Please see the [project releases](https://socketry.github.io/async-service/relea
|
|
|
74
81
|
- Make service name optional and improve code comments.
|
|
75
82
|
- Add `respond_to_missing?` for completeness.
|
|
76
83
|
|
|
77
|
-
### v0.12.0
|
|
78
|
-
|
|
79
|
-
- Add convenient `Configuration.build{...}` method for constructing inline configurations.
|
|
80
|
-
|
|
81
84
|
## Contributing
|
|
82
85
|
|
|
83
86
|
We welcome contributions to this project.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.19.0
|
|
4
|
+
|
|
5
|
+
- Renamed `Async::Service::GenericService` -\> `Async::Service::Generic`, added compatibility alias for `GenericService`.
|
|
6
|
+
- Renamed `Async::Service::ManagedService` -\> `Async::Service::Managed::Service`, added compatibility alias for `ManagedService`.
|
|
7
|
+
- Renamed `Async::Service::ManagedEnvironment` -\> `Async::Service::Managed::Environment`, added compatibility alias for `ManagedEnvironment`.
|
|
8
|
+
- Renamed `Async::Service::HealthChecker` -\> `Async::Service::Managed::HealthChecker`, added compatibility alias for `HealthChecker`.
|
|
9
|
+
|
|
3
10
|
## v0.18.1
|
|
4
11
|
|
|
5
12
|
- Remove prepared and running log messages - not as useful as I imagined, and quite noisy.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: async-service
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.19.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -100,6 +100,9 @@ files:
|
|
|
100
100
|
- lib/async/service/generic_service.rb
|
|
101
101
|
- lib/async/service/health_checker.rb
|
|
102
102
|
- lib/async/service/loader.rb
|
|
103
|
+
- lib/async/service/managed/environment.rb
|
|
104
|
+
- lib/async/service/managed/health_checker.rb
|
|
105
|
+
- lib/async/service/managed/service.rb
|
|
103
106
|
- lib/async/service/managed_environment.rb
|
|
104
107
|
- lib/async/service/managed_service.rb
|
|
105
108
|
- lib/async/service/version.rb
|
|
@@ -126,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
126
129
|
- !ruby/object:Gem::Version
|
|
127
130
|
version: '0'
|
|
128
131
|
requirements: []
|
|
129
|
-
rubygems_version:
|
|
132
|
+
rubygems_version: 4.0.3
|
|
130
133
|
specification_version: 4
|
|
131
134
|
summary: A service layer for Async.
|
|
132
135
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|