Best Practices
This guide outlines recommended patterns and practices for building robust, maintainable services with async-service
.
Application Structure
If you are creating an application that runs services, you should define a top level services.rb
file that includes all your service configurations. This file serves as the main entry point for your services. If you are specifically working with the Falcon web server, this file is typically called falcon.rb
for historical reasons.
Service Configuration
Create a single top-level service.rb
file as your main entry point:
#!/usr/bin/env async-service
# Load your service configurations
require_relative 'lib/my_library/environment/web_environment'
require_relative 'lib/my_library/environment/worker_environment'
service "web" do
include MyLibrary::Environment::WebEnvironment
end
service "worker" do
include MyLibrary::Environment::WorkerEnvironment
end
Multiple Service Configurations
In some cases, you may want to define multiple service configurations, e.g. for different environments or deployment targets. In those cases, you may create web_service.rb
or job_service.rb
, but this usage should be discouraged.
Library Structure
If you are creating a library that exposes services, use the following structure and guidelines:
Directory Structure
Organize your code following these conventions:
├── service.rb
└── lib/
└── my_library/
├── environment/
│ ├── web_environment.rb
│ ├── worker_environment.rb
│ ├── database_environment.rb
│ └── tls_environment.rb
└── service/
├── web_service.rb
└── worker_service.rb
Environment Organization
Place environments in lib/my_library/environment/
:
# lib/my_library/environment/web_environment.rb
module MyLibrary
module Environment
module WebEnvironment
include Async::Service::ContainerEnvironment
def service_class
MyLibrary::Service::WebService
end
def port
3000
end
def host
"localhost"
end
end
end
end
Service Organization
Place services in lib/my_library/service/
:
# lib/my_library/service/web_service.rb
module MyLibrary
module Service
class WebService < Async::Service::ContainerService
private def format_title(evaluator, server)
if server&.respond_to?(:connection_count)
"#{self.name} [#{evaluator.host}:#{evaluator.port}] (#{server.connection_count} connections)"
else
"#{self.name} [#{evaluator.host}:#{evaluator.port}]"
end
end
def run(instance, evaluator)
# Start your service and return the server object.
# ContainerService handles container setup, health checking, and process titles.
start_web_server(evaluator.host, evaluator.port)
end
private
def start_web_server(host, port)
# The return value of this method will be the server object which is returned from `run` and passed to `format_title`.
end
end
end
end
Use ContainerEnvironment
for Services
Include module Async::Service::ContainerEnvironment
for services that run in containers using class Async::Service::ContainerService
:
module WebEnvironment
include Async::Service::ContainerEnvironment
def service_class
WebService
end
end
Environment Best Practices
Use Plain Modules
Prefer plain Ruby modules for environments:
module DatabaseEnvironment
def database_url
"postgresql://localhost/app"
end
def max_connections
10
end
end
One-to-One Service-Environment Correspondence
Maintain a 1:1 relationship between services and their primary environments:
# Primary environment for WebService
module WebEnvironment
def service_class
WebService
end
# Default configuration:
def port
3000
end
def host
'0.0.0.0'
end
end
# Primary environment for WorkerService
module WorkerEnvironment
def service_class
WorkerService
end
def queue_name
'default'
end
def count
4
end
end
Compose with Auxiliary Environments
Use additional environments for cross-cutting concerns:
module WebEnvironment
include DatabaseEnvironment
include TLSEnvironment
include LoggingEnvironment
def service_class
WebService
end
end
Service Best Practices
Use ContainerService as Base Class
Prefer Async::Service::ContainerService
over Generic
for most services:
class WebService < Async::Service::ContainerService
# ContainerService automatically handles:
# - Container setup with proper options.
# - Health checking with process title updates.
# - Integration with Formatting module.
private def format_title(evaluator, server)
# Customize process title display
"#{self.name} [#{evaluator.host}:#{evaluator.port}]"
end
def run(instance, evaluator)
# Focus only on your service logic
start_web_server(evaluator.host, evaluator.port)
end
end
Implement Meaningful Process Titles
Use the format_title
method to provide dynamic process information:
private def format_title(evaluator, server)
# Good - Include service-specific info
"#{self.name} [#{evaluator.host}:#{evaluator.port}]"
# Better - Include dynamic runtime status
if connection_count = server&.connection_count
"#{self.name} [#{evaluator.host}:#{evaluator.port}] (C=#{format_count connection_count})"
else
"#{self.name} [#{evaluator.host}:#{evaluator.port}]"
end
end
Try to keep process titles short and focused.
Testing Best Practices
Test Environments in Isolation
Test environment modules independently:
# test/my_library/environment/web_environment.rb
describe MyLibrary::Environment::WebEnvironment do
let(:environment) do
Async::Service::Environment.build do
include MyLibrary::Environment::WebEnvironment
end
end
it "provides default port" do
expect(environment.port).to be == 3000
end
end
Test Services with Service Controller
Use test environments for service testing:
# test/my_library/service/web_service.rb
describe MyLibrary::Service::WebService do
let(:environment) do
Async::Service::Environment.build do
include MyLibrary::Environment::WebEnvironment
end
end
let(:evaluator) {environment.evaluator}
let(:service) {evaluator.service_class(environment, evaluator)}
let(:controller) {Async::Service::Controller.for(service)}
before do
controller.start
end
after do
controller.stop
end
let(:uri) {URI "http://#{evaluator.host}:#{evaluator.port}"}
it "responds to requests" do
Net::HTTP.get(uri).tap do |response|
expect(response).to be_a(Net::HTTPSuccess)
end
end
end
Note that full end-to-end service tests like this are typically slow and hard to isolate, so it's better to use unit tests for individual components whenever possible.