sanford 0.1.0 → 0.2.0

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