async-service 0.15.1 → 0.16.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 +7 -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 +8 -8
- 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 +62 -0
- data/lib/async/service/managed_service.rb +94 -0
- data/lib/async/service/version.rb +1 -1
- data/readme.md +8 -4
- data/releases.md +6 -0
- data.tar.gz.sig +0 -0
- metadata +6 -5
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1dc20befccfdbe1d36db11898e11a469ff64f2338a7b88942d59cb58e0e8c92f
|
|
4
|
+
data.tar.gz: c2a09fad50e09a6188e30b213d921f832ac14706c8f23d721269e4e48cffe028
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef436a78425bfab299e187e37e81f6d442dd00ffbff6c3e526c94d5753395e03158219d3b92d7e3edec4a68babf277fc97448d2a18991b6ab1d79cfdd6cafb74
|
|
7
|
+
data.tar.gz: 59468d0b53c26be506a8c4195cae58c91627352ebf09555bdcbf539d166c66d97f4c6a9ad3969fd0e8251f857cc058e4ad69e322ece8cd585b0579c0fbc44d69
|
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::ManagedEnvironment
|
|
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::ManagedService
|
|
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,11 +114,11 @@ end
|
|
|
114
114
|
|
|
115
115
|
### Use `Managed::Environment` for Services
|
|
116
116
|
|
|
117
|
-
Include {ruby Async::Service::
|
|
117
|
+
Include {ruby Async::Service::ManagedEnvironment} for services that need robust lifecycle management using {ruby Async::Service::ManagedService}:
|
|
118
118
|
|
|
119
119
|
```ruby
|
|
120
120
|
module WebEnvironment
|
|
121
|
-
include Async::Service::
|
|
121
|
+
include Async::Service::ManagedEnvironment
|
|
122
122
|
|
|
123
123
|
def service_class
|
|
124
124
|
WebService
|
|
@@ -201,10 +201,10 @@ end
|
|
|
201
201
|
|
|
202
202
|
### Use `Managed::Service` as Base Class
|
|
203
203
|
|
|
204
|
-
Prefer `Async::Service::
|
|
204
|
+
Prefer `Async::Service::ManagedService` over `GenericService` for most services:
|
|
205
205
|
|
|
206
206
|
```ruby
|
|
207
|
-
class WebService < Async::Service::
|
|
207
|
+
class WebService < Async::Service::ManagedService
|
|
208
208
|
# Managed::Service automatically handles:
|
|
209
209
|
# - Container setup with proper options.
|
|
210
210
|
# - Health checking with process title updates.
|
|
@@ -247,7 +247,7 @@ 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::ManagedService
|
|
251
251
|
def start
|
|
252
252
|
# Bind to the endpoint in the container:
|
|
253
253
|
@endpoint = @evaluator.endpoint.bind
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Deployment
|
|
2
|
+
|
|
3
|
+
This guide explains how to deploy `async-service` applications using systemd and Kubernetes. We'll use a simple example service to demonstrate deployment configurations.
|
|
4
|
+
|
|
5
|
+
## Example Service
|
|
6
|
+
|
|
7
|
+
Let's start with a simple HTTP service that we'll deploy:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
#!/usr/bin/env async-service
|
|
11
|
+
# frozen_string_literal: true
|
|
12
|
+
|
|
13
|
+
require "async/http"
|
|
14
|
+
require "async/service/managed_service"
|
|
15
|
+
require "async/service/managed_environment"
|
|
16
|
+
|
|
17
|
+
class WebService < Async::Service::ManagedService
|
|
18
|
+
def start
|
|
19
|
+
super
|
|
20
|
+
@endpoint = @evaluator.endpoint
|
|
21
|
+
@bound_endpoint = Sync{@endpoint.bound}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stop
|
|
25
|
+
@endpoint = nil
|
|
26
|
+
@bound_endpoint&.close
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def run(instance, evaluator)
|
|
31
|
+
Console.info(self){"Starting web server on #{@endpoint}"}
|
|
32
|
+
|
|
33
|
+
server = Async::HTTP::Server.for(@bound_endpoint, protocol: @endpoint.protocol, scheme: @endpoint.scheme) do |request|
|
|
34
|
+
Protocol::HTTP::Response[200, {}, ["Hello, World!"]]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
instance.ready!
|
|
38
|
+
server.run
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module WebEnvironment
|
|
43
|
+
include Async::Service::ManagedEnvironment
|
|
44
|
+
|
|
45
|
+
def endpoint
|
|
46
|
+
Async::HTTP::Endpoint.parse("http://0.0.0.0:3000")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
service "web" do
|
|
51
|
+
service_class WebService
|
|
52
|
+
include WebEnvironment
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Save this as `web_service.rb` and make it executable:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
$ chmod +x web_service.rb
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Systemd Deployment
|
|
63
|
+
|
|
64
|
+
Systemd can manage your `async-service` application as a system service, providing automatic startup, restart on failure, and integration with system logging.
|
|
65
|
+
|
|
66
|
+
### Service File
|
|
67
|
+
|
|
68
|
+
Create a systemd service file at `/etc/systemd/system/my-web-service.service`:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
[Unit]
|
|
72
|
+
Description=My Web Service
|
|
73
|
+
After=network.target
|
|
74
|
+
|
|
75
|
+
[Service]
|
|
76
|
+
Type=notify
|
|
77
|
+
ExecStart=/usr/local/bin/bundle exec /path/to/web_service.rb
|
|
78
|
+
WorkingDirectory=/path/to/application
|
|
79
|
+
User=www-data
|
|
80
|
+
Group=www-data
|
|
81
|
+
Restart=always
|
|
82
|
+
RestartSec=5
|
|
83
|
+
StandardOutput=journal
|
|
84
|
+
StandardError=journal
|
|
85
|
+
|
|
86
|
+
[Install]
|
|
87
|
+
WantedBy=multi-user.target
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Key Configuration Points
|
|
91
|
+
|
|
92
|
+
- **Type=notify**: This is essential for `async-service` to notify systemd when the service is ready. The service uses the `sd_notify` protocol via the `$NOTIFY_SOCKET` environment variable.
|
|
93
|
+
- **ExecStart**: Points to your service script. Use `bundle exec` if you're using Bundler.
|
|
94
|
+
- **WorkingDirectory**: Set to your application root directory.
|
|
95
|
+
- **User/Group**: Run the service as a non-privileged user.
|
|
96
|
+
- **Restart=always**: Automatically restart the service if it fails.
|
|
97
|
+
|
|
98
|
+
### Installing and Managing the Service
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Reload systemd to recognize the new service
|
|
102
|
+
$ sudo systemctl daemon-reload
|
|
103
|
+
|
|
104
|
+
# Enable the service to start on boot
|
|
105
|
+
$ sudo systemctl enable my-web-service
|
|
106
|
+
|
|
107
|
+
# Start the service
|
|
108
|
+
$ sudo systemctl start my-web-service
|
|
109
|
+
|
|
110
|
+
# Check service status
|
|
111
|
+
$ sudo systemctl status my-web-service
|
|
112
|
+
|
|
113
|
+
# View service logs
|
|
114
|
+
$ sudo journalctl -u my-web-service -f
|
|
115
|
+
|
|
116
|
+
# Stop the service
|
|
117
|
+
$ sudo systemctl stop my-web-service
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Verifying Readiness
|
|
121
|
+
|
|
122
|
+
The service will notify systemd when it's ready. You can verify this by checking the service status:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
$ sudo systemctl status my-web-service
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The service should show as "active (running)" once it has notified systemd of its readiness.
|
|
129
|
+
|
|
130
|
+
## Kubernetes Deployment
|
|
131
|
+
|
|
132
|
+
Kubernetes can manage your `async-service` application as a containerized workload, providing scaling, health checks, and rolling updates.
|
|
133
|
+
|
|
134
|
+
### Dockerfile
|
|
135
|
+
|
|
136
|
+
First, create a `Dockerfile` for your application:
|
|
137
|
+
|
|
138
|
+
```dockerfile
|
|
139
|
+
FROM ruby:3.2
|
|
140
|
+
|
|
141
|
+
WORKDIR /app
|
|
142
|
+
|
|
143
|
+
# Install dependencies
|
|
144
|
+
COPY Gemfile Gemfile.lock ./
|
|
145
|
+
RUN bundle install --deployment --without development test
|
|
146
|
+
|
|
147
|
+
# Copy application files
|
|
148
|
+
COPY . .
|
|
149
|
+
|
|
150
|
+
# Expose the service port
|
|
151
|
+
EXPOSE 3000
|
|
152
|
+
|
|
153
|
+
# Set the notification log path
|
|
154
|
+
ENV NOTIFY_LOG=/tmp/notify.log
|
|
155
|
+
|
|
156
|
+
# Run the service
|
|
157
|
+
CMD ["bundle", "exec", "./web_service.rb"]
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Deployment Configuration
|
|
161
|
+
|
|
162
|
+
Create a Kubernetes deployment file `web-service-deployment.yaml`:
|
|
163
|
+
|
|
164
|
+
```yaml
|
|
165
|
+
apiVersion: apps/v1
|
|
166
|
+
kind: Deployment
|
|
167
|
+
metadata:
|
|
168
|
+
name: web-service
|
|
169
|
+
spec:
|
|
170
|
+
replicas: 2
|
|
171
|
+
selector:
|
|
172
|
+
matchLabels:
|
|
173
|
+
app: web-service
|
|
174
|
+
template:
|
|
175
|
+
metadata:
|
|
176
|
+
labels:
|
|
177
|
+
app: web-service
|
|
178
|
+
spec:
|
|
179
|
+
containers:
|
|
180
|
+
- name: web-service
|
|
181
|
+
image: my-registry/web-service:latest
|
|
182
|
+
ports:
|
|
183
|
+
- containerPort: 3000
|
|
184
|
+
name: http
|
|
185
|
+
env:
|
|
186
|
+
- name: NOTIFY_LOG
|
|
187
|
+
value: "/tmp/notify.log"
|
|
188
|
+
readinessProbe:
|
|
189
|
+
exec:
|
|
190
|
+
command:
|
|
191
|
+
- bundle
|
|
192
|
+
- exec
|
|
193
|
+
- bake
|
|
194
|
+
- async:container:notify:log:ready?
|
|
195
|
+
initialDelaySeconds: 5
|
|
196
|
+
periodSeconds: 5
|
|
197
|
+
timeoutSeconds: 3
|
|
198
|
+
failureThreshold: 12
|
|
199
|
+
livenessProbe:
|
|
200
|
+
httpGet:
|
|
201
|
+
path: /
|
|
202
|
+
port: 3000
|
|
203
|
+
initialDelaySeconds: 30
|
|
204
|
+
periodSeconds: 10
|
|
205
|
+
timeoutSeconds: 5
|
|
206
|
+
failureThreshold: 3
|
|
207
|
+
resources:
|
|
208
|
+
requests:
|
|
209
|
+
memory: "128Mi"
|
|
210
|
+
cpu: "100m"
|
|
211
|
+
limits:
|
|
212
|
+
memory: "256Mi"
|
|
213
|
+
cpu: "500m"
|
|
214
|
+
---
|
|
215
|
+
apiVersion: v1
|
|
216
|
+
kind: Service
|
|
217
|
+
metadata:
|
|
218
|
+
name: web-service
|
|
219
|
+
spec:
|
|
220
|
+
selector:
|
|
221
|
+
app: web-service
|
|
222
|
+
ports:
|
|
223
|
+
- protocol: TCP
|
|
224
|
+
port: 80
|
|
225
|
+
targetPort: 3000
|
|
226
|
+
type: LoadBalancer
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Key Configuration Points
|
|
230
|
+
|
|
231
|
+
- **readinessProbe**: Uses the `async:container:notify:log:ready?` bake task to check if the service is ready. This reads from the `NOTIFY_LOG` file.
|
|
232
|
+
- **livenessProbe**: HTTP health check to ensure the service is responding to requests.
|
|
233
|
+
- **NOTIFY_LOG**: Environment variable pointing to the notification log file path.
|
|
234
|
+
- **replicas**: Number of pod instances to run.
|
|
235
|
+
|
|
236
|
+
### Deploying to Kubernetes
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Build and push the Docker image
|
|
240
|
+
$ docker build -t my-registry/web-service:latest .
|
|
241
|
+
$ docker push my-registry/web-service:latest
|
|
242
|
+
|
|
243
|
+
# Apply the deployment
|
|
244
|
+
$ kubectl apply -f web-service-deployment.yaml
|
|
245
|
+
|
|
246
|
+
# Check deployment status
|
|
247
|
+
$ kubectl get deployments
|
|
248
|
+
$ kubectl get pods
|
|
249
|
+
|
|
250
|
+
# View pod logs
|
|
251
|
+
$ kubectl logs -f deployment/web-service
|
|
252
|
+
|
|
253
|
+
# Check service endpoints
|
|
254
|
+
$ kubectl get svc web-service
|
|
255
|
+
|
|
256
|
+
# Scale the deployment
|
|
257
|
+
$ kubectl scale deployment web-service --replicas=3
|
|
258
|
+
|
|
259
|
+
# Update the deployment (rolling update)
|
|
260
|
+
$ kubectl set image deployment/web-service web-service=my-registry/web-service:v2
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Verifying Readiness
|
|
264
|
+
|
|
265
|
+
Kubernetes will wait for the readiness probe to pass before routing traffic to the pod:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
# Check pod readiness
|
|
269
|
+
$ kubectl get pods -l app=web-service
|
|
270
|
+
|
|
271
|
+
# Describe pod to see readiness probe status
|
|
272
|
+
$ kubectl describe pod <pod-name>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The pod will show as "Ready" once the readiness probe succeeds, indicating the service has notified that it's ready to accept traffic.
|
|
276
|
+
|
|
277
|
+
## Notification Mechanism
|
|
278
|
+
|
|
279
|
+
Both systemd and Kubernetes deployments rely on the notification mechanism provided by `async-container`. The service uses `instance.ready!` to signal readiness:
|
|
280
|
+
|
|
281
|
+
- **Systemd**: Uses the `sd_notify` protocol via the `$NOTIFY_SOCKET` environment variable (automatically handled by `async-container`).
|
|
282
|
+
- **Kubernetes**: Uses a log file (`NOTIFY_LOG`) that the readiness probe checks using the `async:container:notify:log:ready?` bake task.
|
|
283
|
+
|
|
284
|
+
This ensures that your service is only considered ready when it has actually started and is prepared to handle requests, preventing premature traffic routing and improving reliability.
|
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::GenericService} 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::GenericService` 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::GenericService
|
|
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::GenericService
|
|
77
77
|
def setup(container)
|
|
78
78
|
super
|
|
79
79
|
|
|
@@ -92,6 +92,8 @@ class WebServerService < Async::Service::Generic
|
|
|
92
92
|
end
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
The evaluator is a memoized instance of the service's configuration, allowing for efficient access to configuration values throughout the service's lifecycle. If a service worker is restarted, it will create a new evaluator and a fresh environment.
|
|
96
|
+
|
|
95
97
|
### Multiple Services
|
|
96
98
|
|
|
97
99
|
You can define multiple services in a single configuration file:
|
|
@@ -101,7 +103,7 @@ You can define multiple services in a single configuration file:
|
|
|
101
103
|
|
|
102
104
|
require "async/service"
|
|
103
105
|
|
|
104
|
-
class WebService < Async::Service::
|
|
106
|
+
class WebService < Async::Service::GenericService
|
|
105
107
|
def setup(container)
|
|
106
108
|
super
|
|
107
109
|
|
|
@@ -113,7 +115,7 @@ class WebService < Async::Service::Generic
|
|
|
113
115
|
end
|
|
114
116
|
end
|
|
115
117
|
|
|
116
|
-
class WorkerService < Async::Service::
|
|
118
|
+
class WorkerService < Async::Service::GenericService
|
|
117
119
|
def setup(container)
|
|
118
120
|
super
|
|
119
121
|
|
|
@@ -151,40 +153,3 @@ end
|
|
|
151
153
|
|
|
152
154
|
Async::Service::Controller.run(configuration)
|
|
153
155
|
```
|
|
154
|
-
|
|
155
|
-
### Accessing Configuration Values
|
|
156
|
-
|
|
157
|
-
Services have access to their configuration through the environment and evaluator:
|
|
158
|
-
|
|
159
|
-
```ruby
|
|
160
|
-
class ConfigurableService < Async::Service::Generic
|
|
161
|
-
def setup(container)
|
|
162
|
-
super
|
|
163
|
-
|
|
164
|
-
container.run(count: 1, restart: true) do |instance|
|
|
165
|
-
# Clone the evaluator for thread safety
|
|
166
|
-
evaluator = self.environment.evaluator
|
|
167
|
-
database_url = evaluator.database_url
|
|
168
|
-
max_connections = evaluator.max_connections
|
|
169
|
-
debug_mode = evaluator.debug_mode
|
|
170
|
-
|
|
171
|
-
puts "Database URL: #{database_url}"
|
|
172
|
-
puts "Max connections: #{max_connections}"
|
|
173
|
-
puts "Debug mode: #{debug_mode}"
|
|
174
|
-
|
|
175
|
-
instance.ready!
|
|
176
|
-
|
|
177
|
-
# Your service implementation using these values
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
service "configurable" do
|
|
183
|
-
service_class ConfigurableService
|
|
184
|
-
database_url "postgresql://localhost/myapp"
|
|
185
|
-
max_connections 10
|
|
186
|
-
debug_mode true
|
|
187
|
-
end
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
The evaluator is a memoized instance of the service's configuration, allowing for efficient access to configuration values throughout the service's lifecycle.
|
data/context/index.yaml
CHANGED
|
@@ -18,3 +18,8 @@ files:
|
|
|
18
18
|
title: Best Practices
|
|
19
19
|
description: This guide outlines recommended patterns and practices for building
|
|
20
20
|
robust, maintainable services with `async-service`.
|
|
21
|
+
- path: deployment.md
|
|
22
|
+
title: Deployment
|
|
23
|
+
description: This guide explains how to deploy `async-service` applications using
|
|
24
|
+
systemd and Kubernetes. We'll use a simple example service to demonstrate deployment
|
|
25
|
+
configurations.
|
|
@@ -82,14 +82,14 @@ end
|
|
|
82
82
|
|
|
83
83
|
## Service
|
|
84
84
|
|
|
85
|
-
The {ruby Async::Service::
|
|
85
|
+
The {ruby Async::Service::GenericService} 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::GenericService
|
|
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::GenericService
|
|
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,7 +262,7 @@ end
|
|
|
262
262
|
|
|
263
263
|
### Health Checking
|
|
264
264
|
|
|
265
|
-
For services using `Async::Service::
|
|
265
|
+
For services using `Async::Service::ManagedService`, health checking is handled automatically. For services extending `GenericService`, you can set up health checking manually:
|
|
266
266
|
|
|
267
267
|
```ruby
|
|
268
268
|
def setup(container)
|
|
@@ -284,7 +284,7 @@ def setup(container)
|
|
|
284
284
|
end
|
|
285
285
|
```
|
|
286
286
|
|
|
287
|
-
Note: `Async::Service::
|
|
287
|
+
Note: `Async::Service::ManagedService` automatically handles health checking, container options, and process title formatting, so you typically don't need to set this up manually.
|
|
288
288
|
|
|
289
289
|
## How They Work Together
|
|
290
290
|
|
|
@@ -305,7 +305,7 @@ end
|
|
|
305
305
|
|
|
306
306
|
```ruby
|
|
307
307
|
# Services are defined using environments:
|
|
308
|
-
class WebService < Async::Service::
|
|
308
|
+
class WebService < Async::Service::GenericService
|
|
309
309
|
def setup(container)
|
|
310
310
|
super
|
|
311
311
|
|
|
@@ -440,7 +440,7 @@ Create reusable configuration modules:
|
|
|
440
440
|
|
|
441
441
|
```ruby
|
|
442
442
|
module ManagedEnvironment
|
|
443
|
-
include Async::Service::
|
|
443
|
+
include Async::Service::ManagedEnvironment
|
|
444
444
|
|
|
445
445
|
def count
|
|
446
446
|
4
|
|
@@ -464,7 +464,7 @@ end
|
|
|
464
464
|
Build service hierarchies:
|
|
465
465
|
|
|
466
466
|
```ruby
|
|
467
|
-
class BaseWebService < Async::Service::
|
|
467
|
+
class BaseWebService < Async::Service::GenericService
|
|
468
468
|
def setup(container)
|
|
469
469
|
super
|
|
470
470
|
# Common web service setup
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Copyright, 2024-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "loader"
|
|
7
|
-
require_relative "
|
|
7
|
+
require_relative "generic_service"
|
|
8
8
|
require_relative "controller"
|
|
9
9
|
|
|
10
10
|
module Async
|
|
@@ -70,7 +70,7 @@ module Async
|
|
|
70
70
|
|
|
71
71
|
@environments.each do |environment|
|
|
72
72
|
if implementing.nil? or environment.implements?(implementing)
|
|
73
|
-
if service =
|
|
73
|
+
if service = GenericService.wrap(environment)
|
|
74
74
|
yield service
|
|
75
75
|
end
|
|
76
76
|
end
|
|
@@ -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(GenericService)] 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(GenericService)] 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::GenericService)]
|
|
63
63
|
attr :services
|
|
64
64
|
|
|
65
65
|
# Start all named services.
|
|
@@ -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, 2024-2025, 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,62 @@
|
|
|
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 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 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 service.
|
|
38
|
+
#
|
|
39
|
+
# @returns [Array(String)] The list of scripts to preload.
|
|
40
|
+
def preload
|
|
41
|
+
[]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# General tags for metrics, traces and logging.
|
|
45
|
+
#
|
|
46
|
+
# @returns [Array(String)] The tags for the service.
|
|
47
|
+
def tags
|
|
48
|
+
[]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Prepare the instance for running the service.
|
|
52
|
+
#
|
|
53
|
+
# This is called before {Async::Service::ManagedService#run}.
|
|
54
|
+
#
|
|
55
|
+
# @parameter instance [Object] The container instance.
|
|
56
|
+
def prepare!(instance)
|
|
57
|
+
# No preparation required by default.
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, 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
|
+
def emit_prepared(instance, start_time)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def emit_running(instance, start_time)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Set up the container with health checking and process title formatting.
|
|
64
|
+
# @parameter container [Async::Container] The container to configure.
|
|
65
|
+
def setup(container)
|
|
66
|
+
super
|
|
67
|
+
|
|
68
|
+
container_options = @evaluator.container_options
|
|
69
|
+
health_check_timeout = container_options[:health_check_timeout]
|
|
70
|
+
|
|
71
|
+
container.run(**container_options) do |instance|
|
|
72
|
+
start_time = Async::Clock.start
|
|
73
|
+
|
|
74
|
+
Async do
|
|
75
|
+
evaluator = self.environment.evaluator
|
|
76
|
+
|
|
77
|
+
evaluator.prepare!(instance)
|
|
78
|
+
|
|
79
|
+
emit_prepared(instance, start_time)
|
|
80
|
+
|
|
81
|
+
server = run(instance, evaluator)
|
|
82
|
+
|
|
83
|
+
emit_running(instance, start_time)
|
|
84
|
+
|
|
85
|
+
health_checker(instance) do
|
|
86
|
+
instance.name = format_title(evaluator, server)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
data/readme.md
CHANGED
|
@@ -23,10 +23,18 @@ 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.16.0
|
|
33
|
+
|
|
34
|
+
- Renamed `Async::Service::Generic` -\> `Async::Service::GenericService`, added compatibilty alias.
|
|
35
|
+
- Renamed `Async::Service::Managed::Service` -\> `Async::Service::ManagedService`.
|
|
36
|
+
- Renamed `Async::Service::Managed::Environment` -\> `Async::Service::ManagedEnvironment`.
|
|
37
|
+
|
|
30
38
|
### v0.15.1
|
|
31
39
|
|
|
32
40
|
- `Managed::Service` should run within `Async do ... end`.
|
|
@@ -68,10 +76,6 @@ Please see the [project releases](https://socketry.github.io/async-service/relea
|
|
|
68
76
|
|
|
69
77
|
- Allow providing a list of modules to include in environments.
|
|
70
78
|
|
|
71
|
-
### v0.8.0
|
|
72
|
-
|
|
73
|
-
- Introduce `Environment#implements?` and related methods for interface checking.
|
|
74
|
-
|
|
75
79
|
## Contributing
|
|
76
80
|
|
|
77
81
|
We welcome contributions to this project.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.16.0
|
|
4
|
+
|
|
5
|
+
- Renamed `Async::Service::Generic` -\> `Async::Service::GenericService`, added compatibilty alias.
|
|
6
|
+
- Renamed `Async::Service::Managed::Service` -\> `Async::Service::ManagedService`.
|
|
7
|
+
- Renamed `Async::Service::Managed::Environment` -\> `Async::Service::ManagedEnvironment`.
|
|
8
|
+
|
|
3
9
|
## v0.15.1
|
|
4
10
|
|
|
5
11
|
- `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.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -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
|
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
|
|
@@ -1,17 +0,0 @@
|
|
|
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
|