servitude 0.1.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 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: []