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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce29cb5debee3f9a0b9dbdf80c0087fe52973e00c14c4d114a044fe8656d0920
4
- data.tar.gz: eec4b6a5117ea33293e942a68fc656707b59a434d89df4e604e5ab95a11c9ad3
3
+ metadata.gz: 1dc20befccfdbe1d36db11898e11a469ff64f2338a7b88942d59cb58e0e8c92f
4
+ data.tar.gz: c2a09fad50e09a6188e30b213d921f832ac14706c8f23d721269e4e48cffe028
5
5
  SHA512:
6
- metadata.gz: a95d7e8bc23385c1d874368e0582156e574ed2b9fa9defbd4c2ffff75499e457c48421f0610a94bf8de1f8326245d55f77751a59b0507d31278ecc89c6af0dc2
7
- data.tar.gz: f79918dd27119524826711b5fa872bc37014c93fd460683b13358c14a98dfb4d96ed34dd08c19d4ac1fead8c5ad5acf045a2e006752bcdc05cc8276531b97bec
6
+ metadata.gz: ef436a78425bfab299e187e37e81f6d442dd00ffbff6c3e526c94d5753395e03158219d3b92d7e3edec4a68babf277fc97448d2a18991b6ab1d79cfdd6cafb74
7
+ data.tar.gz: 59468d0b53c26be506a8c4195cae58c91627352ebf09555bdcbf539d166c66d97f4c6a9ad3969fd0e8251f857cc058e4ad69e322ece8cd585b0579c0fbc44d69
checksums.yaml.gz.sig CHANGED
Binary file
@@ -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::Managed::Environment
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::Managed::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::Managed::Environment} for services that need robust lifecycle management using {ruby Async::Service::Managed::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::Managed::Environment
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::Managed::Service` over `Generic` for most services:
204
+ Prefer `Async::Service::ManagedService` over `GenericService` for most services:
205
205
 
206
206
  ```ruby
207
- class WebService < Async::Service::Managed::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::Managed::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.
@@ -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::Generic} which represents the base class for implementing services.
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::Generic` and implements a `setup` method that defines how the service runs.
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::Generic
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::Generic
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::Generic
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::Generic
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::Generic} 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.
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::Generic
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::Generic
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::Managed::Service`, health checking is handled automatically. For services extending `Generic`, you can set up health checking manually:
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::Managed::Service` automatically handles health checking, container options, and process title formatting, so you typically don't need to set this up manually.
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::Generic
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::Managed::Environment
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::Generic
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 "generic"
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 = Generic.wrap(environment)
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(Generic)] The services to control.
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(Generic)] The services to manage.
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::Generic)]
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
- # 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 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
+
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Async
7
7
  module Service
8
- VERSION = "0.15.1"
8
+ VERSION = "0.16.0"
9
9
  end
10
10
  end
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.15.1
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/managed.rb
101
- - lib/async/service/managed/environment.rb
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