async-service 0.14.3 → 0.15.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 +21 -19
- data/context/getting-started.md +3 -3
- data/context/service-architecture.md +10 -8
- data/lib/async/service/environment.rb +1 -9
- data/lib/async/service/formatting.rb +11 -38
- data/lib/async/service/generic.rb +3 -37
- data/lib/async/service/loader.rb +2 -0
- data/lib/async/service/managed/environment.rb +48 -0
- data/lib/async/service/managed/health_checker.rb +49 -0
- data/lib/async/service/managed/service.rb +82 -0
- data/lib/async/service/managed.rb +17 -0
- data/lib/async/service/version.rb +1 -1
- data/readme.md +9 -8
- data/releases.md +9 -0
- data.tar.gz.sig +0 -0
- metadata +19 -3
- metadata.gz.sig +0 -0
- data/lib/async/service/container_environment.rb +0 -45
- data/lib/async/service/container_service.rb +0 -77
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 13ead1ca63b6d4843b39153b021809f8b521ca19a47ecec2772b567076877d49
|
|
4
|
+
data.tar.gz: 26cf9ffae20270a56937b077b8c852d86e67cb0c31f4b75486cc8bfca256282a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d2f8fa1f3b208ab5bc3804daf82a3e7c7c5f598950d60d50dd9d39acad4956ac4c143d76a7c6218e6b29bc3e317b954727916bdc8806d117e85885dd3e6c07b0
|
|
7
|
+
data.tar.gz: f8200b507889e9beb20a46f5f3c857ecab74ab74563a1e53264acb13a033426c8c02fae4dc5b16cc959fbf3d6bc5a3942c19fa606d0245d2c437a7211890f695
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/context/best-practices.md
CHANGED
|
@@ -14,8 +14,8 @@ Create a single top-level `service.rb` file as your main entry point:
|
|
|
14
14
|
#!/usr/bin/env async-service
|
|
15
15
|
|
|
16
16
|
# Load your service configurations
|
|
17
|
-
require_relative
|
|
18
|
-
require_relative
|
|
17
|
+
require_relative "lib/my_library/environment/web_environment"
|
|
18
|
+
require_relative "lib/my_library/environment/worker_environment"
|
|
19
19
|
|
|
20
20
|
service "web" do
|
|
21
21
|
include MyLibrary::Environment::WebEnvironment
|
|
@@ -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)"
|
|
@@ -98,7 +98,7 @@ module MyLibrary
|
|
|
98
98
|
|
|
99
99
|
def run(instance, evaluator)
|
|
100
100
|
# Start your service and return the server object.
|
|
101
|
-
#
|
|
101
|
+
# Managed::Service handles container setup, health checking, and process titles.
|
|
102
102
|
start_web_server(evaluator.host, evaluator.port)
|
|
103
103
|
end
|
|
104
104
|
|
|
@@ -112,13 +112,13 @@ module MyLibrary
|
|
|
112
112
|
end
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
### Use `
|
|
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
|
|
121
|
-
include Async::Service::
|
|
121
|
+
include Async::Service::Managed::Environment
|
|
122
122
|
|
|
123
123
|
def service_class
|
|
124
124
|
WebService
|
|
@@ -159,9 +159,9 @@ module WebEnvironment
|
|
|
159
159
|
def port
|
|
160
160
|
3000
|
|
161
161
|
end
|
|
162
|
-
|
|
162
|
+
|
|
163
163
|
def host
|
|
164
|
-
|
|
164
|
+
"0.0.0.0"
|
|
165
165
|
end
|
|
166
166
|
end
|
|
167
167
|
|
|
@@ -172,7 +172,7 @@ module WorkerEnvironment
|
|
|
172
172
|
end
|
|
173
173
|
|
|
174
174
|
def queue_name
|
|
175
|
-
|
|
175
|
+
"default"
|
|
176
176
|
end
|
|
177
177
|
|
|
178
178
|
def count
|
|
@@ -199,16 +199,16 @@ end
|
|
|
199
199
|
|
|
200
200
|
## Service Best Practices
|
|
201
201
|
|
|
202
|
-
### Use
|
|
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
|
-
class WebService < Async::Service::
|
|
208
|
-
#
|
|
207
|
+
class WebService < Async::Service::Managed::Service
|
|
208
|
+
# Managed::Service automatically handles:
|
|
209
209
|
# - Container setup with proper options.
|
|
210
210
|
# - Health checking with process title updates.
|
|
211
|
-
# -
|
|
211
|
+
# - Preloading of scripts before startup.
|
|
212
212
|
|
|
213
213
|
private def format_title(evaluator, server)
|
|
214
214
|
# Customize process title display
|
|
@@ -247,20 +247,22 @@ Try to keep process titles short and focused.
|
|
|
247
247
|
Utilize the `start` and `stop` hooks to manage shared resources effectively:
|
|
248
248
|
|
|
249
249
|
```ruby
|
|
250
|
-
class WebService < Async::Service::
|
|
250
|
+
class WebService < Async::Service::Managed::Service
|
|
251
251
|
def start
|
|
252
252
|
# Bind to the endpoint in the container:
|
|
253
253
|
@endpoint = @evaluator.endpoint.bind
|
|
254
254
|
|
|
255
255
|
super
|
|
256
256
|
end
|
|
257
|
-
|
|
257
|
+
|
|
258
258
|
def stop
|
|
259
259
|
@endpoint&.close
|
|
260
260
|
end
|
|
261
261
|
end
|
|
262
262
|
```
|
|
263
263
|
|
|
264
|
+
These hooks are invoked **before** the container is setup (e.g. pre-forking).
|
|
265
|
+
|
|
264
266
|
## Testing Best Practices
|
|
265
267
|
|
|
266
268
|
### Test Environments in Isolation
|
|
@@ -302,7 +304,7 @@ describe MyLibrary::Service::WebService do
|
|
|
302
304
|
before do
|
|
303
305
|
controller.start
|
|
304
306
|
end
|
|
305
|
-
|
|
307
|
+
|
|
306
308
|
after do
|
|
307
309
|
controller.stop
|
|
308
310
|
end
|
data/context/getting-started.md
CHANGED
|
@@ -29,7 +29,7 @@ Create a simple service that runs continuously:
|
|
|
29
29
|
```ruby
|
|
30
30
|
#!/usr/bin/env async-service
|
|
31
31
|
|
|
32
|
-
require
|
|
32
|
+
require "async/service"
|
|
33
33
|
|
|
34
34
|
class HelloService < Async::Service::Generic
|
|
35
35
|
def setup(container)
|
|
@@ -99,7 +99,7 @@ You can define multiple services in a single configuration file:
|
|
|
99
99
|
```ruby
|
|
100
100
|
#!/usr/bin/env async-service
|
|
101
101
|
|
|
102
|
-
require
|
|
102
|
+
require "async/service"
|
|
103
103
|
|
|
104
104
|
class WebService < Async::Service::Generic
|
|
105
105
|
def setup(container)
|
|
@@ -141,7 +141,7 @@ end
|
|
|
141
141
|
You can also create and run services programmatically:
|
|
142
142
|
|
|
143
143
|
```ruby
|
|
144
|
-
require
|
|
144
|
+
require "async/service"
|
|
145
145
|
|
|
146
146
|
configuration = Async::Service::Configuration.build do
|
|
147
147
|
service "my-service" do
|
|
@@ -262,6 +262,8 @@ end
|
|
|
262
262
|
|
|
263
263
|
### Health Checking
|
|
264
264
|
|
|
265
|
+
For services using `Async::Service::Managed::Service`, health checking is handled automatically. For services extending `Generic`, you can set up health checking manually:
|
|
266
|
+
|
|
265
267
|
```ruby
|
|
266
268
|
def setup(container)
|
|
267
269
|
container_options = @evaluator.container_options
|
|
@@ -270,7 +272,7 @@ def setup(container)
|
|
|
270
272
|
container.run(**container_options) do |instance|
|
|
271
273
|
# Prepare your service.
|
|
272
274
|
|
|
273
|
-
|
|
275
|
+
Async do
|
|
274
276
|
# Start your service.
|
|
275
277
|
|
|
276
278
|
# Set up health checking, if a timeout was specified:
|
|
@@ -282,6 +284,8 @@ def setup(container)
|
|
|
282
284
|
end
|
|
283
285
|
```
|
|
284
286
|
|
|
287
|
+
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.
|
|
288
|
+
|
|
285
289
|
## How They Work Together
|
|
286
290
|
|
|
287
291
|
The four layers interact in a specific pattern:
|
|
@@ -435,15 +439,13 @@ configuration = Async::Service::Configuration.load(["config/web.rb", "config/wor
|
|
|
435
439
|
Create reusable configuration modules:
|
|
436
440
|
|
|
437
441
|
```ruby
|
|
438
|
-
module
|
|
442
|
+
module ManagedEnvironment
|
|
443
|
+
include Async::Service::Managed::Environment
|
|
444
|
+
|
|
439
445
|
def count
|
|
440
446
|
4
|
|
441
447
|
end
|
|
442
|
-
|
|
443
|
-
def restart
|
|
444
|
-
true
|
|
445
|
-
end
|
|
446
|
-
|
|
448
|
+
|
|
447
449
|
def health_check_timeout
|
|
448
450
|
30
|
|
449
451
|
end
|
|
@@ -451,7 +453,7 @@ end
|
|
|
451
453
|
|
|
452
454
|
configuration = Async::Service::Configuration.build do
|
|
453
455
|
service "my-service" do
|
|
454
|
-
include
|
|
456
|
+
include ManagedEnvironment
|
|
455
457
|
service_class MyService
|
|
456
458
|
end
|
|
457
459
|
end
|
|
@@ -84,14 +84,6 @@ module Async
|
|
|
84
84
|
@facet.define_method(name){argument}
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
|
-
|
|
88
|
-
# Always respond to missing methods for dynamic method definition.
|
|
89
|
-
# @parameter name [Symbol] The method name.
|
|
90
|
-
# @parameter include_private [Boolean] Whether to include private methods.
|
|
91
|
-
# @returns [Boolean] Always true to enable dynamic method definition.
|
|
92
|
-
def respond_to_missing?(name, include_private = false)
|
|
93
|
-
true
|
|
94
|
-
end
|
|
95
87
|
end
|
|
96
88
|
|
|
97
89
|
# Build a new environment using the builder DSL.
|
|
@@ -170,7 +162,7 @@ module Async
|
|
|
170
162
|
end
|
|
171
163
|
|
|
172
164
|
# This lists all zero-argument methods:
|
|
173
|
-
evaluator.define_method(:keys)
|
|
165
|
+
evaluator.define_method(:keys){keys}
|
|
174
166
|
|
|
175
167
|
return evaluator.new
|
|
176
168
|
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
+
require "string/format"
|
|
7
|
+
|
|
6
8
|
module Async
|
|
7
9
|
module Service
|
|
8
10
|
# Formatting utilities for service titles.
|
|
@@ -10,33 +12,15 @@ module Async
|
|
|
10
12
|
# Services need meaningful process/thread names for monitoring and debugging. This module provides consistent formatting for common service metrics like connection counts, request ratios, and load values in process titles.
|
|
11
13
|
#
|
|
12
14
|
# It is expected you will include these into your service class and use them to update the `instance.name` in the health check.
|
|
15
|
+
#
|
|
16
|
+
# @deprecated Use {String::Format} directly.
|
|
13
17
|
module Formatting
|
|
14
|
-
UNITS = [nil, "K", "M", "B", "T", "P", "E", "Z", "Y"]
|
|
15
|
-
|
|
16
18
|
# Format a count into a human-readable string.
|
|
17
19
|
# @parameter value [Numeric] The count to format.
|
|
18
|
-
# @parameter units [Array] The units to use for formatting (default: UNITS).
|
|
20
|
+
# @parameter units [Array] The units to use for formatting (default: String::Format::UNITS).
|
|
19
21
|
# @returns [String] A formatted string representing the count.
|
|
20
|
-
def format_count(value, units = UNITS)
|
|
21
|
-
value
|
|
22
|
-
index = 0
|
|
23
|
-
limit = units.size - 1
|
|
24
|
-
|
|
25
|
-
# Handle negative numbers by working with absolute value:
|
|
26
|
-
negative = value < 0
|
|
27
|
-
value = value.abs
|
|
28
|
-
|
|
29
|
-
while value >= 1000 and index < limit
|
|
30
|
-
value = value / 1000.0
|
|
31
|
-
index += 1
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
result = String.new
|
|
35
|
-
result << "-" if negative
|
|
36
|
-
result << value.round(2).to_s
|
|
37
|
-
result << units[index].to_s if units[index]
|
|
38
|
-
|
|
39
|
-
return result
|
|
22
|
+
def format_count(value, units = String::Format::UNITS)
|
|
23
|
+
String::Format.count(value, units)
|
|
40
24
|
end
|
|
41
25
|
|
|
42
26
|
module_function :format_count
|
|
@@ -46,7 +30,7 @@ module Async
|
|
|
46
30
|
# @parameter total [Numeric] The total value.
|
|
47
31
|
# @returns [String] A formatted ratio string.
|
|
48
32
|
def format_ratio(current, total)
|
|
49
|
-
|
|
33
|
+
String::Format.ratio(current, total)
|
|
50
34
|
end
|
|
51
35
|
|
|
52
36
|
module_function :format_ratio
|
|
@@ -55,27 +39,16 @@ module Async
|
|
|
55
39
|
# @parameter load [Numeric] The load value (typically 0.0 to 1.0+).
|
|
56
40
|
# @returns [String] A formatted load string.
|
|
57
41
|
def format_load(load)
|
|
58
|
-
|
|
42
|
+
String::Format.decimal(load)
|
|
59
43
|
end
|
|
60
44
|
|
|
61
45
|
module_function :format_load
|
|
62
46
|
|
|
63
47
|
# Format multiple statistics into a compact string.
|
|
64
|
-
# @parameter
|
|
48
|
+
# @parameter pairs [Hash] Hash of statistic names to values or [current, total] arrays.
|
|
65
49
|
# @returns [String] A formatted statistics string.
|
|
66
50
|
def format_statistics(**pairs)
|
|
67
|
-
pairs
|
|
68
|
-
case value
|
|
69
|
-
when Array
|
|
70
|
-
if value.length == 2
|
|
71
|
-
"#{key.to_s.upcase}=#{format_ratio(value[0], value[1])}"
|
|
72
|
-
else
|
|
73
|
-
"#{key.to_s.upcase}=#{value.join('/')}"
|
|
74
|
-
end
|
|
75
|
-
else
|
|
76
|
-
"#{key.to_s.upcase}=#{format_count(value)}"
|
|
77
|
-
end
|
|
78
|
-
end.join(" ")
|
|
51
|
+
String::Format.statistics(pairs)
|
|
79
52
|
end
|
|
80
53
|
|
|
81
54
|
module_function :format_statistics
|
|
@@ -47,52 +47,18 @@ module Async
|
|
|
47
47
|
|
|
48
48
|
# Start the service. Called before the container setup.
|
|
49
49
|
def start
|
|
50
|
-
Console.debug(self)
|
|
50
|
+
Console.debug(self){"Starting service #{self.name}..."}
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# Setup the service into the specified container.
|
|
54
54
|
# @parameter container [Async::Container::Generic]
|
|
55
55
|
def setup(container)
|
|
56
|
-
Console.debug(self)
|
|
56
|
+
Console.debug(self){"Setting up service #{self.name}..."}
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
# Stop the service. Called after the container is stopped.
|
|
60
60
|
def stop(graceful = true)
|
|
61
|
-
Console.debug(self)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
protected
|
|
65
|
-
|
|
66
|
-
# Start the health checker.
|
|
67
|
-
#
|
|
68
|
-
# 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).
|
|
69
|
-
#
|
|
70
|
-
# If a timeout is not specified, the health checker will yield the instance immediately and then mark the instance as ready.
|
|
71
|
-
#
|
|
72
|
-
# @parameter instance [Object] The service instance to check.
|
|
73
|
-
# @parameter timeout [Numeric] The timeout duration for the health check.
|
|
74
|
-
# @parameter parent [Async::Task] The parent task to run the health checker in.
|
|
75
|
-
# @yields {|instance| ...} If a block is given, it will be called with the service instance at least once.
|
|
76
|
-
def health_checker(instance, timeout = @evaluator.health_check_timeout, parent: Async::Task.current, &block)
|
|
77
|
-
if timeout
|
|
78
|
-
parent.async(transient: true) do
|
|
79
|
-
while true
|
|
80
|
-
if block_given?
|
|
81
|
-
yield(instance)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
instance.ready!
|
|
85
|
-
|
|
86
|
-
sleep(timeout / 2)
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
else
|
|
90
|
-
if block_given?
|
|
91
|
-
yield(instance)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
instance.ready!
|
|
95
|
-
end
|
|
61
|
+
Console.debug(self){"Stopping service #{self.name}..."}
|
|
96
62
|
end
|
|
97
63
|
end
|
|
98
64
|
end
|
data/lib/async/service/loader.rb
CHANGED
|
@@ -38,6 +38,8 @@ module Async
|
|
|
38
38
|
loader.instance_eval(File.read(path), path)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# Load a configuration file relative to the loader's root path.
|
|
42
|
+
# @parameter path [String] The path to the configuration file, relative to the loader's root.
|
|
41
43
|
def load_file(path)
|
|
42
44
|
Loader.load_file(@configuration, File.expand_path(path, @root))
|
|
43
45
|
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
|
+
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
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
evaluator = self.environment.evaluator
|
|
70
|
+
|
|
71
|
+
server = run(instance, evaluator)
|
|
72
|
+
|
|
73
|
+
health_checker(instance) do
|
|
74
|
+
instance.name = format_title(evaluator, server)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "managed/environment"
|
|
7
|
+
require_relative "managed/service"
|
|
8
|
+
|
|
9
|
+
module Async
|
|
10
|
+
module Service
|
|
11
|
+
# Managed services provide robust lifecycle management including health checking, restart policies, and process title formatting.
|
|
12
|
+
#
|
|
13
|
+
# This module contains components for building managed services that can run multiple instances with automatic restart and health monitoring.
|
|
14
|
+
module Managed
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/readme.md
CHANGED
|
@@ -27,6 +27,15 @@ Please see the [project documentation](https://socketry.github.io/async-service/
|
|
|
27
27
|
|
|
28
28
|
Please see the [project releases](https://socketry.github.io/async-service/releases/index) for all releases.
|
|
29
29
|
|
|
30
|
+
### v0.15.0
|
|
31
|
+
|
|
32
|
+
- Rename `ContainerEnvironment` and `ContainerService` to `Managed::Environment` and `Managed::Service` respectively.
|
|
33
|
+
- Health check uses `Fiber.new{instance.ready!}.resume` to confirm fiber allocation is working.
|
|
34
|
+
|
|
35
|
+
### v0.14.4
|
|
36
|
+
|
|
37
|
+
- Use `String::Format` gem for formatting.
|
|
38
|
+
|
|
30
39
|
### v0.14.0
|
|
31
40
|
|
|
32
41
|
- Introduce `ContainerEnvironment` and `ContainerService` for implementing best-practice services.
|
|
@@ -63,14 +72,6 @@ Please see the [project releases](https://socketry.github.io/async-service/relea
|
|
|
63
72
|
|
|
64
73
|
- Allow instance methods that take arguments in environments.
|
|
65
74
|
|
|
66
|
-
### v0.6.1
|
|
67
|
-
|
|
68
|
-
- Fix requirement that facet must be a module.
|
|
69
|
-
|
|
70
|
-
### v0.6.0
|
|
71
|
-
|
|
72
|
-
- Unify construction of environments for better consistency.
|
|
73
|
-
|
|
74
75
|
## Contributing
|
|
75
76
|
|
|
76
77
|
We welcome contributions to this project.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.15.0
|
|
4
|
+
|
|
5
|
+
- Rename `ContainerEnvironment` and `ContainerService` to `Managed::Environment` and `Managed::Service` respectively.
|
|
6
|
+
- Health check uses `Fiber.new{instance.ready!}.resume` to confirm fiber allocation is working.
|
|
7
|
+
|
|
8
|
+
## v0.14.4
|
|
9
|
+
|
|
10
|
+
- Use `String::Format` gem for formatting.
|
|
11
|
+
|
|
3
12
|
## v0.14.0
|
|
4
13
|
|
|
5
14
|
- Introduce `ContainerEnvironment` and `ContainerService` for implementing best-practice services.
|
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.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -66,6 +66,20 @@ dependencies:
|
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '0.16'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: string-format
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0.2'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0.2'
|
|
69
83
|
executables:
|
|
70
84
|
- async-service
|
|
71
85
|
extensions: []
|
|
@@ -78,13 +92,15 @@ files:
|
|
|
78
92
|
- context/service-architecture.md
|
|
79
93
|
- lib/async/service.rb
|
|
80
94
|
- lib/async/service/configuration.rb
|
|
81
|
-
- lib/async/service/container_environment.rb
|
|
82
|
-
- lib/async/service/container_service.rb
|
|
83
95
|
- lib/async/service/controller.rb
|
|
84
96
|
- lib/async/service/environment.rb
|
|
85
97
|
- lib/async/service/formatting.rb
|
|
86
98
|
- lib/async/service/generic.rb
|
|
87
99
|
- lib/async/service/loader.rb
|
|
100
|
+
- lib/async/service/managed.rb
|
|
101
|
+
- lib/async/service/managed/environment.rb
|
|
102
|
+
- lib/async/service/managed/health_checker.rb
|
|
103
|
+
- lib/async/service/managed/service.rb
|
|
88
104
|
- lib/async/service/version.rb
|
|
89
105
|
- license.md
|
|
90
106
|
- readme.md
|
metadata.gz.sig
CHANGED
|
Binary file
|
|
@@ -1,45 +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
|
-
# Default configuration for container-based services.
|
|
9
|
-
#
|
|
10
|
-
# This is provided not because it is required, but to offer a sensible default for containerized applications, and to expose a consistent interface for service configuration.
|
|
11
|
-
module ContainerEnvironment
|
|
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 health check. Set to `nil` to disable the health check.
|
|
20
|
-
#
|
|
21
|
-
# @returns [Numeric | nil] The health check timeout in seconds.
|
|
22
|
-
def health_check_timeout
|
|
23
|
-
30
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Options to use when creating the container, including `restart`, `count`, and `health_check_timeout`.
|
|
27
|
-
#
|
|
28
|
-
# @returns [Hash] The options for the container.
|
|
29
|
-
def container_options
|
|
30
|
-
{
|
|
31
|
-
restart: true,
|
|
32
|
-
count: self.count,
|
|
33
|
-
health_check_timeout: self.health_check_timeout,
|
|
34
|
-
}.compact
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Any scripts to preload before starting the server.
|
|
38
|
-
#
|
|
39
|
-
# @returns [Array(String)] The list of scripts to preload.
|
|
40
|
-
def preload
|
|
41
|
-
[]
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,77 +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
|
-
|
|
9
|
-
module Async
|
|
10
|
-
module Service
|
|
11
|
-
# A service that runs in a container with built-in health checking and process title formatting.
|
|
12
|
-
#
|
|
13
|
-
# This is the recommended base class for most services.
|
|
14
|
-
class ContainerService < Async::Service::Generic
|
|
15
|
-
include Async::Service::Formatting
|
|
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 => 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
|
-
# Set up the container with health checking and process title formatting.
|
|
58
|
-
# @parameter container [Async::Container] The container to configure.
|
|
59
|
-
def setup(container)
|
|
60
|
-
super
|
|
61
|
-
|
|
62
|
-
container_options = @evaluator.container_options
|
|
63
|
-
health_check_timeout = container_options[:health_check_timeout]
|
|
64
|
-
|
|
65
|
-
container.run(**container_options) do |instance|
|
|
66
|
-
evaluator = self.environment.evaluator
|
|
67
|
-
|
|
68
|
-
server = run(instance, evaluator)
|
|
69
|
-
|
|
70
|
-
health_checker(instance) do
|
|
71
|
-
instance.name = format_title(evaluator, server)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|