sanford 0.1.0 → 0.2.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.
Files changed (43) hide show
  1. data/bench/report.txt +34 -4
  2. data/bench/runner.rb +122 -1
  3. data/bench/services.rb +5 -2
  4. data/lib/sanford/error_handler.rb +60 -0
  5. data/lib/sanford/exceptions.rb +11 -10
  6. data/lib/sanford/host.rb +79 -101
  7. data/lib/sanford/host_data.rb +55 -0
  8. data/lib/sanford/logger.rb +23 -0
  9. data/lib/sanford/manager.rb +13 -22
  10. data/lib/sanford/rake.rb +1 -0
  11. data/lib/sanford/runner.rb +50 -0
  12. data/lib/sanford/server.rb +31 -15
  13. data/lib/sanford/service_handler.rb +34 -43
  14. data/lib/sanford/test_runner.rb +47 -0
  15. data/lib/sanford/version.rb +1 -1
  16. data/lib/sanford/worker.rb +124 -0
  17. data/lib/sanford.rb +49 -6
  18. data/sanford.gemspec +1 -1
  19. data/test/helper.rb +1 -0
  20. data/test/support/fake_connection.rb +18 -0
  21. data/test/support/helpers.rb +6 -10
  22. data/test/support/service_handlers.rb +56 -68
  23. data/test/support/services.rb +55 -10
  24. data/test/system/managing_test.rb +18 -18
  25. data/test/system/request_handling_test.rb +10 -100
  26. data/test/unit/config_test.rb +1 -43
  27. data/test/unit/error_handler_test.rb +133 -0
  28. data/test/unit/host_configuration_test.rb +41 -0
  29. data/test/unit/host_data_test.rb +65 -0
  30. data/test/unit/host_test.rb +20 -112
  31. data/test/unit/{host/version_group_test.rb → host_version_group_test.rb} +0 -0
  32. data/test/unit/hosts_test.rb +56 -0
  33. data/test/unit/manager_test.rb +3 -3
  34. data/test/unit/runner_test.rb +26 -0
  35. data/test/unit/server_test.rb +10 -2
  36. data/test/unit/service_handler_test.rb +126 -115
  37. data/test/unit/worker_test.rb +195 -0
  38. metadata +28 -16
  39. data/lib/sanford/config.rb +0 -33
  40. data/lib/sanford/connection.rb +0 -70
  41. data/lib/sanford/exception_handler.rb +0 -43
  42. data/test/unit/connection_test.rb +0 -23
  43. data/test/unit/exception_handler_test.rb +0 -69
@@ -0,0 +1,50 @@
1
+ require 'ostruct'
2
+ require 'sanford-protocol'
3
+
4
+ require 'sanford/logger'
5
+
6
+ module Sanford
7
+
8
+ class Runner
9
+
10
+ ResponseArgs = Struct.new(:status, :data)
11
+
12
+ attr_reader :handler_class, :request, :logger
13
+
14
+ def initialize(handler_class, request, logger = nil)
15
+ @handler_class, @request = handler_class, request
16
+ @logger = logger || Sanford::NullLogger.new
17
+ @handler = @handler_class.new(self)
18
+ end
19
+
20
+ def run
21
+ response_args = catch_halt do
22
+ @handler.init
23
+ @handler.run
24
+ end
25
+ Sanford::Protocol::Response.new(response_args.status, response_args.data)
26
+ end
27
+
28
+ module HaltMethods
29
+
30
+ # It's best to keep what `halt` and `catch_halt` return in the same format.
31
+ # Currently this is a `ResponseArgs` object. This is so no matter how the
32
+ # block returns (either by throwing or running normally), you get the same
33
+ # thing kind of object.
34
+
35
+ def halt(status, options = nil)
36
+ options = OpenStruct.new(options || {})
37
+ response_status = [ status, options.message ]
38
+ throw :halt, ResponseArgs.new(response_status, options.data)
39
+ end
40
+
41
+ def catch_halt(&block)
42
+ catch(:halt){ ResponseArgs.new(*block.call) }
43
+ end
44
+
45
+ end
46
+ include HaltMethods
47
+
48
+ end
49
+
50
+ end
@@ -1,37 +1,53 @@
1
- # Sanford's server uses DatTCP for a TCP Server. When a client connects, the
2
- # `serve` method is called. Sanford creates a new instance of a connection
3
- # handler and hands it the service host and client socket. This is because the
4
- # `serve` method can be accessed by multiple threads, so we essentially create a
5
- # new connection handler per thread.
6
- #
7
1
  require 'dat-tcp'
2
+ require 'sanford-protocol'
8
3
 
9
- require 'sanford/connection'
4
+ require 'sanford/host_data'
5
+ require 'sanford/worker'
10
6
 
11
7
  module Sanford
12
8
 
13
9
  class Server
14
10
  include DatTCP::Server
15
11
 
16
- attr_reader :service_host
12
+ attr_reader :host_data
17
13
 
18
- def initialize(service_host, options = {})
19
- @service_host = service_host
20
- super(self.service_host.ip, self.service_host.port, options)
14
+ def initialize(host, options = {})
15
+ @host_data = host.kind_of?(Sanford::HostData) ? host : Sanford::HostData.new(host)
16
+ super(@host_data.ip, @host_data.port, options)
21
17
  end
22
18
 
23
19
  def name
24
- self.service_host.name
20
+ @host_data.name
25
21
  end
26
22
 
23
+ # `serve` can be called at the same time by multiple threads. Thus we create
24
+ # a new instance of the handler for every request.
27
25
  def serve(socket)
28
- connection = Sanford::Connection.new(self.service_host, socket)
29
- connection.process
26
+ Sanford::Worker.new(@host_data, Connection.new(socket)).run
30
27
  end
31
28
 
32
29
  def inspect
33
30
  reference = '0x0%x' % (self.object_id << 1)
34
- "#<#{self.class}:#{reference} @service_host=#{self.service_host.inspect}>"
31
+ "#<#{self.class}:#{reference} @service_host=#{@host_data.inspect}>"
32
+ end
33
+
34
+ class Connection
35
+
36
+ DEFAULT_TIMEOUT = 1
37
+
38
+ def initialize(socket)
39
+ @connection = Sanford::Protocol::Connection.new(socket)
40
+ @timeout = (ENV['SANFORD_TIMEOUT'] || DEFAULT_TIMEOUT).to_f
41
+ end
42
+
43
+ def read_data
44
+ @connection.read(@timeout)
45
+ end
46
+
47
+ def write_data(data)
48
+ @connection.write data
49
+ end
50
+
35
51
  end
36
52
 
37
53
  end
@@ -1,4 +1,3 @@
1
- require 'ostruct'
2
1
  require 'sanford-protocol'
3
2
 
4
3
  module Sanford
@@ -15,77 +14,69 @@ module Sanford
15
14
  false
16
15
  end
17
16
 
18
- attr_reader :logger, :request
19
-
20
- def initialize(logger, request)
21
- @logger = logger
22
- @request = request
17
+ def initialize(runner)
18
+ @sanford_runner = runner
23
19
  end
24
20
 
25
21
  def init
22
+ self.run_callback 'before_init'
26
23
  self.init!
24
+ self.run_callback 'after_init'
27
25
  end
28
26
 
29
27
  def init!
30
28
  end
31
29
 
32
- # This method has very specific handling when before/after callbacks halt.
33
- # It should always return a response tuple: `[ status, data ]`
34
- # * If `before_run` halts, then the handler is not 'run' (it's `init` and
35
- # `run` methods are not called) and it's response tuple is returned.
36
- # * If `after_run` halts, then it's response tuple is returned, even if
37
- # calling `before_run` or 'running' the handler generated a response
38
- # tuple.
39
- # * If `before_run` and `after_run` do not halt, then the response tuple
40
- # from 'running' is used.
41
30
  def run
42
- response_tuple = self.run_callback 'before_run'
43
- response_tuple ||= catch(:halt) do
44
- self.init
45
- data = self.run!
46
- [ 200, data ]
47
- end
48
- after_response_tuple = self.run_callback 'after_run'
49
- (response_tuple = after_response_tuple) if after_response_tuple
50
- response_tuple
31
+ self.run_callback 'before_run'
32
+ data = self.run!
33
+ self.run_callback 'after_run'
34
+ [ 200, data ]
51
35
  end
52
36
 
53
37
  def run!
54
38
  raise NotImplementedError
55
39
  end
56
40
 
41
+ def inspect
42
+ reference = '0x0%x' % (self.object_id << 1)
43
+ "#<#{self.class}:#{reference} @request=#{self.request.inspect}>"
44
+ end
45
+
46
+ protected
47
+
48
+ def before_init
49
+ end
50
+
51
+ def after_init
52
+ end
53
+
57
54
  def before_run
58
55
  end
59
56
 
60
57
  def after_run
61
58
  end
62
59
 
63
- def params
64
- self.request.params
60
+ # Helpers
61
+
62
+ def halt(*args)
63
+ @sanford_runner.halt(*args)
65
64
  end
66
65
 
67
- def inspect
68
- reference = '0x0%x' % (self.object_id << 1)
69
- "#<#{self.class}:#{reference} @request=#{self.request.inspect}>"
66
+ def request
67
+ @sanford_runner.request
70
68
  end
71
69
 
72
- protected
70
+ def params
71
+ self.request.params
72
+ end
73
73
 
74
- def halt(status, options = nil)
75
- options = OpenStruct.new(options || {})
76
- response_status = [ status, options.message ]
77
- throw(:halt, [ response_status, options.data ])
74
+ def logger
75
+ @sanford_runner.logger
78
76
  end
79
77
 
80
- # Notes:
81
- # * Callbacks need to catch :halt incase the halt method is called. They
82
- # also need to be sure to return nil if nothing is thrown, so that it
83
- # is not considered as a response.
84
- def run_callback(name)
85
- catch(:halt) do
86
- self.send(name.to_s)
87
- nil
88
- end
78
+ def run_callback(callback)
79
+ self.send(callback.to_s)
89
80
  end
90
81
 
91
82
  end
@@ -0,0 +1,47 @@
1
+ require 'sanford-protocol'
2
+
3
+ require 'sanford/logger'
4
+ require 'sanford/runner'
5
+
6
+ module Sanford
7
+
8
+ class TestRunner
9
+ include Sanford::Runner::HaltMethods
10
+
11
+ attr_reader :handler, :response, :request, :logger
12
+
13
+ def initialize(handler_class, params = {}, logger = nil)
14
+ @handler_class = handler_class
15
+ @request = params.kind_of?(Sanford::Protocol::Request) ? params : test_request(params)
16
+ @logger = logger || Sanford::NullLogger.new
17
+
18
+ @handler = @handler_class.new(self)
19
+ @response = build_response catch(:halt){ @handler.init; nil }
20
+ end
21
+
22
+ def run
23
+ @response ||= build_response catch_halt{ @handler.run }
24
+ end
25
+
26
+ protected
27
+
28
+ def test_request(params)
29
+ Sanford::Protocol::Request.new('test_version', 'test_service', params)
30
+ end
31
+
32
+ def build_response(response_args)
33
+ Sanford::Protocol::Response.new(response_args.status, response_args.data) if response_args
34
+ end
35
+
36
+ module Helpers
37
+ module_function
38
+
39
+ def test_runner(*args)
40
+ TestRunner.new(*args)
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -1,3 +1,3 @@
1
1
  module Sanford
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -0,0 +1,124 @@
1
+ require 'benchmark'
2
+ require 'sanford-protocol'
3
+
4
+ require 'sanford/error_handler'
5
+ require 'sanford/logger'
6
+ require 'sanford/runner'
7
+
8
+ module Sanford
9
+
10
+ class Worker
11
+
12
+ ProcessedService = Struct.new(:request, :handler_class, :response, :exception, :time_taken)
13
+
14
+ attr_reader :logger
15
+
16
+ def initialize(host_data, connection)
17
+ @host_data, @connection = host_data, connection
18
+
19
+ @logger = Sanford::Logger.new(@host_data.logger, @host_data.verbose)
20
+ end
21
+
22
+ def run
23
+ processed_service = nil
24
+ self.log_received
25
+ benchmark = Benchmark.measure do
26
+ processed_service = self.run!
27
+ end
28
+ processed_service.time_taken = self.round_time(benchmark.real)
29
+ self.log_complete(processed_service)
30
+ self.raise_if_debugging!(processed_service.exception)
31
+ processed_service
32
+ end
33
+
34
+ protected
35
+
36
+ def run!
37
+ request, handler_class, response, exception = nil, nil, nil, nil
38
+ begin
39
+ request = Sanford::Protocol::Request.parse(@connection.read_data)
40
+ self.log_request(request)
41
+ handler_class = @host_data.handler_class_for(request.version, request.name)
42
+ self.log_handler_class(handler_class)
43
+ response = Sanford::Runner.new(handler_class, request, @host_data.logger).run
44
+ rescue Exception => exception
45
+ error_handler = Sanford::ErrorHandler.new(exception, @host_data, request)
46
+ response = error_handler.run
47
+ self.log_exception(error_handler.exception)
48
+ ensure
49
+ @connection.write_data response.to_hash
50
+ end
51
+ ProcessedService.new(request, handler_class, response, exception)
52
+ end
53
+
54
+ def raise_if_debugging!(exception)
55
+ raise exception if exception && ENV['SANFORD_DEBUG']
56
+ end
57
+
58
+ def log_received
59
+ self.logger.verbose.info("Received request")
60
+ end
61
+
62
+ def log_request(request)
63
+ self.logger.verbose.info(" Version: #{request.version.inspect}")
64
+ self.logger.verbose.info(" Service: #{request.name.inspect}")
65
+ self.logger.verbose.info(" Params: #{request.params.inspect}")
66
+ end
67
+
68
+ def log_handler_class(handler_class)
69
+ self.logger.verbose.info(" Handler: #{handler_class}")
70
+ end
71
+
72
+ def log_complete(processed_service)
73
+ self.logger.verbose.info "Completed in #{processed_service.time_taken}ms " \
74
+ "#{processed_service.response.status}\n"
75
+ self.logger.summary.info self.summary_line(processed_service).to_s
76
+ end
77
+
78
+ def log_exception(exception)
79
+ self.logger.verbose.error("#{exception.class}: #{exception.message}")
80
+ self.logger.verbose.error(exception.backtrace.join("\n"))
81
+ end
82
+
83
+ def summary_line(processed_service)
84
+ SummaryLine.new.tap do |line|
85
+ if (request = processed_service.request)
86
+ line.add 'version', request.version
87
+ line.add 'service', request.name
88
+ line.add 'params', request.params
89
+ end
90
+ line.add 'handler', processed_service.handler_class
91
+ line.add 'status', processed_service.response.status.code if processed_service.response
92
+ line.add 'duration', processed_service.time_taken
93
+ end
94
+ end
95
+
96
+ ROUND_PRECISION = 2
97
+ ROUND_MODIFIER = 10 ** ROUND_PRECISION
98
+ def round_time(time_in_seconds)
99
+ (time_in_seconds * 1000 * ROUND_MODIFIER).to_i / ROUND_MODIFIER.to_f
100
+ end
101
+
102
+ class SummaryLine
103
+
104
+ def initialize
105
+ @hash = {}
106
+ end
107
+
108
+ def add(key, value)
109
+ @hash[key] = value.inspect if value
110
+ end
111
+
112
+ # change the key's order in the array to change the order to change the
113
+ # order they appear in when logged
114
+ def to_s
115
+ [ 'version', 'service', 'handler', 'status', 'duration', 'params' ].map do |key|
116
+ "#{key}=#{@hash[key]}" if @hash[key]
117
+ end.compact.join(" ")
118
+ end
119
+
120
+ end
121
+
122
+ end
123
+
124
+ end
data/lib/sanford.rb CHANGED
@@ -1,12 +1,26 @@
1
1
  module Sanford; end
2
2
 
3
- require 'sanford/config'
4
- require 'sanford/manager'
3
+ require 'ns-options'
4
+ require 'pathname'
5
+ require 'set'
6
+
5
7
  require 'sanford/host'
8
+ require 'sanford/server'
9
+ require 'sanford/service_handler'
6
10
  require 'sanford/version'
7
11
 
12
+ ENV['SANFORD_SERVICES_CONFIG'] ||= 'config/services'
13
+
8
14
  module Sanford
9
15
 
16
+ def self.register(host)
17
+ @hosts.add(host)
18
+ end
19
+
20
+ def self.hosts
21
+ @hosts
22
+ end
23
+
10
24
  def self.config
11
25
  Sanford::Config
12
26
  end
@@ -17,14 +31,43 @@ module Sanford
17
31
  end
18
32
 
19
33
  def self.init
34
+ @hosts ||= Hosts.new
20
35
  require self.config.services_config
21
36
  end
22
37
 
23
- class NullLogger
24
- require 'logger'
38
+ module Config
39
+ include NsOptions::Proxy
40
+
41
+ option :services_config, Pathname, :default => ENV['SANFORD_SERVICES_CONFIG']
42
+
43
+ end
44
+
45
+ class Hosts
46
+
47
+ def initialize(values = [])
48
+ @set = Set.new(values)
49
+ end
50
+
51
+ # We want class names to take precedence over a configured name, so that if
52
+ # a user specifies a specific class, they always get it
53
+ def find(name)
54
+ self.find_by_class_name(name) || self.find_by_name(name)
55
+ end
56
+
57
+ def find_by_class_name(class_name)
58
+ @set.detect{|host_class| host_class.to_s == class_name.to_s }
59
+ end
60
+
61
+ def find_by_name(name)
62
+ @set.detect{|host_class| host_class.name == name.to_s }
63
+ end
64
+
65
+ def method_missing(method, *args, &block)
66
+ @set.send(method, *args, &block)
67
+ end
25
68
 
26
- Logger::Severity.constants.each do |name|
27
- define_method(name.downcase){|*args| } # no-op
69
+ def respond_to?(method)
70
+ super || @set.respond_to?(method)
28
71
  end
29
72
 
30
73
  end
data/sanford.gemspec CHANGED
@@ -19,7 +19,7 @@ Gem::Specification.new do |gem|
19
19
 
20
20
  gem.add_dependency("daemons", ["~>1.1"])
21
21
  gem.add_dependency("dat-tcp", ["~>0.1"])
22
- gem.add_dependency("ns-options", ["~>1.0.0"])
22
+ gem.add_dependency("ns-options", ["~>1.0"])
23
23
  gem.add_dependency("sanford-protocol", ["~>0.5"])
24
24
 
25
25
  gem.add_development_dependency("assert", ["~>1.0"])
data/test/helper.rb CHANGED
@@ -11,6 +11,7 @@ Sanford.configure do |config|
11
11
  end
12
12
  Sanford.init
13
13
 
14
+ require 'test/support/fake_connection'
14
15
  require 'test/support/service_handlers'
15
16
  require 'test/support/simple_client'
16
17
  require 'test/support/helpers'
@@ -0,0 +1,18 @@
1
+ class FakeConnection
2
+
3
+ attr_reader :read_data, :response
4
+
5
+ def self.with_request(version, name, params = {})
6
+ request = Sanford::Protocol::Request.new(version, name, params)
7
+ self.new(request.to_hash)
8
+ end
9
+
10
+ def initialize(read_data)
11
+ @read_data = read_data
12
+ end
13
+
14
+ def write_data(data)
15
+ @response = Sanford::Protocol::Response.parse(data)
16
+ end
17
+
18
+ end
@@ -3,19 +3,17 @@ module Test
3
3
  module Environment
4
4
 
5
5
  def self.store_and_clear_hosts
6
- @previous_hosts = Sanford.config.hosts.dup
7
- Sanford.config.hosts.clear
6
+ @previous_hosts = Sanford.hosts.instance_variable_get("@set").dup
7
+ Sanford.hosts.clear
8
8
  end
9
9
 
10
10
  def self.restore_hosts
11
- Sanford.config.hosts = @previous_hosts
11
+ Sanford.instance_variable_set("@hosts", Sanford::Hosts.new(@previous_hosts))
12
12
  @previous_hosts = nil
13
13
  end
14
14
 
15
15
  end
16
16
 
17
-
18
-
19
17
  module ForkServerHelper
20
18
 
21
19
  def start_server(server, &block)
@@ -37,18 +35,16 @@ module Test
37
35
 
38
36
  end
39
37
 
40
-
41
-
42
38
  module ForkManagerHelper
43
39
 
44
40
  # start a Sanford server using Sanford's manager in a forked process
45
41
  def call_sanford_manager(*args, &block)
46
42
  pid = fork do
47
- STDOUT.reopen('/dev/null')
43
+ STDOUT.reopen('/dev/null') unless ENV['SANFORD_DEBUG']
48
44
  trap("TERM"){ exit }
49
45
  Sanford::Manager.call(*args)
50
46
  end
51
- sleep 1 # give time for the command to run
47
+ sleep 1.5 # give time for the command to run
52
48
  yield
53
49
  ensure
54
50
  if pid
@@ -64,7 +60,7 @@ module Test
64
60
  end
65
61
 
66
62
  def expected_pid_file(host, ip, port)
67
- host.config.pid_dir.join("#{ip}_#{port}_#{host}.pid")
63
+ host.pid_dir.join("#{ip}_#{port}_#{host}.pid")
68
64
  end
69
65
 
70
66
  end