sanford 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.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
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
18
+ log/*.output
19
+ .pid
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sanford.gemspec
4
+ gemspec
5
+
6
+ gem 'bson_ext'
7
+ gem 'rake', '~>0.9.2'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 jcredding
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,164 @@
1
+ # Sanford
2
+
3
+ Sanford: simple hosts for Sanford services. Define hosts for versioned services. Setup handlers for the services. Run the host as a daemon.
4
+
5
+ Sanford uses [Sanford::Protocol](https://github.com/redding/sanford-protocol) to communicate with clients.
6
+
7
+ ## Usage
8
+
9
+ ```ruby
10
+ # define a host
11
+ class MyHost
12
+ include Sanford::Host
13
+
14
+ port 8000
15
+ pid_dir '/path/to/pids'
16
+
17
+ # define some services
18
+ version 'v1' do
19
+ service 'get_user', 'MyHost::V1Services::GetUser'
20
+ end
21
+
22
+ end
23
+
24
+ # define handlers for the services
25
+ class MyHost::V1Services::GetUser
26
+ include Sanford::ServiceHandler
27
+
28
+ def run!
29
+ # process the service call and build a response
30
+ # the return value of this method will be used as the response data
31
+ end
32
+ end
33
+
34
+ ```
35
+
36
+ ## Hosts
37
+
38
+ To define a Sanford host, include the mixin `Sanford::Host` on a class and use the DSL to configure it. A few options can be set:
39
+
40
+ * `ip` - (string) A hostname or IP address for the server to bind to; default: `'0.0.0.0'`.
41
+ * `port` - (integer) The port number for the server to bind to.
42
+ * `pid_dir` - (string) Path to the directory where you want the pid file to be written; default: `Dir.pwd`.
43
+ * `logger`- (logger) A logger for Sanford to use when handling requests; default: `Logger.new`.
44
+
45
+ Any values specified using the DSL act as defaults for instances of the host. You can overwritten when creating new instances:
46
+
47
+ ```ruby
48
+ host = MyHost.new({ :port => 12000 })
49
+ ```
50
+
51
+ ## Services
52
+
53
+ ```ruby
54
+ class MyHost
55
+ include Sanford::Host
56
+
57
+ version 'v1' do
58
+ service 'get_user', 'MyHost::ServicesV1::GetUser'
59
+ end
60
+ end
61
+ ```
62
+
63
+ Services are defined on hosts by version. Each named service maps to a 'service handler' class. The version and service name are used to 'route' requests to handler classes.
64
+
65
+ When defining services handlers, it's typical to organize them all under a common namespace. Use `service_handler_ns` to define a default namespace for all handler classes under the version:
66
+
67
+ ```ruby
68
+ class MyHost
69
+ include Sanford::Host
70
+
71
+ version 'v1' do
72
+ service_handler_ns 'MyHost::ServicesV1'
73
+
74
+ service 'get_user', 'GetUser'
75
+ service 'get_article', 'GetArticle'
76
+ service 'get_comments', '::MyHost::OtherServices::GetComments'
77
+ end
78
+ end
79
+ ```
80
+
81
+ ## Service Handlers
82
+
83
+ Define handlers by mixing in `Sanford::ServiceHandler` on a class and defining a `run!` method:
84
+
85
+ ```ruby
86
+ class MyHost::Services::GetUser
87
+ include Sanford::ServiceHandler
88
+
89
+ def run!
90
+ # process the service call and generate a response
91
+ # the return value of this method will be used as
92
+ # the response data returned to the client
93
+ end
94
+ end
95
+ ```
96
+
97
+ This is the most basic way to define a service handler. In addition to this, the `init!` method can be overwritten. This will be called after an instance of the service handler is created. The `init!` method is intended as a hook to add constructor logic. The `initialize` method should not be overwritten.
98
+
99
+ In addition to these, there are some helpers methods that can be used in your `run!` method:
100
+
101
+ * `request`: returns the request object the host received
102
+ * `params`: returns the params payload from the request object
103
+ * `halt`: stop processing and return response data with a status code and message
104
+
105
+ ```ruby
106
+ class MyHost::Services::GetUser
107
+ include Sanford::ServiceHandler
108
+
109
+ def run!
110
+ User.find(params['user_id']).attributes
111
+ rescue NotFoundException => e
112
+ halt :not_found, :message => e.message, :data => request.params
113
+ rescue Exception => e
114
+ halt :error, :message => e.message
115
+ end
116
+ end
117
+ ```
118
+
119
+ ## Running Host Daemons
120
+
121
+ Sanford comes with rake tasks for running hosts:
122
+
123
+ * `rake sanford:start` - spin up a background process running the host daemon.
124
+ * `rake sanford:stop` - shutdown the background process running the host gracefully.
125
+ * `rake sanford:restart` - runs the stop and then the start tasks.
126
+ * `rake sanford:run` - starts the server, but don't daemonize it (runs in the current ruby process). Convenient when using the server in a development environment.
127
+
128
+ These can be installed by requiring it's rake tasks in your `Rakefile`:
129
+
130
+ ```ruby
131
+ require 'sanford/rake'
132
+ ```
133
+
134
+ The basic rake tasks are useful if your application only has one host defined and if you only want to run the host on a single port. In the case you have multiple hosts defined or you want to run a single host on multiple ports, use environment variables to set custom configurations.
135
+
136
+ ```bash
137
+ rake sanford:start # starts the first defined host
138
+ SANFORD_HOST=AnotherHost SANFORD_PORT=13001 rake sanford:start # choose a specific host and port to run on with ENV vars
139
+ ```
140
+
141
+ The rake tasks allow using environment variables for specifying which host to run the command against and for overriding the host's configuration. They recognize the following environment variables: `SANFORD_HOST`, `SANFORD_IP`, and `SANFORD_PORT`.
142
+
143
+ Define a `name` on a Host to set a string name for your host that can be used to reference a host when using the rake tasks. If no name is set, Sanford will use the host's class name.
144
+
145
+ ### Loading An Application
146
+
147
+ Typically, a Sanford host is part of a larger application and parts of the application need to be setup or loaded when you start your Sanford server. The task `sanford:setup` is called before running any start, stop, or restart task; override it to hook in your application setup code:
148
+
149
+ ```ruby
150
+ # In your Rakefile
151
+ namespace :sanford do
152
+ task :setup do
153
+ require 'config/environment'
154
+ end
155
+ end
156
+ ```
157
+
158
+ ## Contributing
159
+
160
+ 1. Fork it
161
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
162
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
163
+ 4. Push to the branch (`git push origin my-new-feature`)
164
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ ENV['SANFORD_SERVICES_CONFIG'] = 'bench/services'
4
+ require 'sanford/rake'
5
+ require 'bench/tasks'
6
+
7
+ require "assert/rake_tasks"
8
+ Assert::RakeTasks.install
data/bench/client.rb ADDED
@@ -0,0 +1,30 @@
1
+ require 'socket'
2
+
3
+ require 'sanford-protocol'
4
+
5
+ module Bench
6
+
7
+ class Client
8
+
9
+ def initialize(host, port)
10
+ @host, @port = [ host, port ]
11
+ end
12
+
13
+ def call(version, name, params)
14
+ socket = TCPSocket.open(@host, @port)
15
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true) # TODO - explain
16
+ connection = Sanford::Protocol::Connection.new(socket)
17
+ request = Sanford::Protocol::Request.new(version, name, params)
18
+ connection.write(request.to_hash)
19
+ if IO.select([ socket ], nil, nil, 10)
20
+ Sanford::Protocol::Response.parse(connection.read)
21
+ else
22
+ raise "Timed out!"
23
+ end
24
+ ensure
25
+ socket.close rescue false
26
+ end
27
+
28
+ end
29
+
30
+ end
data/bench/report.txt ADDED
@@ -0,0 +1,10 @@
1
+ Running benchmark report...
2
+
3
+ Hitting "simple" service with {}, 10000 times
4
+ ....................................................................................................
5
+ Total Time: 15768.8473ms
6
+ Average Time: 1.5768ms
7
+ Min Time: 0.9949ms
8
+ Max Time: 102.2670ms
9
+
10
+ Done running benchmark report
data/bench/runner.rb ADDED
@@ -0,0 +1,102 @@
1
+ Bundler.setup(:benchmark)
2
+ require 'benchmark'
3
+
4
+ require 'bench/client'
5
+
6
+ module Bench
7
+
8
+ class Runner
9
+ # this should match up with bench/services host and port
10
+ HOST_AND_PORT = [ '127.0.0.1', 12000 ]
11
+
12
+ REQUESTS = [
13
+ [ 'v1', 'simple', {}, 10000 ]
14
+ ]
15
+
16
+ TIME_MODIFIER = 10 ** 4 # 4 decimal places
17
+
18
+ def initialize(options = {})
19
+ options[:output] ||= File.expand_path("../report.txt", __FILE__)
20
+
21
+ @file = File.open(options[:output], "w")
22
+ end
23
+
24
+ def build_report
25
+ output "Running benchmark report..."
26
+
27
+ REQUESTS.each do |version, name, params, times|
28
+ self.benchmark_service(version, name, params, times, false)
29
+ end
30
+
31
+ output "Done running benchmark report"
32
+ end
33
+
34
+ def benchmark_service(version, name, params, times, show_result = false)
35
+ benchmarks = []
36
+
37
+ output "\nHitting #{name.inspect} service with #{params.inspect}, #{times} times"
38
+ [*(1..times.to_i)].each do |index|
39
+ benchmark = self.hit_service(name, version, params.merge({ :request_number => index }), show_result)
40
+ benchmarks << self.round_time(benchmark.real * 1000.to_f)
41
+ output('.', false) if ((index - 1) % 100 == 0) && !show_result
42
+ end
43
+ output("\n", false)
44
+
45
+ total_time = benchmarks.inject(0){|s, n| s + n }
46
+ data = {
47
+ :number_of_requests => times,
48
+ :total_time_taken => self.round_and_display(total_time),
49
+ :average_time_taken => self.round_and_display(total_time / benchmarks.size),
50
+ :min_time_taken => self.round_and_display(benchmarks.min),
51
+ :max_time_taken => self.round_and_display(benchmarks.max)
52
+ }
53
+ size = data.values.map(&:size).max
54
+ output "Total Time: #{data[:total_time_taken].rjust(size)}ms"
55
+ output "Average Time: #{data[:average_time_taken].rjust(size)}ms"
56
+ output "Min Time: #{data[:min_time_taken].rjust(size)}ms"
57
+ output "Max Time: #{data[:max_time_taken].rjust(size)}ms"
58
+ output "\n"
59
+ end
60
+
61
+ protected
62
+
63
+ def hit_service(version, name, params, show_result)
64
+ Benchmark.measure do
65
+ begin
66
+ client = Bench::Client.new(*HOST_AND_PORT)
67
+ response = client.call(name, version, params)
68
+ if show_result
69
+ output "Got a response:"
70
+ output " #{response.status}"
71
+ output " #{response.data.inspect}"
72
+ end
73
+ rescue Exception => exception
74
+ puts "FAILED -> #{exception.class}: #{exception.message}"
75
+ puts exception.backtrace.join("\n")
76
+ end
77
+ end
78
+ end
79
+
80
+ def output(message, puts = true)
81
+ method = puts ? :puts : :print
82
+ self.send(method, message)
83
+ @file.send(method, message)
84
+ STDOUT.flush if method == :print
85
+ end
86
+
87
+ def round_and_display(time_in_ms)
88
+ self.display_time(self.round_time(time_in_ms))
89
+ end
90
+
91
+ def round_time(time_in_ms)
92
+ (time_in_ms * TIME_MODIFIER).to_i / TIME_MODIFIER.to_f
93
+ end
94
+
95
+ def display_time(time)
96
+ integer, fractional = time.to_s.split('.')
97
+ [ integer, fractional.ljust(4, '0') ].join('.')
98
+ end
99
+
100
+ end
101
+
102
+ end
data/bench/services.rb ADDED
@@ -0,0 +1,23 @@
1
+ class BenchHost
2
+ include Sanford::Host
3
+
4
+ self.port = 12000
5
+ self.pid_dir = File.expand_path("../../tmp", __FILE__)
6
+
7
+ version 'v1' do
8
+ service 'simple', 'BenchHost::Simple'
9
+ end
10
+
11
+ class Simple
12
+ include Sanford::ServiceHandler
13
+
14
+ def run!
15
+ { :string => 'test', :int => 1, :float => 2.1, :boolean => true,
16
+ :hash => { :something => 'else' }, :array => [ 1, 2, 3 ],
17
+ :request_number => self.request.params['request_number']
18
+ }
19
+ end
20
+
21
+ end
22
+
23
+ end
data/bench/tasks.rb ADDED
@@ -0,0 +1,18 @@
1
+ namespace :bench do
2
+
3
+ task :load do
4
+ require 'bench/runner'
5
+ end
6
+
7
+ desc "Run a Benchmark report against the Benchmark server"
8
+ task :report => :load do
9
+ Bench::Runner.new.build_report
10
+ end
11
+
12
+ desc "Run Benchmark requests against the 'simple' service"
13
+ task :simple, [ :times ] => :load do |t, args|
14
+ runner = Bench::Runner.new(:output => '/dev/null')
15
+ runner.benchmark_service('v1', 'simple', {}, args[:times] || 1, true)
16
+ end
17
+
18
+ end
@@ -0,0 +1,33 @@
1
+ require 'ns-options'
2
+ require 'pathname'
3
+ require 'set'
4
+
5
+ ENV['SANFORD_SERVICES_CONFIG'] ||= 'config/services'
6
+
7
+ module Sanford
8
+
9
+ module Config
10
+ include NsOptions::Proxy
11
+
12
+ option :hosts, Set, :default => []
13
+ option :services_config, Pathname, :default => ENV['SANFORD_SERVICES_CONFIG']
14
+
15
+ # We want class names to take precedence over a configured name, so that if
16
+ # a user specifies a specific class, they always get it
17
+ def self.find_host(name)
18
+ self.find_host_by_class_name(name) || self.find_host_by_name(name)
19
+ end
20
+
21
+ protected
22
+
23
+ def self.find_host_by_class_name(class_name)
24
+ self.hosts.detect{|host_class| host_class.to_s == class_name.to_s }
25
+ end
26
+
27
+ def self.find_host_by_name(name)
28
+ self.hosts.detect{|host_class| host_class.name == name.to_s }
29
+ end
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,70 @@
1
+ # Sanford's connection class is an extesion of the connection class provided by
2
+ # Sanford-Protocol. It provides the main process of reading a request, routing
3
+ # it and writing a response. All requests are benchmarked and logged. The
4
+ # connection's `process` method should always try to return a response, so that
5
+ # clients do not have to timeout.
6
+ #
7
+ # Notes:
8
+ # * This class is separated from `Sanford::Server` to help with thread safety.
9
+ # The server creates a new instance of this class per connection, which means
10
+ # there is a separate connection per thread.
11
+ #
12
+ require 'benchmark'
13
+ require 'sanford-protocol'
14
+
15
+ require 'sanford/exceptions'
16
+
17
+ module Sanford
18
+
19
+ class Connection < Sanford::Protocol::Connection
20
+
21
+ DEFAULT_TIMEOUT = 1
22
+
23
+ attr_reader :service_host, :logger, :exception_handler, :timeout
24
+
25
+ def initialize(service_host, client_socket)
26
+ @service_host = service_host
27
+ @exception_handler = self.service_host.exception_handler
28
+ @logger = self.service_host.logger
29
+ @timeout = (ENV['SANFORD_TIMEOUT'] || DEFAULT_TIMEOUT).to_f
30
+ super(client_socket)
31
+ end
32
+
33
+ def process
34
+ response = nil
35
+ self.logger.info("Received request")
36
+ benchmark = Benchmark.measure do
37
+ begin
38
+ request = Sanford::Protocol::Request.parse(self.read(self.timeout))
39
+ self.log_request(request)
40
+ response = Sanford::Protocol::Response.new(*self.run(request))
41
+ rescue Exception => exception
42
+ handler = self.exception_handler.new(exception, self.logger)
43
+ response = handler.response
44
+ ensure
45
+ self.write(response.to_hash)
46
+ end
47
+ end
48
+ time_taken = self.round_time(benchmark.real)
49
+ self.logger.info("Completed in #{time_taken}ms #{response.status}\n")
50
+ end
51
+
52
+ protected
53
+
54
+ def run(request)
55
+ self.service_host.run(request)
56
+ end
57
+
58
+ def log_request(request)
59
+ self.logger.info(" Version: #{request.version.inspect}")
60
+ self.logger.info(" Service: #{request.name.inspect}")
61
+ self.logger.info(" Parameters: #{request.params.inspect}")
62
+ end
63
+
64
+ def round_time(time_in_seconds)
65
+ ((time_in_seconds * 1000.to_f) + 0.5).to_i
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,43 @@
1
+ # Sanford's exception handler class takes an exception and builds a valid
2
+ # response. For certain exceptions, Sanford will use special response codes and
3
+ # for all others it will classify them as generic error requests.
4
+ #
5
+ require 'sanford-protocol'
6
+
7
+ require 'sanford/exceptions'
8
+
9
+ module Sanford
10
+
11
+ class ExceptionHandler
12
+ attr_reader :exception, :logger
13
+
14
+ def initialize(exception, logger)
15
+ @exception = exception
16
+ @logger = logger
17
+ end
18
+
19
+ def response
20
+ self.logger.error("#{exception.class}: #{exception.message}")
21
+ self.logger.error(exception.backtrace.join("\n"))
22
+ status = Sanford::Protocol::ResponseStatus.new(*self.determine_code_and_message)
23
+ Sanford::Protocol::Response.new(status)
24
+ end
25
+
26
+ protected
27
+
28
+ def determine_code_and_message
29
+ case(self.exception)
30
+ when Sanford::Protocol::BadMessageError, Sanford::Protocol::BadRequestError
31
+ [ :bad_request, self.exception.message ]
32
+ when Sanford::NotFoundError
33
+ [ :not_found ]
34
+ when Sanford::Protocol::TimeoutError
35
+ [ :timeout ]
36
+ when Exception
37
+ [ :error, "An unexpected error occurred." ]
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,37 @@
1
+ module Sanford
2
+
3
+ class BaseError < RuntimeError; end
4
+
5
+ class NotFoundError < BaseError; end
6
+
7
+ class NoHostError < BaseError
8
+ attr_reader :message
9
+
10
+ def initialize(host_name)
11
+ @message = if Sanford.config.hosts.empty?
12
+ "No hosts have been defined. " \
13
+ "Please define a host before trying to run Sanford."
14
+ else
15
+ "A host couldn't be found with the name #{host_name.inspect}. "
16
+ end
17
+ end
18
+ end
19
+
20
+ class InvalidHostError < BaseError
21
+ attr_reader :message
22
+
23
+ def initialize(host)
24
+ @message = "A port must be configured or provided to build an instance of '#{host}'"
25
+ end
26
+ end
27
+
28
+ class NoHandlerClassError < BaseError
29
+ attr_reader :message
30
+
31
+ def initialize(host, handler_class_name)
32
+ @message = "Sanford couldn't find the service handler '#{handler_class_name}'." \
33
+ "It doesn't exist or hasn't been required in yet."
34
+ end
35
+ end
36
+
37
+ end