A set of tools for writing single or multi-threaded Ruby servers.
Add this line to your application's Gemfile:
gem 'servitude'
And then execute:
$ bundle
Or install it yourself as:
$ gem install servitude
To build a server with Servitude only a couple of steps are required.
- Include the Servitude::Base module in the base module of your server project.
- Create a server class and include the Servitude::Server module (also include Servitude::ServerThreaded if you want multi-threaded server).
- Create a CLI class and include the Servitude::Cli module.
- If a single threaded server, implement your functionality in the Server#run method.
- If a multi-threaded server, implement your functionality in a handler class that includes the Servitude::Actor module and call the handler from the Server#run method.
For executable examples see the examples folder. To run the examples, clone the project, install the bundle and follow the usage instructions in each example.
The rest of this document will discuss the functionality each module provides.
In order to achieve well abstracted multi-threaded functionality Servitude employs the Celluloid gem. The actor module simply abstracts some of the details of creating an actor away so you may concentrate on the functionality. For example:
module AwesomeServer
class MessageHandler
include Servitude::Actor
def call( options )
# some neat functionality ...
end
end
end
While the #call method is not a Celluloid concept, in order to integrate with the Servitude::Server's default implementation, the #call method is the expected entry point to the actor.
The Celluloid wiki does a very good job of explaining the actor pattern. In summary, an actor is a concurrent object that runs in its own thread.
The Base module provides core functionality for your Ruby server and should be inlcuded in the outermost namespace of your server project. In addition to including the Base module, you must call the ::boot method and provide the required arguments to it. Note, the arguments for ::boot are Ruby "required keyword arguments" and not a Hash.
If you do not call ::boot, an error is raised before your server can be started.
module AwesomeServer
include Servitude::Base
boot host_namespace: AwesomeServer,
app_id: 'awesome-server',
app_name: 'Aswesome Server',
attribution: "v#{VERSION} \u00A9#{Time.now.year} Awesome, Inc."
author: 'Awesome, Inc.',
default_config_path: "/etc/awesome/awesome-server.conf"
end
The Cli module provides several classes with Command Line Interface functionality for your server. The Cli::Service class provides standard unix service sub-commands: start, stop, status and restart.
module AwesomeServer
class Cli < Servitude::Cli::Service
end
end
In your CLI file (bin/awesome-server):
#!/usr/bin/env ruby
require 'awesome_server'
AwesomeServer::Cli.start
To build a custom CLI, you can inherit from Cli::Base.
module AwesomeServer
class Cli < Servitude::Cli::Base
end
end
For details on how to add commands to your custom or standard service CLIs see the Thor documentation.
All Servitude servers automatically have a configuration instantiated for them (although it may be empty). The default class for the configuration is Servitude::Configuration. In order to define a custom configuration, define a custom configuration class (which may inherit from Servitude::Configuration) and simply override the Servitude::Cli::Service#configuration method in your Server class. Be sure the custom configuration calss accepts the command line options and passes them to the super class's initializer or configuration will be completely broken.
module AwesomeServer
class Cli < Servitude::Cli::Base
# necessary so Thor does not pick this method up as a CLI method
no_commands do
def configuration_class
AwesomeServer::Configuration
end
end
end
end
The Servitude::Configuration class delegates to a Hashie::Mash backend, which gives it great flexibiltiy. Any Hash or JSON like structure can be passed directly into the configuration and work. Thus, one does not have to explicitly define the configuration attributes as the configuration will represent exactly what is in the JSON config file. In addition, the command line options are passed into the configuration and merged to the configuration that came from a config file (if there is a config file). The merge results in the command line options overriding any matching file configurations.
For example, given a config file:
{
"key1": "value1",
"log_level": "info",
"envs": {
"development": {
"key2": "value2",
},
"production": {
"key2": "value3",
}
}
}
And command line options of:
$ awesome-server start --interactive --log_level debug
The configuration result will be:
{
"key1": "value1",
"log_level": "debug",
"interactive": true,
"envs": {
"development": {
"key2": "value2",
},
"production": {
"key2": "value3",
}
}
}
Notice the log_level has been overridden to the command line option value instead of the file value. Because the command line options are an inherently flass structure, any config file options that should be overridden should be at the first level of the JSON structure.
Because Hashie::Mash is the backend for the configuration values may be accessed using a hash notation or an object notation.
config['key1'] # => "value1"
config[:key1] # => "value1"
config.key1 # => "value1"
config['development']['key2'] # => "value2"
config[:development][:key2] # => "value2"
config.development.key2 # => "value2"
The startup banner for a Servitude server automatically outputs the ocnfiguration options in a dot notation format. Continuing our configuraiton example, the smart banner would look like:
***
* Awesome Server started
*
* v1.0.0 ©2014 Awesome Company
*
* Configuration
* config: /Users/cjharrelson/development/personal/gems/servitude/config/echo-server.conf
* log_level: debug
* log: STDOUT
* pid: /Users/cjharrelson/development/personal/gems/servitude/tmp/echo-server.pid
* threads: 1
* key1: value1
* envs.development.key2: value2
* envs.production.key2: value3
*
***
You may notice the absence of the interactive value. This is due to filtering built into the start banner output. Several values are already in the default_config_filters that are a result of the Trollop implementation of the command line option parsing. If you would like to add additional keys to be filterd, override the config_filters method in your server class and provide an array of keys (in dot notation) to filter.
module AwesomeServer
class Server
...
def config_filters
%w(
key1
envs.development.key2
)
end
...
end
end
Building upon Servitude::Configuration, the EnvironmentConfiguration adds the concept of environments to configuration. In order to use EnvironmentConfiguration override #configuration_class in your server class.
module AwesomeServer
class Server
include Servitude::Server
def configuration_class
Servitude::EnvironmentConfiguration
end
end
end
The command line can except and --environment (-e) switch, although it is not required. If using config file and environemnt, a best practice is to put the default environment in your config file so there is a default environment.
{
"environment": "development",
"development": {
...
},
"production": {
...
}
}
The Server module provides the base functionality for implementing a server, such as configuring the loggers, setting up Unix signal handling, outputting a startup banner, etc. You must override the #run method in order to implement your functionality
module AwesomeServer
class Server
include Servitude::Server
def run
info 'Running ...'
end
end
end
The Server module provides callbacks to utilize in your server implementation:
- before_initialize: executes just before the initilaization of the server
- after_initialize: executes immediately after initilaization of the server
- before_run: executes just before the run method is called
- before_sleep: executes just before the main thread sleeps to avoid exiting
- finalize: executes before server exits
You can provide one or more method names or procs to the callbacks to be executed.
module AwesomeServer
class Server
after_initialize :configure_server
finalize :cleanup
finalize do
info "Shutting down ..."
end
protected
def configure_server
# configuration code here ...
end
def cleanup
# cleanup code here ...
end
end
end
You can also define callbacks on your server and use them. The callback/hook functionality is provided by the hooks gem.
module AwesomeServer
class Server
define_hook :before_run
before_run do
# do something ...
end
def run
run_hook :before_run
# do something ...
end
end
end
The ServerThreaded module extends server functionality to be multi-threaded, providing several convenience methods to abstract away correctly handling certain situations Celluloid actors present. The ServerThreaded module must be included after the Server module.
module AwesomeServer
class Server
include Servitude::Server
include Servitude::ServerThreaded
end
end
The ServerThreaded module assumes you will use the Celluloid actor pattern to implement your functionality. Al you must do to implement the threaded functionality is override the #handler_class method to specify the class that will act as your handler (actor) and utilize the #with_supervision block and #call_handler_respecting_thread_count method providing the options to pass to your handler's #call method.
The #with_supervision block implements error handling/retry logic required to correctly interact with Celluloid supervision without bombing due to dead actor errors.
module AwesomeServer
class Server
include Servitude::Server
include Servitude::ServerThreaded
def run
some_event_generated_block do |event_args|
with_supervision do
call_handler_respecting_thread_count( info: event_args.info )
end
end
end
def handler_class
AwesomeServer::MessageHandler
end
end
end
The #some_event_generated_block method call in the code block above represents some even that happend that needs to be processed. All servers sleep until an event happens and then do some work, respond and then go back to sleep. Some good examples are receiving packets form a TCP/UDP socket or receiving a message from a message queue.
- Nils Jonsson njonsson