async-service 0.15.1 → 0.17.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 +30 -7
- data/context/deployment.md +284 -0
- data/context/getting-started.md +8 -43
- data/context/index.yaml +5 -0
- data/context/service-architecture.md +38 -9
- data/lib/async/service/configuration.rb +2 -2
- data/lib/async/service/controller.rb +3 -3
- data/lib/async/service/generic.rb +6 -56
- data/lib/async/service/generic_service.rb +65 -0
- data/lib/async/service/health_checker.rb +48 -0
- data/lib/async/service/managed_environment.rb +70 -0
- data/lib/async/service/managed_service.rb +108 -0
- data/lib/async/service/version.rb +1 -1
- data/license.md +1 -1
- data/readme.md +13 -8
- data/releases.md +11 -0
- data.tar.gz.sig +0 -0
- metadata +9 -8
- metadata.gz.sig +0 -0
- data/lib/async/service/managed/environment.rb +0 -48
- data/lib/async/service/managed/health_checker.rb +0 -49
- data/lib/async/service/managed/service.rb +0 -83
- data/lib/async/service/managed.rb +0 -17
|
@@ -3,63 +3,13 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2024-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
+
# Compatibility shim for Async::Service::Generic
|
|
7
|
+
# Use Async::Service::GenericService instead
|
|
8
|
+
require_relative "generic_service"
|
|
9
|
+
|
|
6
10
|
module Async
|
|
7
11
|
module Service
|
|
8
|
-
#
|
|
9
|
-
|
|
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
|
|
12
|
+
# @deprecated Use {GenericService} instead.
|
|
13
|
+
Generic = GenericService
|
|
64
14
|
end
|
|
65
15
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
# 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 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
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Async
|
|
7
|
+
module Service
|
|
8
|
+
# A health checker for managed services.
|
|
9
|
+
module HealthChecker
|
|
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.ready!
|
|
32
|
+
end.resume
|
|
33
|
+
|
|
34
|
+
sleep(timeout / 2)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
if block_given?
|
|
39
|
+
yield(instance)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
instance.ready!
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
# Default configuration for managed services.
|
|
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
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "generic_service"
|
|
7
|
+
require_relative "health_checker"
|
|
8
|
+
|
|
9
|
+
module Async
|
|
10
|
+
module Service
|
|
11
|
+
# A managed service with built-in health checking, restart policies, and process title formatting.
|
|
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 start_time [Async::Clock] The monotonic start time from {Async::Clock.start}.
|
|
63
|
+
def emit_prepared(instance, start_time)
|
|
64
|
+
# Override in subclasses as needed.
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Called after the service has started running.
|
|
68
|
+
#
|
|
69
|
+
# Override this method to emit metrics, logs, or perform other actions when the service begins running.
|
|
70
|
+
#
|
|
71
|
+
# @parameter instance [Async::Container::Instance] The container instance.
|
|
72
|
+
# @parameter start_time [Async::Clock] The monotonic start time from {Async::Clock.start}.
|
|
73
|
+
def emit_running(instance, start_time)
|
|
74
|
+
# Override in subclasses as needed.
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Set up the container with health checking and process title formatting.
|
|
78
|
+
# @parameter container [Async::Container] The container to configure.
|
|
79
|
+
def setup(container)
|
|
80
|
+
super
|
|
81
|
+
|
|
82
|
+
container_options = @evaluator.container_options
|
|
83
|
+
health_check_timeout = container_options[:health_check_timeout]
|
|
84
|
+
|
|
85
|
+
container.run(**container_options) do |instance|
|
|
86
|
+
start_time = Async::Clock.start
|
|
87
|
+
|
|
88
|
+
Async do
|
|
89
|
+
evaluator = self.environment.evaluator
|
|
90
|
+
|
|
91
|
+
instance.status!("Preparing...")
|
|
92
|
+
evaluator.prepare!(instance)
|
|
93
|
+
emit_prepared(instance, start_time)
|
|
94
|
+
|
|
95
|
+
instance.status!("Running...")
|
|
96
|
+
server = run(instance, evaluator)
|
|
97
|
+
emit_running(instance, start_time)
|
|
98
|
+
|
|
99
|
+
health_checker(instance) do
|
|
100
|
+
instance.name = format_title(evaluator, server)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
data/license.md
CHANGED
data/readme.md
CHANGED
|
@@ -23,10 +23,23 @@ Please see the [project documentation](https://socketry.github.io/async-service/
|
|
|
23
23
|
|
|
24
24
|
- [Best Practices](https://socketry.github.io/async-service/guides/best-practices/index) - This guide outlines recommended patterns and practices for building robust, maintainable services with `async-service`.
|
|
25
25
|
|
|
26
|
+
- [Deployment](https://socketry.github.io/async-service/guides/deployment/index) - This guide explains how to deploy `async-service` applications using systemd and Kubernetes. We'll use a simple example service to demonstrate deployment configurations.
|
|
27
|
+
|
|
26
28
|
## Releases
|
|
27
29
|
|
|
28
30
|
Please see the [project releases](https://socketry.github.io/async-service/releases/index) for all releases.
|
|
29
31
|
|
|
32
|
+
### v0.17.0
|
|
33
|
+
|
|
34
|
+
- `ManagedService` now sends `status!` messages during startup to prevent premature health check timeouts for slow-starting services.
|
|
35
|
+
- Support for `startup_timeout` option via `container_options` to detect processes that hang during startup and never become ready.
|
|
36
|
+
|
|
37
|
+
### v0.16.0
|
|
38
|
+
|
|
39
|
+
- Renamed `Async::Service::Generic` -\> `Async::Service::GenericService`, added compatibilty alias.
|
|
40
|
+
- Renamed `Async::Service::Managed::Service` -\> `Async::Service::ManagedService`.
|
|
41
|
+
- Renamed `Async::Service::Managed::Environment` -\> `Async::Service::ManagedEnvironment`.
|
|
42
|
+
|
|
30
43
|
### v0.15.1
|
|
31
44
|
|
|
32
45
|
- `Managed::Service` should run within `Async do ... end`.
|
|
@@ -64,14 +77,6 @@ Please see the [project releases](https://socketry.github.io/async-service/relea
|
|
|
64
77
|
- Add `Environment::Evaluator#as_json` for JSON serialization support.
|
|
65
78
|
- Allow constructing a configuration with existing environments.
|
|
66
79
|
|
|
67
|
-
### v0.9.0
|
|
68
|
-
|
|
69
|
-
- Allow providing a list of modules to include in environments.
|
|
70
|
-
|
|
71
|
-
### v0.8.0
|
|
72
|
-
|
|
73
|
-
- Introduce `Environment#implements?` and related methods for interface checking.
|
|
74
|
-
|
|
75
80
|
## Contributing
|
|
76
81
|
|
|
77
82
|
We welcome contributions to this project.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.17.0
|
|
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
|
+
|
|
8
|
+
## v0.16.0
|
|
9
|
+
|
|
10
|
+
- Renamed `Async::Service::Generic` -\> `Async::Service::GenericService`, added compatibilty alias.
|
|
11
|
+
- Renamed `Async::Service::Managed::Service` -\> `Async::Service::ManagedService`.
|
|
12
|
+
- Renamed `Async::Service::Managed::Environment` -\> `Async::Service::ManagedEnvironment`.
|
|
13
|
+
|
|
3
14
|
## v0.15.1
|
|
4
15
|
|
|
5
16
|
- `Managed::Service` should run within `Async do ... end`.
|
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.17.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -58,14 +58,14 @@ dependencies:
|
|
|
58
58
|
requirements:
|
|
59
59
|
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '0.
|
|
61
|
+
version: '0.28'
|
|
62
62
|
type: :runtime
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '0.
|
|
68
|
+
version: '0.28'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: string-format
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -87,6 +87,7 @@ extra_rdoc_files: []
|
|
|
87
87
|
files:
|
|
88
88
|
- bin/async-service
|
|
89
89
|
- context/best-practices.md
|
|
90
|
+
- context/deployment.md
|
|
90
91
|
- context/getting-started.md
|
|
91
92
|
- context/index.yaml
|
|
92
93
|
- context/service-architecture.md
|
|
@@ -96,11 +97,11 @@ files:
|
|
|
96
97
|
- lib/async/service/environment.rb
|
|
97
98
|
- lib/async/service/formatting.rb
|
|
98
99
|
- lib/async/service/generic.rb
|
|
100
|
+
- lib/async/service/generic_service.rb
|
|
101
|
+
- lib/async/service/health_checker.rb
|
|
99
102
|
- lib/async/service/loader.rb
|
|
100
|
-
- lib/async/service/
|
|
101
|
-
- lib/async/service/
|
|
102
|
-
- lib/async/service/managed/health_checker.rb
|
|
103
|
-
- lib/async/service/managed/service.rb
|
|
103
|
+
- lib/async/service/managed_environment.rb
|
|
104
|
+
- lib/async/service/managed_service.rb
|
|
104
105
|
- lib/async/service/version.rb
|
|
105
106
|
- license.md
|
|
106
107
|
- readme.md
|
|
@@ -125,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
125
126
|
- !ruby/object:Gem::Version
|
|
126
127
|
version: '0'
|
|
127
128
|
requirements: []
|
|
128
|
-
rubygems_version:
|
|
129
|
+
rubygems_version: 4.0.3
|
|
129
130
|
specification_version: 4
|
|
130
131
|
summary: A service layer for Async.
|
|
131
132
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2025, 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 health check. Set to `nil` to disable the health check.
|
|
21
|
-
#
|
|
22
|
-
# @returns [Numeric | nil] The health check timeout in seconds.
|
|
23
|
-
def health_check_timeout
|
|
24
|
-
30
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Options to use when creating the container, including `restart`, `count`, and `health_check_timeout`.
|
|
28
|
-
#
|
|
29
|
-
# @returns [Hash] The options for the container.
|
|
30
|
-
def container_options
|
|
31
|
-
{
|
|
32
|
-
restart: true,
|
|
33
|
-
count: self.count,
|
|
34
|
-
health_check_timeout: self.health_check_timeout,
|
|
35
|
-
}.compact
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Any scripts to preload before starting the service.
|
|
39
|
-
#
|
|
40
|
-
# @returns [Array(String)] The list of scripts to preload.
|
|
41
|
-
def preload
|
|
42
|
-
[]
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2025, 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.ready!
|
|
33
|
-
end.resume
|
|
34
|
-
|
|
35
|
-
sleep(timeout / 2)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
else
|
|
39
|
-
if block_given?
|
|
40
|
-
yield(instance)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
instance.ready!
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2025, by Samuel Williams.
|
|
5
|
-
|
|
6
|
-
require_relative "../generic"
|
|
7
|
-
require_relative "../formatting"
|
|
8
|
-
require_relative "health_checker"
|
|
9
|
-
|
|
10
|
-
module Async
|
|
11
|
-
module Service
|
|
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 Formatting
|
|
18
|
-
include HealthChecker
|
|
19
|
-
|
|
20
|
-
private def format_title(evaluator, server)
|
|
21
|
-
"#{evaluator.name} #{server.to_s}"
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Run the service logic.
|
|
25
|
-
#
|
|
26
|
-
# 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.
|
|
27
|
-
#
|
|
28
|
-
# @parameter instance [Object] The container instance.
|
|
29
|
-
# @parameter evaluator [Environment::Evaluator] The environment evaluator.
|
|
30
|
-
# @returns [Object] The service object (server, task, etc.)
|
|
31
|
-
def run(instance, evaluator)
|
|
32
|
-
Async do
|
|
33
|
-
sleep
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Preload any resources specified by the environment.
|
|
38
|
-
def preload!
|
|
39
|
-
if scripts = @evaluator.preload
|
|
40
|
-
root = @evaluator.root
|
|
41
|
-
scripts = Array(scripts)
|
|
42
|
-
|
|
43
|
-
scripts.each do |path|
|
|
44
|
-
Console.info(self){"Preloading #{path}..."}
|
|
45
|
-
full_path = File.expand_path(path, root)
|
|
46
|
-
require(full_path)
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
rescue StandardError, LoadError => error
|
|
50
|
-
Console.warn(self, "Service preload failed!", error)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Start the service, including preloading resources.
|
|
54
|
-
def start
|
|
55
|
-
preload!
|
|
56
|
-
|
|
57
|
-
super
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Set up the container with health checking and process title formatting.
|
|
61
|
-
# @parameter container [Async::Container] The container to configure.
|
|
62
|
-
def setup(container)
|
|
63
|
-
super
|
|
64
|
-
|
|
65
|
-
container_options = @evaluator.container_options
|
|
66
|
-
health_check_timeout = container_options[:health_check_timeout]
|
|
67
|
-
|
|
68
|
-
container.run(**container_options) do |instance|
|
|
69
|
-
Async do
|
|
70
|
-
evaluator = self.environment.evaluator
|
|
71
|
-
|
|
72
|
-
server = run(instance, evaluator)
|
|
73
|
-
|
|
74
|
-
health_checker(instance) do
|
|
75
|
-
instance.name = format_title(evaluator, server)
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|