async-service 0.12.0 → 0.14.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/agent.md +129 -0
- data/bin/async-service +2 -1
- data/context/best-practices.md +320 -0
- data/context/getting-started.md +190 -0
- data/context/index.yaml +20 -0
- data/context/service-architecture.md +499 -0
- data/lib/async/service/configuration.rb +30 -7
- data/lib/async/service/container_environment.rb +42 -0
- data/lib/async/service/container_service.rb +77 -0
- data/lib/async/service/controller.rb +23 -3
- data/lib/async/service/environment.rb +63 -1
- data/lib/async/service/formatting.rb +84 -0
- data/lib/async/service/generic.rb +49 -7
- data/lib/async/service/loader.rb +12 -7
- data/lib/async/service/version.rb +2 -2
- data/lib/async/service.rb +11 -4
- data/license.md +1 -1
- data/readme.md +71 -4
- data/releases.md +85 -0
- data.tar.gz.sig +0 -0
- metadata +14 -9
- metadata.gz.sig +0 -0
@@ -0,0 +1,499 @@
|
|
1
|
+
# Service Architecture
|
2
|
+
|
3
|
+
This guide explains the key architectural components of `async-service` and how they work together to provide a clean separation of concerns.
|
4
|
+
|
5
|
+
## Core Components
|
6
|
+
|
7
|
+
`Async::Service` is built around four main concepts:
|
8
|
+
|
9
|
+
- **Environment**: Represents a service configuration and provides lazy evaluation of settings.
|
10
|
+
- **Service**: Represents a specific service implementation, defined by an environment.
|
11
|
+
- **Configuration**: Represents one or more configured services, defined by environments.
|
12
|
+
- **Container**: Used to run the actual service logic in child processes or threads.
|
13
|
+
|
14
|
+
## Environment
|
15
|
+
|
16
|
+
The {ruby Async::Service::Environment} represents a lazy-evaluated chain of key-value pairs. It handles:
|
17
|
+
|
18
|
+
### Configuration Storage
|
19
|
+
|
20
|
+
Each service definition creates an environment:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
service "web" do
|
24
|
+
port 3000
|
25
|
+
host "localhost"
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
### Lazy Evaluation
|
30
|
+
|
31
|
+
Environments use lazy evaluation through evaluators:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
service "web" do
|
35
|
+
root "/app"
|
36
|
+
|
37
|
+
# Computed when accessed:
|
38
|
+
log_path {File.join(root, "logs", "app.log")}
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
The `log_path` is calculated when the service accesses it, not when defined.
|
43
|
+
|
44
|
+
### Composition and Inheritance
|
45
|
+
|
46
|
+
Environments support modular configuration:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
module DatabaseEnvironment
|
50
|
+
def database_url
|
51
|
+
"postgresql://localhost/app"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
service "web" do
|
56
|
+
# Environment includes the module:
|
57
|
+
include DatabaseEnvironment
|
58
|
+
|
59
|
+
database {Database.new(database_url)}
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
You can also explicitly create environments:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
SharedEnvironment = environment do
|
67
|
+
port 3000
|
68
|
+
host "localhost"
|
69
|
+
url {"http://#{host}:#{port}"}
|
70
|
+
end
|
71
|
+
|
72
|
+
service "web1" do
|
73
|
+
include SharedEnvironment
|
74
|
+
port 3001
|
75
|
+
end
|
76
|
+
|
77
|
+
service "web2" do
|
78
|
+
include SharedEnvironment
|
79
|
+
port 3002
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
## Service
|
84
|
+
|
85
|
+
The {ruby Async::Service::Generic} represents the service implementation layer. It handles:
|
86
|
+
|
87
|
+
### Business Logic
|
88
|
+
|
89
|
+
Services contain the actual implementation of what your service does:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
class WebService < Async::Service::Generic
|
93
|
+
def setup(container)
|
94
|
+
super
|
95
|
+
|
96
|
+
# Define how the service runs:
|
97
|
+
container.run(count: 1, restart: true) do |instance|
|
98
|
+
# Access configuration through evaluator:
|
99
|
+
evaluator = self.environment.evaluator
|
100
|
+
|
101
|
+
# Indicate that the service instance is healthy thus far:
|
102
|
+
instance.ready!
|
103
|
+
|
104
|
+
# Your service implementation:
|
105
|
+
start_web_server(evaluator.host, evaluator.port)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
### Configuration Access
|
112
|
+
|
113
|
+
Services access their configuration through their environment's evaluator:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
def setup(container)
|
117
|
+
super
|
118
|
+
|
119
|
+
container.run do |instance|
|
120
|
+
# Create a new evaluator as we are in a different execution context:
|
121
|
+
evaluator = self.environment.evaluator
|
122
|
+
|
123
|
+
# Use evaluator in service configuration:
|
124
|
+
database_url = evaluator.database_url
|
125
|
+
max_connections = evaluator.max_connections
|
126
|
+
|
127
|
+
# Your service implementation.
|
128
|
+
end
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
Within the service class itself, you can use `@evaluator`. However, evaluators are not thread safe, so you should not use them across threads, and instead create distinct evaluators for each child process, thread or ractor, etc.
|
133
|
+
|
134
|
+
#### Lazy Evaluation
|
135
|
+
|
136
|
+
Environment evaluators are lazy-evaluated and memoized, meaning that any configuration defined within them is not computed until it is accessed. By doing this in the scope of the child process, you can ensure that each instance has its own configuration values.
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
service do
|
140
|
+
log_path {File.join(root, "logs", "app-#{Process.pid}.log")}
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
By evaluating the log_path in the child process, you ensure that each instance has its own log file with the correct process ID.
|
145
|
+
|
146
|
+
### Lifecycle Management
|
147
|
+
|
148
|
+
Services define their startup, running, and shutdown behavior:
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
class MyService < Async::Service::Generic
|
152
|
+
def start
|
153
|
+
super
|
154
|
+
# Service-specific startup logic including pre-loading libraries and binding to network interfaces before forking.
|
155
|
+
end
|
156
|
+
|
157
|
+
def setup(container)
|
158
|
+
super
|
159
|
+
|
160
|
+
# Define container execution:
|
161
|
+
container.run do |instance|
|
162
|
+
# Your service implementation.
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def stop(graceful = true)
|
167
|
+
# Service-specific cleanup including releasing any resources acquired during startup.
|
168
|
+
super
|
169
|
+
end
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
## Configuration
|
174
|
+
|
175
|
+
The {ruby Async::Service::Configuration} represents the top-level orchestration layer. It handles:
|
176
|
+
|
177
|
+
### Service Definitions
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
configuration = Async::Service::Configuration.build do
|
181
|
+
service "web" do
|
182
|
+
service_class WebService
|
183
|
+
port 3000
|
184
|
+
host "localhost"
|
185
|
+
end
|
186
|
+
|
187
|
+
service "worker" do
|
188
|
+
service_class WorkerService
|
189
|
+
worker_count 4
|
190
|
+
end
|
191
|
+
end
|
192
|
+
```
|
193
|
+
|
194
|
+
### Service Discovery and Management
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
# Access all services in the configuration:
|
198
|
+
configuration.services.each do |service|
|
199
|
+
puts "Service: #{service.name}"
|
200
|
+
end
|
201
|
+
|
202
|
+
# Create a controller to manage all services:
|
203
|
+
controller = configuration.controller
|
204
|
+
|
205
|
+
# Start all services:
|
206
|
+
controller.start
|
207
|
+
```
|
208
|
+
|
209
|
+
### Loading from Files
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
# Load services from configuration files:
|
213
|
+
configuration = Async::Service::Configuration.load(["web.rb", "worker.rb"])
|
214
|
+
|
215
|
+
# Or load from command line arguments:
|
216
|
+
configuration = Async::Service::Configuration.load(ARGV)
|
217
|
+
```
|
218
|
+
|
219
|
+
### Introspection
|
220
|
+
|
221
|
+
Given a `service.rb` file, you can list the defined services and their configurations.
|
222
|
+
|
223
|
+
```bash
|
224
|
+
$ bundle exec bake async:service:configuration:load service.rb async:service:configuration:list o
|
225
|
+
utput --format json
|
226
|
+
[
|
227
|
+
{
|
228
|
+
"port": 3000,
|
229
|
+
"name": "web",
|
230
|
+
"host": "localhost",
|
231
|
+
"service_class": "WebService",
|
232
|
+
"root": "/Users/samuel/Developer/socketry/async-service"
|
233
|
+
},
|
234
|
+
{
|
235
|
+
"name": "worker",
|
236
|
+
"worker_count": 4,
|
237
|
+
"service_class": "WorkerService",
|
238
|
+
"root": "/Users/samuel/Developer/socketry/async-service"
|
239
|
+
}
|
240
|
+
]
|
241
|
+
```
|
242
|
+
|
243
|
+
Note that only `service do ... end` definitions are included in the configuration listing.
|
244
|
+
|
245
|
+
## Container
|
246
|
+
|
247
|
+
The container layer (provided by the `async-container` gem) handles process management. Services don't interact with containers directly, but configure how containers should run them.
|
248
|
+
|
249
|
+
### Process Management
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
def setup(container)
|
253
|
+
# Configure how many instances, restart behavior, etc.
|
254
|
+
container.run(count: 4, restart: true) do |instance|
|
255
|
+
# This block runs in a separate process/fiber
|
256
|
+
instance.ready!
|
257
|
+
|
258
|
+
# Your service implementation.
|
259
|
+
end
|
260
|
+
end
|
261
|
+
```
|
262
|
+
|
263
|
+
### Health Checking
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
def setup(container)
|
267
|
+
container_options = @evaluator.container_options
|
268
|
+
health_check_timeout = container_options[:health_check_timeout]
|
269
|
+
|
270
|
+
container.run(**container_options) do |instance|
|
271
|
+
# Prepare your service.
|
272
|
+
|
273
|
+
Sync do
|
274
|
+
# Start your service.
|
275
|
+
|
276
|
+
# Set up health checking, if a timeout was specified:
|
277
|
+
health_checker(instance, health_check_timeout) do
|
278
|
+
instance.name = "#{self.name}: #{current_status}"
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
```
|
284
|
+
|
285
|
+
## How They Work Together
|
286
|
+
|
287
|
+
The four layers interact in a specific pattern:
|
288
|
+
|
289
|
+
### 1. Environment Creation
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
# Environments define individual service configuration:
|
293
|
+
module DatabaseEnvironment
|
294
|
+
def database_url
|
295
|
+
"postgresql://localhost/app"
|
296
|
+
end
|
297
|
+
end
|
298
|
+
```
|
299
|
+
|
300
|
+
### 2. Service Definition
|
301
|
+
|
302
|
+
```ruby
|
303
|
+
# Services are defined using environments:
|
304
|
+
class WebService < Async::Service::Generic
|
305
|
+
def setup(container)
|
306
|
+
super
|
307
|
+
|
308
|
+
# Access configuration through evaluator
|
309
|
+
evaluator = @environment.evaluator
|
310
|
+
port = evaluator.port
|
311
|
+
|
312
|
+
# Define how the service runs
|
313
|
+
container.run(count: 1, restart: true) do |instance|
|
314
|
+
instance.ready!
|
315
|
+
|
316
|
+
# Your service implementation:
|
317
|
+
start_web_server(port)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
```
|
322
|
+
|
323
|
+
### 3. Configuration Assembly
|
324
|
+
|
325
|
+
```ruby
|
326
|
+
#!/usr/bin/env async-service
|
327
|
+
|
328
|
+
require "database_environment"
|
329
|
+
require "web_service"
|
330
|
+
|
331
|
+
service "web" do
|
332
|
+
include DatabaseEnvironment # Use the environment
|
333
|
+
service_class WebService
|
334
|
+
port 3000
|
335
|
+
end
|
336
|
+
```
|
337
|
+
|
338
|
+
### 4. Container Execution
|
339
|
+
|
340
|
+
```ruby
|
341
|
+
# Load the service configurations:
|
342
|
+
configuration = Async::Service::Configuration.load("service.rb")
|
343
|
+
|
344
|
+
# Controller manages the actual execution using containers:
|
345
|
+
Async::Service::Controller.run(configuration)
|
346
|
+
```
|
347
|
+
|
348
|
+
## Benefits of This Architecture
|
349
|
+
|
350
|
+
### Separation of Concerns
|
351
|
+
|
352
|
+
- **Environment**: "How should individual service configuration be stored and evaluated?"
|
353
|
+
- **Service**: "What should each service do when it runs?"
|
354
|
+
- **Configuration**: "What services should run and how should they be combined?"
|
355
|
+
- **Container**: "How should services be executed and monitored in processes?"
|
356
|
+
|
357
|
+
### Testability
|
358
|
+
|
359
|
+
```ruby
|
360
|
+
# Test configuration
|
361
|
+
configuration = Async::Service::Configuration.build do
|
362
|
+
service "test-service" do
|
363
|
+
service_class MyService
|
364
|
+
port 3001
|
365
|
+
database_url "sqlite://memory"
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# Test individual service
|
370
|
+
service = configuration.services.first
|
371
|
+
mock_container = double("container")
|
372
|
+
service.setup(mock_container)
|
373
|
+
```
|
374
|
+
|
375
|
+
### Flexibility
|
376
|
+
|
377
|
+
- Define multiple services in one configuration
|
378
|
+
- Same service class with different configurations
|
379
|
+
- Load configurations from files or build programmatically
|
380
|
+
- Different container strategies per service
|
381
|
+
|
382
|
+
### Reusability
|
383
|
+
|
384
|
+
```ruby
|
385
|
+
# Reusable environment modules:
|
386
|
+
module DatabaseEnvironment
|
387
|
+
def database_url
|
388
|
+
"postgresql://localhost/app"
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
module RedisEnvironment
|
393
|
+
def redis_url
|
394
|
+
"redis://localhost:6379"
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# Compose in service definitions:
|
399
|
+
configuration = Async::Service::Configuration.build do
|
400
|
+
service "worker" do
|
401
|
+
include DatabaseEnvironment
|
402
|
+
include RedisEnvironment
|
403
|
+
|
404
|
+
service_class WorkerService
|
405
|
+
end
|
406
|
+
end
|
407
|
+
```
|
408
|
+
|
409
|
+
## Common Patterns
|
410
|
+
|
411
|
+
### Configuration Files
|
412
|
+
|
413
|
+
Create reusable configuration files:
|
414
|
+
|
415
|
+
```ruby
|
416
|
+
# config/web.rb
|
417
|
+
service "web" do
|
418
|
+
service_class WebService
|
419
|
+
port 3000
|
420
|
+
host "0.0.0.0"
|
421
|
+
end
|
422
|
+
|
423
|
+
# config/worker.rb
|
424
|
+
service "worker" do
|
425
|
+
service_class WorkerService
|
426
|
+
concurrency 4
|
427
|
+
end
|
428
|
+
|
429
|
+
# Load both:
|
430
|
+
configuration = Async::Service::Configuration.load(["config/web.rb", "config/worker.rb"])
|
431
|
+
```
|
432
|
+
|
433
|
+
### Environment Modules
|
434
|
+
|
435
|
+
Create reusable configuration modules:
|
436
|
+
|
437
|
+
```ruby
|
438
|
+
module ContainerEnvironment
|
439
|
+
def count
|
440
|
+
4
|
441
|
+
end
|
442
|
+
|
443
|
+
def restart
|
444
|
+
true
|
445
|
+
end
|
446
|
+
|
447
|
+
def health_check_timeout
|
448
|
+
30
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
configuration = Async::Service::Configuration.build do
|
453
|
+
service "my-service" do
|
454
|
+
include ContainerEnvironment
|
455
|
+
service_class MyService
|
456
|
+
end
|
457
|
+
end
|
458
|
+
```
|
459
|
+
|
460
|
+
### Service Inheritance
|
461
|
+
|
462
|
+
Build service hierarchies:
|
463
|
+
|
464
|
+
```ruby
|
465
|
+
class BaseWebService < Async::Service::Generic
|
466
|
+
def setup(container)
|
467
|
+
super
|
468
|
+
# Common web service setup
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
class APIService < BaseWebService
|
473
|
+
def setup(container)
|
474
|
+
super
|
475
|
+
# API-specific setup
|
476
|
+
end
|
477
|
+
end
|
478
|
+
```
|
479
|
+
|
480
|
+
### Programmatic Configuration
|
481
|
+
|
482
|
+
Build configurations dynamically:
|
483
|
+
|
484
|
+
```ruby
|
485
|
+
configuration = Async::Service::Configuration.new
|
486
|
+
|
487
|
+
# Add services programmatically
|
488
|
+
["web", "api", "worker"].each do |name|
|
489
|
+
environment = Async::Service::Environment.build do
|
490
|
+
service_class MyService
|
491
|
+
service_name name
|
492
|
+
port 3000 + name.hash % 1000
|
493
|
+
end
|
494
|
+
|
495
|
+
configuration.add(environment)
|
496
|
+
end
|
497
|
+
```
|
498
|
+
|
499
|
+
This architecture provides a clean, testable, and flexible foundation for building services while maintaining clear boundaries between configuration, implementation, and execution concerns.
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2024, by Samuel Williams.
|
4
|
+
# Copyright, 2024-2025, by Samuel Williams.
|
5
5
|
|
6
|
-
require_relative
|
7
|
-
require_relative
|
8
|
-
require_relative
|
6
|
+
require_relative "loader"
|
7
|
+
require_relative "generic"
|
8
|
+
require_relative "controller"
|
9
9
|
|
10
10
|
module Async
|
11
11
|
module Service
|
@@ -13,6 +13,10 @@ module Async
|
|
13
13
|
#
|
14
14
|
# Environments are key-value maps with lazy value resolution. An environment can inherit from a parent environment, which can provide defaults
|
15
15
|
class Configuration
|
16
|
+
# Build a configuration using a block.
|
17
|
+
# @parameter root [String] The root directory for loading files.
|
18
|
+
# @yields {|loader| ...} A loader instance for configuration.
|
19
|
+
# @returns [Configuration] A new configuration instance.
|
16
20
|
def self.build(root: Dir.pwd, &block)
|
17
21
|
configuration = self.new
|
18
22
|
|
@@ -22,6 +26,9 @@ module Async
|
|
22
26
|
return configuration
|
23
27
|
end
|
24
28
|
|
29
|
+
# Load configuration from file paths.
|
30
|
+
# @parameter paths [Array(String)] File paths to load, defaults to `ARGV`.
|
31
|
+
# @returns [Configuration] A new configuration instance.
|
25
32
|
def self.load(paths = ARGV)
|
26
33
|
configuration = self.new
|
27
34
|
|
@@ -32,6 +39,9 @@ module Async
|
|
32
39
|
return configuration
|
33
40
|
end
|
34
41
|
|
42
|
+
# Create configuration from environments.
|
43
|
+
# @parameter environments [Array] Environment instances.
|
44
|
+
# @returns [Configuration] A new configuration instance.
|
35
45
|
def self.for(*environments)
|
36
46
|
self.new(environments)
|
37
47
|
end
|
@@ -43,20 +53,33 @@ module Async
|
|
43
53
|
|
44
54
|
attr :environments
|
45
55
|
|
56
|
+
# Check if the configuration is empty.
|
57
|
+
# @returns [Boolean] True if no environments are configured.
|
46
58
|
def empty?
|
47
59
|
@environments.empty?
|
48
60
|
end
|
49
61
|
|
62
|
+
# Enumerate all services in the configuration.
|
63
|
+
#
|
64
|
+
# A service is an environment that has a `service_class` key.
|
65
|
+
#
|
66
|
+
# @parameter implementing [Module] If specified, only services implementing this module will be returned/yielded.
|
67
|
+
# @yields {|service| ...} Each service in the configuration.
|
50
68
|
def services(implementing: nil)
|
51
69
|
return to_enum(:services, implementing: implementing) unless block_given?
|
52
70
|
|
53
71
|
@environments.each do |environment|
|
54
|
-
|
55
|
-
|
56
|
-
|
72
|
+
if implementing.nil? or environment.implements?(implementing)
|
73
|
+
if service = Generic.wrap(environment)
|
74
|
+
yield service
|
75
|
+
end
|
76
|
+
end
|
57
77
|
end
|
58
78
|
end
|
59
79
|
|
80
|
+
# Create a controller for the configured services.
|
81
|
+
#
|
82
|
+
# @returns [Controller] A controller that can be used to start/stop services.
|
60
83
|
def controller(**options)
|
61
84
|
Controller.new(self.services(**options).to_a)
|
62
85
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Async
|
4
|
+
module Service
|
5
|
+
# Default configuration for container-based services.
|
6
|
+
#
|
7
|
+
# 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.
|
8
|
+
module ContainerEnvironment
|
9
|
+
# Number of instances to start. By default, when `nil`, uses `Etc.nprocessors`.
|
10
|
+
#
|
11
|
+
# @returns [Integer | nil] The number of instances to start, or `nil` to use the default.
|
12
|
+
def count
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# The timeout duration for the health check. Set to `nil` to disable the health check.
|
17
|
+
#
|
18
|
+
# @returns [Numeric | nil] The health check timeout in seconds.
|
19
|
+
def health_check_timeout
|
20
|
+
30
|
21
|
+
end
|
22
|
+
|
23
|
+
# Options to use when creating the container, including `restart`, `count`, and `health_check_timeout`.
|
24
|
+
#
|
25
|
+
# @returns [Hash] The options for the container.
|
26
|
+
def container_options
|
27
|
+
{
|
28
|
+
restart: true,
|
29
|
+
count: self.count,
|
30
|
+
health_check_timeout: self.health_check_timeout,
|
31
|
+
}.compact
|
32
|
+
end
|
33
|
+
|
34
|
+
# Any scripts to preload before starting the server.
|
35
|
+
#
|
36
|
+
# @returns [Array(String)] The list of scripts to preload.
|
37
|
+
def preload
|
38
|
+
[]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,77 @@
|
|
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
|