async-service 0.15.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/best-practices.md +30 -7
- data/context/deployment.md +284 -0
- data/context/getting-started.md +8 -43
- data/context/index.yaml +5 -0
- data/context/service-architecture.md +38 -9
- data/lib/async/service/configuration.rb +2 -2
- data/lib/async/service/controller.rb +3 -3
- data/lib/async/service/generic.rb +6 -56
- data/lib/async/service/generic_service.rb +65 -0
- data/lib/async/service/health_checker.rb +48 -0
- data/lib/async/service/managed_environment.rb +70 -0
- data/lib/async/service/managed_service.rb +108 -0
- data/lib/async/service/version.rb +1 -1
- data/license.md +1 -1
- data/readme.md +13 -8
- data/releases.md +11 -0
- data.tar.gz.sig +0 -0
- metadata +9 -8
- metadata.gz.sig +0 -0
- data/lib/async/service/managed/environment.rb +0 -48
- data/lib/async/service/managed/health_checker.rb +0 -49
- data/lib/async/service/managed/service.rb +0 -83
- data/lib/async/service/managed.rb +0 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e9258424ee834d70b309984ba6f2d0c89f87ed9b2a16ec38fb298dfafb33fcd
|
|
4
|
+
data.tar.gz: e91b2129702c7c8ad7f3203063d3dd526b8e6e91d322bb84388afe873cf638f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: edc78ce3ff189ef951f19b206d02bbef4f2c121b10e24f074b6a7eab702c6ffeceb5e47c7c29d330363c7389c721b371c4584f33b9145cde42b798ec08e47237
|
|
7
|
+
data.tar.gz: be8a666be488d9d428339edea966834c0a7d5e935bcf330b8669f914b58661ed11deff0782b4202327951b108063992e5ac6f4fb9427beca5d6c1daaf09f440a
|
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,13 +201,14 @@ 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.
|
|
211
|
+
# - Status messages during startup to prevent premature timeouts.
|
|
211
212
|
# - Preloading of scripts before startup.
|
|
212
213
|
|
|
213
214
|
private def format_title(evaluator, server)
|
|
@@ -222,6 +223,28 @@ class WebService < Async::Service::Managed::Service
|
|
|
222
223
|
end
|
|
223
224
|
```
|
|
224
225
|
|
|
226
|
+
### Configure Timeouts for Slow-Starting Services
|
|
227
|
+
|
|
228
|
+
If your service takes a long time to start up (e.g., loading large datasets, connecting to external services), configure appropriate timeouts:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
module WebEnvironment
|
|
232
|
+
include Async::Service::ManagedEnvironment
|
|
233
|
+
|
|
234
|
+
# Allow 2 minutes for startup.
|
|
235
|
+
def startup_timeout
|
|
236
|
+
120
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Require health checks every 30 seconds.
|
|
240
|
+
def health_check_timeout
|
|
241
|
+
30
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The `startup_timeout` ensures processes that hang during startup are detected, while `health_check_timeout` monitors processes after they've become ready. `ManagedService` automatically sends `status!` messages during startup to keep the health check clock resetting until the service is ready.
|
|
247
|
+
|
|
225
248
|
### Implement Meaningful Process Titles
|
|
226
249
|
|
|
227
250
|
Use the `format_title` method to provide dynamic process information:
|
|
@@ -247,7 +270,7 @@ Try to keep process titles short and focused.
|
|
|
247
270
|
Utilize the `start` and `stop` hooks to manage shared resources effectively:
|
|
248
271
|
|
|
249
272
|
```ruby
|
|
250
|
-
class WebService < Async::Service::
|
|
273
|
+
class WebService < Async::Service::ManagedService
|
|
251
274
|
def start
|
|
252
275
|
# Bind to the endpoint in the container:
|
|
253
276
|
@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,9 @@ 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. The service sends `status!` messages during startup to prevent premature health check timeouts, then transitions to sending `ready!` messages once the service is actually ready.
|
|
266
|
+
|
|
267
|
+
For services extending `GenericService`, you can set up health checking manually:
|
|
266
268
|
|
|
267
269
|
```ruby
|
|
268
270
|
def setup(container)
|
|
@@ -270,12 +272,15 @@ def setup(container)
|
|
|
270
272
|
health_check_timeout = container_options[:health_check_timeout]
|
|
271
273
|
|
|
272
274
|
container.run(**container_options) do |instance|
|
|
275
|
+
# Send status updates during startup:
|
|
276
|
+
instance.status!("Preparing...")
|
|
273
277
|
# Prepare your service.
|
|
274
278
|
|
|
275
279
|
Async do
|
|
276
280
|
# Start your service.
|
|
281
|
+
instance.status!("Starting...")
|
|
277
282
|
|
|
278
|
-
#
|
|
283
|
+
# Once ready, set up health checking:
|
|
279
284
|
health_checker(instance, health_check_timeout) do
|
|
280
285
|
instance.name = "#{self.name}: #{current_status}"
|
|
281
286
|
end
|
|
@@ -284,7 +289,31 @@ def setup(container)
|
|
|
284
289
|
end
|
|
285
290
|
```
|
|
286
291
|
|
|
287
|
-
|
|
292
|
+
#### Startup Timeout vs Health Check Timeout
|
|
293
|
+
|
|
294
|
+
The container supports two separate timeout mechanisms:
|
|
295
|
+
|
|
296
|
+
- **`startup_timeout`**: Maximum time a process can take to become ready (call `ready!`) before being terminated. This detects processes that hang during startup and never become ready.
|
|
297
|
+
- **`health_check_timeout`**: Maximum time between health check messages after the process has become ready. This detects processes that stop responding after they've started.
|
|
298
|
+
|
|
299
|
+
Both timeouts use a single clock that starts when the process starts. The clock resets when the process becomes ready, transitioning from startup timeout monitoring to health check timeout monitoring.
|
|
300
|
+
|
|
301
|
+
You can configure these timeouts via `container_options`:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
module WebEnvironment
|
|
305
|
+
include Async::Service::ManagedEnvironment
|
|
306
|
+
|
|
307
|
+
def container_options
|
|
308
|
+
super.merge(
|
|
309
|
+
startup_timeout: 60, # Allow up to 60 seconds for startup.
|
|
310
|
+
health_check_timeout: 30 # Require health checks every 30 seconds after ready.
|
|
311
|
+
)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
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
317
|
|
|
289
318
|
## How They Work Together
|
|
290
319
|
|
|
@@ -305,7 +334,7 @@ end
|
|
|
305
334
|
|
|
306
335
|
```ruby
|
|
307
336
|
# Services are defined using environments:
|
|
308
|
-
class WebService < Async::Service::
|
|
337
|
+
class WebService < Async::Service::GenericService
|
|
309
338
|
def setup(container)
|
|
310
339
|
super
|
|
311
340
|
|
|
@@ -440,7 +469,7 @@ Create reusable configuration modules:
|
|
|
440
469
|
|
|
441
470
|
```ruby
|
|
442
471
|
module ManagedEnvironment
|
|
443
|
-
include Async::Service::
|
|
472
|
+
include Async::Service::ManagedEnvironment
|
|
444
473
|
|
|
445
474
|
def count
|
|
446
475
|
4
|
|
@@ -464,7 +493,7 @@ end
|
|
|
464
493
|
Build service hierarchies:
|
|
465
494
|
|
|
466
495
|
```ruby
|
|
467
|
-
class BaseWebService < Async::Service::
|
|
496
|
+
class BaseWebService < Async::Service::GenericService
|
|
468
497
|
def setup(container)
|
|
469
498
|
super
|
|
470
499
|
# 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.
|