servitude 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0875adaf0bae960908e33467f31e8a15dc79bf5e
4
+ data.tar.gz: cbde282dc8d7eb9964168bfadcab4bc564d51ea2
5
+ SHA512:
6
+ metadata.gz: ed319f26554dc51dc9bcdbe5db9e03db4c67fce36cab1fe85de5f8924f7b5102167532803781d608b9e2bb4b8b2cd1c727eeda4a8b754bd8f6430c427a318899
7
+ data.tar.gz: ed3c76b26403ed32f0eb10437c039b7e53b3625fbf2c8f57a99c0d0b4661e33c3c954151bfc8213d399ddf62e77a122d3c015699e7008946a12ac0bd99f79910
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ servitude
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.1.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in servitude.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jason Harrelson
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # Servitude
2
+
3
+ A set of tools for writing single or multi-threaded Ruby servers.
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'servitude'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install servitude
19
+
20
+
21
+ ## Usage
22
+
23
+ For executable examples see the [examples folder](https://github.com/midas/servitude/tree/master/examples).
24
+
25
+ To build a server with Servitude only a couple of steps are required.
26
+
27
+ * Include the Servitude::Base module in the base module of your server project.
28
+ * Create a server class and include the Servitude::Server module (also include Servitude::ServerThreaded if you want multi-threaded server).
29
+ * Create a CLI class and include the Servitude::Cli module.
30
+ * If a single threaded server, implement your functionality in the Server#run method.
31
+ * 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.
32
+
33
+ For more details see the examples folder.
34
+
35
+ The rest of this document will discuss the functionality each module provides.
36
+
37
+ ### Servitude::Actor
38
+
39
+ In order to achieve well abstracted multi-threaded functionality Servitude employs the [Celluloid](https://github.com/celluloid/celluloid) gem. The actor module simply
40
+ abstracts some of the details of creating an actor away so you may concentrate on the functionality. For example:
41
+
42
+ module AwesomeServer
43
+ class MessageHandler
44
+ include Servitude::Actor
45
+
46
+ def call( options )
47
+ # some neat functionality ...
48
+ end
49
+ end
50
+ end
51
+
52
+ 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
53
+ expected entry point to the actor.
54
+
55
+ The [Celluloid wiki](https://github.com/celluloid/celluloid/wiki) does a very good job of explaining the actor pattern. In summary, an actor is a concurrent object
56
+ that runs in its own thread.
57
+
58
+ ### Servitude::Base
59
+
60
+ 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
61
+ 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"
62
+ and not a Hash.
63
+
64
+ If you do not call ::boot, an error is raised before your server can be started.
65
+
66
+ module AwesomeServer
67
+ include Servitude::Base
68
+
69
+ boot host_namespace: AwesomeServer,
70
+ app_id: 'awesome-server',
71
+ app_name: 'Aswesome Server',
72
+ company: 'Awesome, Inc.',
73
+ default_config_path: "/etc/awesome/awesome-server.conf",
74
+ default_log_path: "/var/log/awesome/awesome-server.log",
75
+ default_pid_path: "/var/run/awesome/awesome-server.pid",
76
+ default_thread_count: 1,
77
+ version_copyright: "v#{VERSION} \u00A9#{Time.now.year} Awesome, Inc."
78
+ end
79
+
80
+ ### Servitude::Cli
81
+
82
+ The Cli module provides the functionality of a Command Line Interface for your server.
83
+
84
+ module AwesomeServer
85
+ class Cli
86
+ include Servitude::Cli
87
+ end
88
+ end
89
+
90
+ In your CLI file (bin/awesome-server):
91
+
92
+ #!/usr/bin/env ruby
93
+ require 'awesome_server'
94
+ AwesomeServer::Cli.new( ARGV ).run
95
+
96
+ ### Servitude::Configuration
97
+
98
+ The Configuration module provides functionality for creating a configuration class. You must call the ::configurations method and provide configuration
99
+ attributes to it. The Configuration module also provides a ::from_file method that allows a configuration to be read from a JSON config file.
100
+
101
+ module AwesomeServer
102
+ class Configuration
103
+ include Servitude::Configuration
104
+
105
+ configurations :some_config, # attribute with no default
106
+ [:another, 'some value'] # attribute with a default
107
+ end
108
+ end
109
+
110
+ If you want to load your configuration from a JSON config file you may do so by registering a callback (most likely an after_initialize callback). If the
111
+ configuration file does not exist an error will be raised by Servitude.
112
+
113
+ module AwesomeServer
114
+ class Server
115
+ include Servitude::Server
116
+
117
+ after_initialize do
118
+ Configuration.from_file( options[:config] )
119
+ end
120
+
121
+ # ...
122
+ end
123
+ end
124
+
125
+ ### Servitude::Server
126
+
127
+ The Server module provides the base functionality for implementing a server, such as configuring the loggers, setting up Unix signal handling, outputting a
128
+ startup banner, etc. You must override the #run method in order to implement your functionality
129
+
130
+ module AwesomeServer
131
+ class Server
132
+ include Servitude::Server
133
+
134
+ def run
135
+ info 'Running ...'
136
+ end
137
+ end
138
+ end
139
+
140
+ #### Callbacks
141
+
142
+ The Server module provides callbacks to utilize in your server implementation:
143
+
144
+ * __before_initialize__: executes just before the initilaization of the server
145
+ * __after_initialize__: executes immediately after initilaization of the server
146
+ * __before_sleep__: executes just before the main thread sleeps to avoid exiting
147
+ * __finalize__: executes before server exits
148
+
149
+ You can provide one or more method names or procs to the callbacks to be executed.
150
+
151
+ module AwesomeServer
152
+ class Server
153
+ after_initialize :configure_server
154
+
155
+ finalize :cleanup
156
+
157
+ finalize do
158
+ info "Shutting down ..."
159
+ end
160
+
161
+ protected
162
+
163
+ def configure_server
164
+ # configuration code here ...
165
+ end
166
+
167
+ def cleanup
168
+ # cleanup code here ...
169
+ end
170
+ end
171
+ end
172
+
173
+ You can also define callbacks on your server and use them. The callback/hook functionality is provided by the [hooks gem](https://github.com/apotonick/hooks).
174
+
175
+ module AwesomeServer
176
+ class Server
177
+ define_hook :before_run
178
+
179
+ before_run do
180
+ # do something ...
181
+ end
182
+
183
+ def run
184
+ run_hook :before_run
185
+ # do something ...
186
+ end
187
+ end
188
+ end
189
+
190
+ ### Servitude::ServerThreaded
191
+
192
+ The ServerThreaded module extends server functionality to be multi-threaded, providing several convenience methods to abstract away correctly handling certain
193
+ situations Celluloid actors present. The ServerThreaded module must be included after the Server module.
194
+
195
+ module AwesomeServer
196
+ class Server
197
+ include Servitude::Server
198
+ include Servitude::ServerThreaded
199
+ end
200
+ end
201
+
202
+ The ServerThreaded module assumes you will use the Celluloid actor pattern to implement your functionality. Al you must do to implement the threaded functionality
203
+ is override the #handler\_class method to specify the class that will act as your handler (actor) and utilize the #with_supervision block and
204
+ \#call_handler_respecting_thread_count method providing the options to pass to your handler's #call method.
205
+
206
+ The #with_supervision block implements error handling/retry logic required to correctly interact with Celluloid supervision without bombing due to dead actor errors.
207
+
208
+ module AwesomeServer
209
+ class Server
210
+ include Servitude::Server
211
+ include Servitude::ServerThreaded
212
+
213
+ def run
214
+ some_event_generated_block do |event_args|
215
+ with_supervision do
216
+ call_handler_respecting_thread_count( info: event_args.info )
217
+ end
218
+ end
219
+ end
220
+
221
+ def handler_class
222
+ AwesomeServer::MessageHandler
223
+ end
224
+ end
225
+ end
226
+
227
+ 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
228
+ 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
229
+ message queue.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'servitude'
5
+
6
+ # The simple server is as actually not a true server at all as it does nothing except for start and run (serving no
7
+ # requests).
8
+ #
9
+ # Use CTRL-c (INT signal) to stop the server. Additionally send the process an INT or TERM signal using the kill comand
10
+ # or your # OS's # process monitoring application. All 3 strategies result in a graceful shutdown as displayed by the
11
+ # 'Shutting down ...' which occurs due to the finalize block.
12
+ #
13
+ # Usage:
14
+ # bundle exec examples/1_simple_server
15
+ #
16
+ module SimpleServer
17
+
18
+ include Servitude::Base
19
+
20
+ APP_FOLDER = 'simple-server'
21
+ VERSION = '1.0.0'
22
+
23
+ boot host_namespace: SimpleServer,
24
+ app_id: 'simple-server',
25
+ app_name: 'Simple Server',
26
+ company: 'LFE',
27
+ default_config_path: "/usr/local/etc/#{APP_FOLDER}/#{APP_FOLDER}.conf",
28
+ default_log_path: "/usr/local/var/log/#{APP_FOLDER}/#{APP_FOLDER}.log",
29
+ default_pid_path: "/usr/local/var/run/#{APP_FOLDER}/#{APP_FOLDER}.pid",
30
+ default_thread_count: 1,
31
+ version_copyright: "v#{VERSION} \u00A9#{Time.now.year} LFE"
32
+
33
+ class Server
34
+
35
+ include Servitude::Server
36
+
37
+ finalize do
38
+ info 'Shutting down ...'
39
+ end
40
+
41
+ def run
42
+ info "Running ..."
43
+ end
44
+
45
+ end
46
+ end
47
+
48
+ SimpleServer::Server.new.start
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'servitude'
5
+ require 'socket'
6
+ require 'pry'
7
+
8
+ # The echo server accepts a message and repeats it back. It is the first real server in the examples.
9
+ #
10
+ # Note: Due to TcpServer#accept's implementation, the server is not currently gracefully shutting down as the trap of INT
11
+ # appears to never happen.
12
+ #
13
+ # Usage:
14
+ # bundle exec examples/2_echo_server
15
+ #
16
+ # Then use telent to exercise the server:
17
+ # $ telnet localhost 1234
18
+ # Hello World!
19
+ # You said: Hello World!
20
+ # Connection closed by foreign host.
21
+ #
22
+ module EchoServer
23
+
24
+ include Servitude::Base
25
+
26
+ APP_FOLDER = 'echo-server'
27
+ VERSION = '1.0.0'
28
+
29
+ boot host_namespace: EchoServer,
30
+ app_id: 'echo-server',
31
+ app_name: 'Echo Server',
32
+ company: 'LFE',
33
+ default_config_path: "/usr/local/etc/#{APP_FOLDER}/#{APP_FOLDER}.conf",
34
+ default_log_path: "/usr/local/var/log/#{APP_FOLDER}/#{APP_FOLDER}.log",
35
+ default_pid_path: "/usr/local/var/run/#{APP_FOLDER}/#{APP_FOLDER}.pid",
36
+ default_thread_count: 1,
37
+ version_copyright: "v#{VERSION} \u00A9#{Time.now.year} LFE"
38
+
39
+ class Server
40
+
41
+ include Servitude::Server
42
+
43
+ after_initialize do
44
+ @tcp_server = TCPServer.open( 'localhost', '1234' )
45
+ end
46
+
47
+ finalize do
48
+ binding.pry
49
+ info 'Shutting down ...'
50
+ end
51
+
52
+ def run
53
+ while client = tcp_server.accept
54
+ line = client.gets
55
+ info "Received '#{line.strip}'"
56
+ response = "You said: #{line.strip}"
57
+ client.puts response
58
+ info "Responded with '#{response}'"
59
+ info "Closing connection"
60
+ client.close
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ attr_reader :tcp_server
67
+
68
+ end
69
+ end
70
+
71
+ EchoServer::Server.new.start
@@ -0,0 +1,13 @@
1
+ require 'celluloid/autostart'
2
+
3
+ module Servitude
4
+ module Actor
5
+
6
+ def self.included( base )
7
+ base.class_eval do
8
+ include Celluloid
9
+ end
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,54 @@
1
+ require 'servitude'
2
+
3
+ module Servitude
4
+ module Base
5
+
6
+ def self.included( base )
7
+ base.extend( ClassMethods )
8
+ base.class_eval do
9
+ class << self
10
+ attr_accessor :logger
11
+ end
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ def boot( host_namespace:,
18
+ app_id:,
19
+ app_name:,
20
+ company:,
21
+ default_config_path:,
22
+ default_log_path:,
23
+ default_pid_path:,
24
+ default_thread_count:,
25
+ version_copyright:)
26
+ Servitude::const_set :NS, host_namespace
27
+
28
+ const_set :APP_ID, app_id
29
+ const_set :APP_NAME, app_name
30
+ const_set :COMPANY, company
31
+ const_set :DEFAULT_CONFIG_PATH, default_config_path
32
+ const_set :DEFAULT_LOG_PATH, default_log_path
33
+ const_set :DEFAULT_PID_PATH, default_pid_path
34
+ const_set :DEFAULT_THREAD_COUNT, default_thread_count
35
+ const_set :VERSION_COPYRIGHT, version_copyright
36
+
37
+ Servitude::boot_called = true
38
+ end
39
+
40
+ def configuration
41
+ @configuration ||= Servitude::NS::Configuration.new
42
+ end
43
+
44
+ def configuration=( configuration )
45
+ @configuration = configuration
46
+ end
47
+
48
+ def configure
49
+ yield( configuration ) if block_given?
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,136 @@
1
+ require 'rubygems'
2
+ require 'servitude'
3
+ require 'trollop'
4
+ require 'yell'
5
+
6
+ module Servitude
7
+ module Cli
8
+
9
+ def self.included( base )
10
+ base.class_eval do
11
+ attr_reader :cmd,
12
+ :options
13
+ end
14
+ end
15
+
16
+ SUB_COMMANDS = %w(
17
+ restart
18
+ start
19
+ status
20
+ stop
21
+ )
22
+
23
+ def initialize( args )
24
+ unless Servitude.boot_called
25
+ raise 'You must call boot before starting server'
26
+ end
27
+
28
+ Trollop::options do
29
+ version Servitude::NS::VERSION_COPYRIGHT
30
+ banner <<-EOS
31
+ #{Servitude::NS::APP_NAME} #{Servitude::NS::VERSION_COPYRIGHT}
32
+
33
+ Usage:
34
+ #{Servitude::NS::APP_ID} [command] [options]
35
+
36
+ commands:
37
+ #{SUB_COMMANDS.map { |sub_cmd| " #{sub_cmd}" }.join( "\n" )}
38
+
39
+ (For help with a command: #{Servitude::NS::APP_ID} [command] -h)
40
+
41
+ options:
42
+ EOS
43
+ stop_on SUB_COMMANDS
44
+ end
45
+
46
+ # Get the sub-command and its options
47
+ #
48
+ @cmd = ARGV.shift || ''
49
+ @options = case( cmd )
50
+ when ''
51
+ Trollop::die 'No command provided'
52
+ when "restart"
53
+ Trollop::options do
54
+ opt :config, "The path for the config file", :type => String, :short => '-c', :default => Servitude::NS::DEFAULT_CONFIG_PATH
55
+ opt :log_level, "The log level", :type => String, :default => 'info', :short => '-o'
56
+ opt :log, "The path for the log file", :type => String, :short => '-l', :default => Servitude::NS::DEFAULT_LOG_PATH
57
+ opt :pid, "The path for the PID file", :type => String, :default => Servitude::NS::DEFAULT_PID_PATH
58
+ opt :threads, "The number of threads", :type => Integer, :default => Servitude::NS::DEFAULT_THREAD_COUNT, :short => '-t'
59
+ end
60
+ when "start"
61
+ Trollop::options do
62
+ opt :config, "The path for the config file", :type => String, :short => '-c', :default => Servitude::NS::DEFAULT_CONFIG_PATH
63
+ opt :interactive, "Execute the server in interactive mode", :short => '-i'
64
+ opt :log_level, "The log level", :type => String, :default => 'info', :short => '-o'
65
+ opt :log, "The path for the log file", :type => String, :short => '-l', :default => Servitude::NS::DEFAULT_LOG_PATH
66
+ opt :pid, "The path for the PID file", :type => String, :default => Servitude::NS::DEFAULT_PID_PATH
67
+ opt :threads, "The number of threads", :type => Integer, :default => Servitude::NS::DEFAULT_THREAD_COUNT, :short => '-t'
68
+ end
69
+ when "status"
70
+ Trollop::options do
71
+ opt :pid, "The path for the PID file", :type => String, :default => Servitude::NS::DEFAULT_PID_PATH
72
+ end
73
+ when "stop"
74
+ Trollop::options do
75
+ opt :pid, "The path for the PID file", :type => String, :default => Servitude::NS::DEFAULT_PID_PATH
76
+ end
77
+ else
78
+ Trollop::die "unknown command #{cmd.inspect}"
79
+ end
80
+
81
+ if cmd == 'start'
82
+ unless options[:interactive]
83
+ Trollop::die( :config, "is required when running as daemon" ) unless options[:config]
84
+ Trollop::die( :log, "is required when running as daemon" ) unless options[:log]
85
+ Trollop::die( :pid, "is required when running as daemon" ) unless options[:pid]
86
+ end
87
+ end
88
+
89
+ if %w(restart status stop).include?( cmd )
90
+ Trollop::die( :pid, "is required" ) unless options[:pid]
91
+ end
92
+ end
93
+
94
+ def self.commands( &block )
95
+ end
96
+
97
+ def run
98
+ send( cmd )
99
+ end
100
+
101
+ protected
102
+
103
+ def start
104
+ if options[:interactive]
105
+ start_interactive
106
+ else
107
+ start_daemon
108
+ end
109
+ end
110
+
111
+ def start_interactive
112
+ server = Servitude::NS::Server.new( options.merge( log: nil ))
113
+ server.start
114
+ end
115
+
116
+ def start_daemon
117
+ server = Servitude::Daemon.new( options )
118
+ server.start
119
+ end
120
+
121
+ def stop
122
+ server = Servitude::Daemon.new( options )
123
+ server.stop
124
+ end
125
+
126
+ def restart
127
+ stop
128
+ start_daemon
129
+ end
130
+
131
+ def status
132
+ Servitude::Daemon.new( options ).status
133
+ end
134
+
135
+ end
136
+ end
@@ -0,0 +1,58 @@
1
+ module Servitude
2
+ module Configuration
3
+
4
+ def self.included( base )
5
+ base.extend ClassMethods
6
+
7
+ base.class_eval do
8
+ class << self
9
+ attr_accessor :attributes,
10
+ :configured
11
+ end
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ def configurations( *attrs )
18
+ raise 'Already configured: cannot call configurations more than once' if self.configured
19
+
20
+ self.attributes = attrs.map { |attr| Array( attr ).first.to_s }
21
+
22
+ class_eval do
23
+ attr_accessor( *self.attributes )
24
+ end
25
+
26
+ attrs.select { |attr| attr.is_a?( Array ) }.
27
+ each do |k, v|
28
+
29
+ define_method k do
30
+ instance_variable_get( "@#{k}" ) ||
31
+ instance_variable_set( "@#{k}", v )
32
+ end
33
+
34
+ end
35
+
36
+ self.configured = true
37
+ end
38
+
39
+ def from_file( file_path )
40
+ unless File.exists?( file_path )
41
+ raise "Configuration file #{file_path} does not exist"
42
+ end
43
+
44
+ options = Oj.load( File.read( file_path ))
45
+ Servitude::NS::configuration = Servitude::NS::Configuration.new
46
+
47
+ attributes.each do |c|
48
+ if options[c]
49
+ Servitude::NS::configuration.send( :"#{c}=", options[c] )
50
+ end
51
+ end
52
+
53
+ options[:config_loaded] = true
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,176 @@
1
+ require 'rubygems'
2
+ require 'fileutils'
3
+ require 'timeout'
4
+
5
+ module Servitude
6
+ class Daemon
7
+
8
+ attr_reader :name,
9
+ :options,
10
+ :pid,
11
+ :pid_path,
12
+ :script,
13
+ :timeout
14
+
15
+ def initialize( options )
16
+ @options = options
17
+ @name = options[:name] || Servitude::NS::APP_NAME
18
+ @pid_path = options[:pid] || '.'
19
+ @pid = get_pid
20
+ @timeout = options[:timeout] || 10
21
+ end
22
+
23
+ def start
24
+ abort "Process already running!" if process_exists?
25
+
26
+ pid = fork do
27
+ exit if fork
28
+ Process.setsid
29
+ exit if fork
30
+ store_pid( Process.pid )
31
+ File.umask 0000
32
+ redirect_output!
33
+ run
34
+ end
35
+
36
+ Process.waitpid( pid )
37
+ end
38
+
39
+ def run
40
+ Servitude::NS::Server.new( options ).start
41
+ end
42
+
43
+ def stop
44
+ case kill_process
45
+ when :success
46
+ remove_pid
47
+ when :failed_to_stop
48
+ when :does_not_exist
49
+ puts "#{Servitude::NS::APP_NAME} process is not running"
50
+ prompt_and_remove_pid_file if pid_file_exists?
51
+ else
52
+ raise 'Unknown return code from #kill_process'
53
+ end
54
+ end
55
+
56
+ def status
57
+ if process_exists?
58
+ puts "#{Servitude::NS::APP_NAME} process running with PID: #{pid}"
59
+ else
60
+ puts "#{Servitude::NS::APP_NAME} process does not exist"
61
+ prompt_and_remove_pid_file if pid_file_exists?
62
+ end
63
+ end
64
+
65
+ protected
66
+
67
+ def prompt_and_remove_pid_file
68
+ puts "PID file still exists at '#{pid_path}', would you like to remove it (y/n)?"
69
+ answer = $stdin.gets.strip
70
+ if answer == 'y'
71
+ remove_pid
72
+ puts "Removed PID file"
73
+ end
74
+ end
75
+
76
+ def remove_pid
77
+ FileUtils.rm( pid_path ) if File.exists?( pid_path )
78
+ end
79
+
80
+ def store_pid( pid )
81
+ File.open( pid_path, 'w' ) do |f|
82
+ f.puts pid
83
+ end
84
+ rescue => e
85
+ $stderr.puts "Unable to open #{pid_path} for writing:\n\t(#{e.class}) #{e.message}"
86
+ exit!
87
+ end
88
+
89
+ def get_pid
90
+ return nil unless File.exists?( pid_path )
91
+
92
+ pid = nil
93
+
94
+ File.open( @pid_path, 'r' ) do |f|
95
+ pid = f.readline.to_s.gsub( /[^0-9]/, '' )
96
+ end
97
+
98
+ pid.to_i
99
+ rescue Errno::ENOENT
100
+ nil
101
+ end
102
+
103
+ def remove_pidfile
104
+ File.unlink( pid_path )
105
+ rescue => e
106
+ $stderr.puts "Unable to unlink #{pid_path}:\n\t(#{e.class}) #{e.message}"
107
+ exit
108
+ end
109
+
110
+ def kill_process
111
+ return :does_not_exist unless process_exists?
112
+
113
+ $stdout.write "Attempting to stop #{Servitude::NS::APP_NAME} process #{pid}..."
114
+ Process.kill INT, pid
115
+
116
+ iteration_num = 0
117
+ while process_exists? && iteration_num < 10
118
+ sleep 1
119
+ $stdout.write "."
120
+ iteration_num += 1
121
+ end
122
+
123
+ if process_exists?
124
+ $stderr.puts "\nFailed to stop #{Servitude::NS::APP_NAME} process #{pid}"
125
+ return :failed_to_stop
126
+ else
127
+ $stdout.puts "\nSuccessfuly stopped #{Servitude::NS::APP_NAME} process #{pid}"
128
+ end
129
+
130
+ return :success
131
+ rescue Errno::EPERM
132
+ $stderr.puts "No permission to query #{pid}!";
133
+ end
134
+
135
+ def pid_file_exists?
136
+ File.exists?( pid_path )
137
+ end
138
+
139
+ def process_exists?
140
+ return false unless pid
141
+ Process.kill( 0, pid )
142
+ true
143
+ rescue Errno::ESRCH, TypeError # "PID is NOT running or is zombied
144
+ false
145
+ rescue Errno::EPERM
146
+ $stderr.puts "No permission to query #{pid}!";
147
+ false
148
+ end
149
+
150
+ def redirect_output!
151
+ if log_path = options[:log]
152
+ #puts "redirecting to log"
153
+ # if the log directory doesn't exist, create it
154
+ FileUtils.mkdir_p( File.dirname( log_path ), :mode => 0755 )
155
+ # touch the log file to create it
156
+ FileUtils.touch( log_path )
157
+ # Set permissions on the log file
158
+ File.chmod( 0644, log_path )
159
+ # Reopen $stdout (NOT +STDOUT+) to start writing to the log file
160
+ $stdout.reopen( log_path, 'a' )
161
+ # Redirect $stderr to $stdout
162
+ $stderr.reopen $stdout
163
+ $stdout.sync = true
164
+ else
165
+ #puts "redirecting to /dev/null"
166
+ # We're not bothering to sync if we're dumping to /dev/null
167
+ # because /dev/null doesn't care about buffered output
168
+ $stdin.reopen '/dev/null'
169
+ $stdout.reopen '/dev/null', 'a'
170
+ $stderr.reopen $stdout
171
+ end
172
+ log_path = options[:log] ? options[:log] : '/dev/null'
173
+ end
174
+
175
+ end
176
+ end
@@ -0,0 +1,21 @@
1
+ module Servitude
2
+ module Logging
3
+
4
+ %w(
5
+ debug
6
+ error
7
+ fatal
8
+ info
9
+ warn
10
+ ).each do |level|
11
+
12
+ define_method level do |*messages|
13
+ messages.each do |message|
14
+ Servitude::NS.logger.send level, message
15
+ end
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,62 @@
1
+ require 'servitude'
2
+ require 'hooks'
3
+
4
+ module Servitude
5
+ module Server
6
+
7
+ def self.included( base )
8
+ base.class_eval do
9
+ include Logging
10
+ include ServerLogging
11
+ include Hooks
12
+
13
+ define_hook :after_initialize,
14
+ :before_initialize,
15
+ :before_sleep,
16
+ :finalize
17
+
18
+ attr_reader :options
19
+ end
20
+ end
21
+
22
+ def initialize( options={} )
23
+ unless Servitude.boot_called
24
+ raise 'You must call boot before starting server'
25
+ end
26
+
27
+ @options = options
28
+
29
+ run_hook :before_initialize
30
+ initialize_loggers
31
+ run_hook :after_initialize
32
+ end
33
+
34
+ def start
35
+ log_startup
36
+ run
37
+
38
+ trap( INT ) { stop }
39
+ trap( TERM ) { stop }
40
+
41
+ run_hook :before_sleep
42
+ sleep
43
+ end
44
+
45
+ protected
46
+
47
+ def run
48
+ raise NotImplementedError
49
+ end
50
+
51
+ private
52
+
53
+ def finalize
54
+ run_hook :finalize
55
+ end
56
+
57
+ def stop
58
+ Thread.new { finalize; exit }.join
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,80 @@
1
+ require 'yell'
2
+
3
+ # Provides logging services for the base server.
4
+ #
5
+ module Servitude
6
+ module ServerLogging
7
+
8
+ protected
9
+
10
+ def initialize_loggers
11
+ Servitude::NS.logger = Yell.new do |l|
12
+ l.level = log_level
13
+ l.adapter $stdout, :level => [:debug, :info, :warn]
14
+ l.adapter $stderr, :level => [:error, :fatal]
15
+ end
16
+
17
+ #Celluloid.logger = Yell.new do |l|
18
+ #l.level = :info
19
+ #l.adapter :file, File.join( File.dirname( options[:log] ), "#{APP_ID}-celluloid.log" )
20
+ #end
21
+ end
22
+
23
+ def log_startup
24
+ start_banner( options ).each do |line|
25
+ Servitude::NS.logger.info line
26
+ end
27
+ end
28
+
29
+ def start_banner( options )
30
+ configuration_attributes = Servitude::NS::Configuration.attributes rescue nil
31
+ [
32
+ "",
33
+ "***",
34
+ "* #{Servitude::NS::APP_NAME} started",
35
+ "*",
36
+ "* #{Servitude::NS::VERSION_COPYRIGHT}",
37
+ "*",
38
+ "* Configuration:",
39
+ (options[:config_loaded] ? "* file: #{options[:config]}" : nil),
40
+ Array( configuration_attributes ).map { |a| "* #{a}: #{config_value a}" },
41
+ "*",
42
+ "***",
43
+ ].flatten.reject( &:nil? )
44
+ end
45
+
46
+ def log_level
47
+ options.fetch( :log_level, :info ).to_sym
48
+ end
49
+
50
+
51
+ def config_value( key )
52
+ value = Servitude::NS.configuration.send( key )
53
+
54
+ return value unless value.is_a?( Hash )
55
+
56
+ return redacted_hash( value )
57
+ end
58
+
59
+ def redacted_hash( hash )
60
+ redacted_hash = {}
61
+
62
+ hash.keys.
63
+ collect( &:to_s ).
64
+ grep( /#{redacted_keys}/i ).
65
+ each do |blacklisted_key|
66
+ value = hash[blacklisted_key]
67
+ redacted_hash[blacklisted_key] = value.nil? ? nil : '[REDACTED]'
68
+ end
69
+
70
+ hash.merge( redacted_hash )
71
+ end
72
+
73
+ def redacted_keys
74
+ %w(
75
+ password
76
+ ).join( '|' )
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,62 @@
1
+ module Servitude
2
+ module ServerThreaded
3
+
4
+ def self.included( base )
5
+ base.class_eval do
6
+ after_initialize :initialize_thread
7
+ end
8
+ end
9
+
10
+ protected
11
+
12
+ def with_supervision( &block )
13
+ begin
14
+ block.call
15
+ rescue Servitude::SupervisionError
16
+ # supervisor is restarting actor
17
+ #warn ANSI.cyan { "RETRYING due to waiting on supervisor to restart actor ..." }
18
+ retry
19
+ rescue Celluloid::DeadActorError
20
+ # supervisor has yet to begin restarting actor
21
+ #warn ANSI.blue { "RETRYING due to Celluloid::DeadActorError ..." }
22
+ retry
23
+ end
24
+ end
25
+
26
+ # Correctly calls a single supervised actor when the threads configuraiton is set
27
+ # to 1, or a pool of actors if threads configuration is > 1. Also protects against
28
+ # a supervised actor from being nil if the supervisor is reinitializing when access
29
+ # is attempted.
30
+ #
31
+ def call_handler_respecting_thread_count( options )
32
+ if options[:threads] > 1
33
+ pool.async.call( options )
34
+ else
35
+ raise Servitude::SupervisionError unless handler
36
+ handler.call( options )
37
+ end
38
+ end
39
+
40
+ def handler_class
41
+ raise NotImplementedError
42
+ end
43
+
44
+ def run
45
+ raise NotImplementedError
46
+ end
47
+
48
+ def pool
49
+ @pool ||= handler_class.pool( size: options[:threads] )
50
+ end
51
+
52
+ def handler
53
+ Celluloid::Actor[:handler]
54
+ end
55
+
56
+ def initialize_thread
57
+ return unless options[:threads] == 1
58
+ handler_class.supervise_as :handler
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,5 @@
1
+ module Servitude
2
+ class SupervisionError < RuntimeError
3
+
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Servitude
2
+ VERSION = "0.1.0"
3
+ end
data/lib/servitude.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'servitude/version'
2
+ require 'rainbow'
3
+
4
+ module Servitude
5
+
6
+ autoload :Actor, 'servitude/actor'
7
+ autoload :Base, 'servitude/base'
8
+ autoload :Cli, 'servitude/cli'
9
+ autoload :Configuration, 'servitude/configuration'
10
+ autoload :Daemon, 'servitude/daemon'
11
+ autoload :Logging, 'servitude/logging'
12
+ autoload :ServerLogging, 'servitude/server_logging'
13
+ autoload :Server, 'servitude/server'
14
+ autoload :ServerThreaded, 'servitude/server_threaded'
15
+ autoload :SupervisionError, 'servitude/supervision_error'
16
+
17
+ INT = "INT"
18
+ TERM = "TERM"
19
+
20
+ class << self
21
+ attr_accessor :boot_called
22
+ end
23
+
24
+ end
data/servitude.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'servitude/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "servitude"
8
+ spec.version = Servitude::VERSION
9
+ spec.authors = ["Jason Harrelson"]
10
+ spec.email = ["jason@lookforwardenterprises.com"]
11
+ spec.summary = %q{A set of utilities to aid in building multithreaded Ruby servers utilizing Celluloid.}
12
+ spec.description = %q{A set of utilities to aid in building multithreaded Ruby servers utilizing Celluloid. See README for more details.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "pry-debugger"
23
+ spec.add_development_dependency "rake"
24
+
25
+ spec.add_dependency "celluloid", "~> 0"
26
+ spec.add_dependency "hooks", "~> 0"
27
+ spec.add_dependency "oj", "~> 2"
28
+ spec.add_dependency "rainbow", "~> 2"
29
+ spec.add_dependency "trollop", "~> 2"
30
+ spec.add_dependency "yell", "~> 1"
31
+ end
metadata ADDED
@@ -0,0 +1,194 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: servitude
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jason Harrelson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-09-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry-debugger
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: celluloid
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: hooks
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: oj
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rainbow
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2'
111
+ - !ruby/object:Gem::Dependency
112
+ name: trollop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '2'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '2'
125
+ - !ruby/object:Gem::Dependency
126
+ name: yell
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1'
139
+ description: A set of utilities to aid in building multithreaded Ruby servers utilizing
140
+ Celluloid. See README for more details.
141
+ email:
142
+ - jason@lookforwardenterprises.com
143
+ executables: []
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - ".gitignore"
148
+ - ".ruby-gemset"
149
+ - ".ruby-version"
150
+ - Gemfile
151
+ - LICENSE.txt
152
+ - README.md
153
+ - Rakefile
154
+ - examples/1_simple_server
155
+ - examples/2_echo_server
156
+ - lib/servitude.rb
157
+ - lib/servitude/actor.rb
158
+ - lib/servitude/base.rb
159
+ - lib/servitude/cli.rb
160
+ - lib/servitude/configuration.rb
161
+ - lib/servitude/daemon.rb
162
+ - lib/servitude/logging.rb
163
+ - lib/servitude/server.rb
164
+ - lib/servitude/server_logging.rb
165
+ - lib/servitude/server_threaded.rb
166
+ - lib/servitude/supervision_error.rb
167
+ - lib/servitude/version.rb
168
+ - servitude.gemspec
169
+ homepage: ''
170
+ licenses:
171
+ - MIT
172
+ metadata: {}
173
+ post_install_message:
174
+ rdoc_options: []
175
+ require_paths:
176
+ - lib
177
+ required_ruby_version: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ required_rubygems_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ requirements: []
188
+ rubyforge_project:
189
+ rubygems_version: 2.2.1
190
+ signing_key:
191
+ specification_version: 4
192
+ summary: A set of utilities to aid in building multithreaded Ruby servers utilizing
193
+ Celluloid.
194
+ test_files: []