sanford 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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