sanford 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,162 @@
1
+ # Sanford's Host mixin is used to define service hosts. When mixed into a class
2
+ # it provides the interface for configuring the service host and for adding
3
+ # versioned services. It also contains the logic for routing a request to a
4
+ # a service handler.
5
+ #
6
+ # Options:
7
+ # * `name` - A string for naming this host. This can be used when specifying
8
+ # a host with the rake tasks and will be used to name the PID
9
+ # file. Defaults to the class's name.
10
+ # * `ip` - The string for the ip that the TCP Server should bind to. This
11
+ # defaults to '0.0.0.0'.
12
+ # * `port` - The integer for the port that the TCP Server should bind to.
13
+ # This isn't defaulted and must be provided.
14
+ # * `pid_dir` - The directory to write the PID file to. This is defaulted to
15
+ # Dir.pwd.
16
+ # * `logger` - The logger to use if the Sanford server logs messages. This is
17
+ # defaulted to an instance of Ruby's Logger.
18
+ #
19
+ require 'logger'
20
+ require 'ns-options'
21
+ require 'pathname'
22
+
23
+ require 'sanford/config'
24
+ require 'sanford/exception_handler'
25
+ require 'sanford/exceptions'
26
+ require 'sanford/service_handler'
27
+
28
+ module Sanford
29
+ module Host
30
+
31
+ # Notes:
32
+ # * When Host is included on a class, it needs to mixin NsOptions and define
33
+ # the options directly on the class (instead of on the Host module).
34
+ # Otherwise, NsOptions will not work correctly for the class.
35
+ def self.included(host_class)
36
+ host_class.class_eval do
37
+ include NsOptions
38
+ extend Sanford::Host::Interface
39
+
40
+ options :config do
41
+ option :name, String, :default => host_class.to_s
42
+ option :ip, String, :default => '0.0.0.0'
43
+ option :port, Integer
44
+ option :pid_dir, Pathname, :default => Dir.pwd
45
+ option :logger, :default => proc{ Sanford::NullLogger.new }
46
+
47
+ option :exception_handler, :default => Sanford::ExceptionHandler
48
+ option :versioned_services, Hash, :default => {}
49
+ end
50
+ end
51
+ Sanford.config.hosts.add(host_class)
52
+ end
53
+
54
+ INTERFACE_OPTIONS = [ :name, :ip, :port, :pid_dir, :logger, :exception_handler ]
55
+
56
+ # Notes:
57
+ # * The `initialize` takes the values configured on the class and merges
58
+ # the passed in options. This is used to set the individual instance's
59
+ # configuration (which allows overwriting options like the port).
60
+ def initialize(options = nil)
61
+ options = self.remove_nil_values(options)
62
+ config_options = self.class.config.to_hash.merge(options)
63
+ self.config.apply(config_options)
64
+ raise(Sanford::InvalidHostError.new(self.class)) if !self.port
65
+ end
66
+
67
+ INTERFACE_OPTIONS.each do |name|
68
+
69
+ define_method(name) do
70
+ self.config.send(name)
71
+ end
72
+
73
+ end
74
+
75
+ def run(request)
76
+ request_handler(request).run
77
+ end
78
+
79
+ def inspect
80
+ reference = '0x0%x' % (self.object_id << 1)
81
+ "#<#{self.class}:#{reference} ip=#{self.config.ip.inspect} " \
82
+ "port=#{self.config.port.inspect}>"
83
+ end
84
+
85
+ protected
86
+
87
+ def request_handler(request)
88
+ handler_class(get_handler_class_name(request)).new(self.logger, request)
89
+ end
90
+
91
+ def handler_class(class_name_str)
92
+ self.logger.info(" Handler: #{class_name_str.inspect}")
93
+ Sanford::ServiceHandler.constantize(class_name_str).tap do |handler_class|
94
+ raise Sanford::NoHandlerClassError.new(self, class_name_str) if !handler_class
95
+ end
96
+ end
97
+
98
+ def get_handler_class_name(request)
99
+ services = self.config.versioned_services[request.version] || {}
100
+ services[request.name].tap do |name|
101
+ raise Sanford::NotFoundError if !name
102
+ end
103
+ end
104
+
105
+ def remove_nil_values(options)
106
+ (options || {}).inject({}) do |hash, (k, v)|
107
+ hash.merge!({ k => v }) if !v.nil?
108
+ hash
109
+ end
110
+ end
111
+
112
+ module Interface
113
+
114
+ INTERFACE_OPTIONS.each do |name|
115
+
116
+ define_method(name) do |*args|
117
+ self.config.send("#{name}=", *args) if !args.empty?
118
+ self.config.send(name)
119
+ end
120
+
121
+ define_method("#{name}=") do |new_value|
122
+ self.config.send("#{name}=", new_value)
123
+ end
124
+
125
+ end
126
+
127
+ def version(name, &block)
128
+ version_group = Sanford::Host::VersionGroup.new(name, &block)
129
+ self.config.versioned_services.merge!(version_group.to_hash)
130
+ end
131
+
132
+ end
133
+
134
+ class VersionGroup
135
+ attr_reader :name, :services
136
+
137
+ def initialize(name, &definition_block)
138
+ @name = name
139
+ @services = {}
140
+ self.instance_eval(&definition_block)
141
+ end
142
+
143
+ def service_handler_ns(value = nil)
144
+ @service_handler_ns = value if value
145
+ @service_handler_ns
146
+ end
147
+
148
+ def service(service_name, handler_class_name)
149
+ if self.service_handler_ns && !(handler_class_name =~ /^::/)
150
+ handler_class_name = "#{self.service_handler_ns}::#{handler_class_name}"
151
+ end
152
+ @services[service_name] = handler_class_name
153
+ end
154
+
155
+ def to_hash
156
+ { self.name => self.services }
157
+ end
158
+
159
+ end
160
+
161
+ end
162
+ end
@@ -0,0 +1,58 @@
1
+ # The Manager class is responsible for managing sanford's server process. Given
2
+ # a host, it can start and stop the host's server process. This is done using
3
+ # the Daemons gem and `run_proc`. The class provides a convenience method on the
4
+ # class called `call`, which will find a host, build a new manager and call the
5
+ # relevant action (this is what the rake tasks use).
6
+ #
7
+ require 'daemons'
8
+
9
+ require 'sanford/config'
10
+ require 'sanford/exceptions'
11
+ require 'sanford/server'
12
+
13
+ module Sanford
14
+
15
+ class Manager
16
+ attr_reader :host, :process_name
17
+
18
+ def self.call(action, options = nil)
19
+ options ||= {}
20
+ options[:host] ||= ENV['SANFORD_HOST']
21
+ options[:ip] ||= ENV['SANFORD_IP']
22
+ options[:port] ||= ENV['SANFORD_PORT']
23
+
24
+ host_class = if (host_class_or_name = options.delete(:host))
25
+ Sanford.config.find_host(host_class_or_name)
26
+ else
27
+ Sanford.config.hosts.first
28
+ end
29
+ raise(Sanford::NoHostError.new(host_class_or_name)) if !host_class
30
+ self.new(host_class, options).call(action)
31
+ end
32
+
33
+ def initialize(host_class, options = {})
34
+ @host = host_class.new(options)
35
+ @process_name = [ self.host.ip, self.host.port, self.host.name ].join('_')
36
+ end
37
+
38
+ def call(action)
39
+ options = self.default_options.merge({ :ARGV => [ action.to_s ] })
40
+ FileUtils.mkdir_p(options[:dir])
41
+ ::Daemons.run_proc(self.process_name, options) do
42
+ server = Sanford::Server.new(self.host)
43
+ server.start
44
+ server.join_thread
45
+ end
46
+ end
47
+
48
+ protected
49
+
50
+ def default_options
51
+ { :dir_mode => :normal,
52
+ :dir => self.host.pid_dir
53
+ }
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,45 @@
1
+ module Sanford::Rake
2
+
3
+ class Tasks
4
+ extend ::Rake::DSL
5
+
6
+ def self.load
7
+ namespace :sanford do
8
+
9
+ # Overwrite this to load your application's environment so that it can
10
+ # be used with Sanford
11
+ task :setup
12
+
13
+ task :load_manager => :setup do
14
+ require 'sanford'
15
+ Sanford.init
16
+ end
17
+
18
+ desc "Start a Sanford server and daemonize the process"
19
+ task :start => :load_manager do
20
+ Sanford::Manager.call :start
21
+ end
22
+
23
+ desc "Stop a daemonized Sanford server process"
24
+ task :stop => :load_manager do
25
+ Sanford::Manager.call :stop
26
+ end
27
+
28
+ desc "Restart a daemonized Sanford server process"
29
+ task :restart => :load_manager do
30
+ Sanford::Manager.call :restart
31
+ end
32
+
33
+ desc "Run a Sanford server (not daemonized)"
34
+ task :run => :load_manager do
35
+ Sanford::Manager.call :run
36
+ end
37
+
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+
45
+ Sanford::Rake::Tasks.load
@@ -0,0 +1,39 @@
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
+ require 'dat-tcp'
8
+
9
+ require 'sanford/connection'
10
+
11
+ module Sanford
12
+
13
+ class Server
14
+ include DatTCP::Server
15
+
16
+ attr_reader :service_host
17
+
18
+ def initialize(service_host, options = {})
19
+ @service_host = service_host
20
+ super(self.service_host.ip, self.service_host.port, options)
21
+ end
22
+
23
+ def name
24
+ self.service_host.name
25
+ end
26
+
27
+ def serve(socket)
28
+ connection = Sanford::Connection.new(self.service_host, socket)
29
+ connection.process
30
+ end
31
+
32
+ def inspect
33
+ reference = '0x0%x' % (self.object_id << 1)
34
+ "#<#{self.class}:#{reference} @service_host=#{self.service_host.inspect}>"
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,93 @@
1
+ require 'ostruct'
2
+ require 'sanford-protocol'
3
+
4
+ module Sanford
5
+
6
+ module ServiceHandler
7
+
8
+ def self.constantize(class_name)
9
+ names = class_name.to_s.split('::').reject{|name| name.empty? }
10
+ klass = names.inject(Object) do |constant, name|
11
+ constant.const_get(name)
12
+ end
13
+ klass == Object ? false : klass
14
+ rescue NameError
15
+ false
16
+ end
17
+
18
+ attr_reader :logger, :request
19
+
20
+ def initialize(logger, request)
21
+ @logger = logger
22
+ @request = request
23
+ end
24
+
25
+ def init
26
+ self.init!
27
+ end
28
+
29
+ def init!
30
+ end
31
+
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
+ 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
51
+ end
52
+
53
+ def run!
54
+ raise NotImplementedError
55
+ end
56
+
57
+ def before_run
58
+ end
59
+
60
+ def after_run
61
+ end
62
+
63
+ def params
64
+ self.request.params
65
+ end
66
+
67
+ def inspect
68
+ reference = '0x0%x' % (self.object_id << 1)
69
+ "#<#{self.class}:#{reference} @request=#{self.request.inspect}>"
70
+ end
71
+
72
+ protected
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 ])
78
+ end
79
+
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
89
+ end
90
+
91
+ end
92
+
93
+ end
@@ -0,0 +1,3 @@
1
+ module Sanford
2
+ VERSION = "0.1.0"
3
+ end
data/lib/sanford.rb ADDED
@@ -0,0 +1,32 @@
1
+ module Sanford; end
2
+
3
+ require 'sanford/config'
4
+ require 'sanford/manager'
5
+ require 'sanford/host'
6
+ require 'sanford/version'
7
+
8
+ module Sanford
9
+
10
+ def self.config
11
+ Sanford::Config
12
+ end
13
+
14
+ def self.configure(&block)
15
+ self.config.define(&block)
16
+ self.config
17
+ end
18
+
19
+ def self.init
20
+ require self.config.services_config
21
+ end
22
+
23
+ class NullLogger
24
+ require 'logger'
25
+
26
+ Logger::Severity.constants.each do |name|
27
+ define_method(name.downcase){|*args| } # no-op
28
+ end
29
+
30
+ end
31
+
32
+ end
data/log/.gitkeep ADDED
File without changes
data/sanford.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sanford/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "sanford"
8
+ gem.version = Sanford::VERSION
9
+ gem.authors = ["Collin Redding", "Kelly Redding"]
10
+ gem.email = ["collin.redding@me.com", "kelly@kellyredding.com"]
11
+ gem.description = "Simple hosts for Sanford services."
12
+ gem.summary = "Simple hosts for Sanford services."
13
+ gem.homepage = "https://github.com/redding/sanford"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency("daemons", ["~>1.1"])
21
+ gem.add_dependency("dat-tcp", ["~>0.1"])
22
+ gem.add_dependency("ns-options", ["~>1.0.0"])
23
+ gem.add_dependency("sanford-protocol", ["~>0.5"])
24
+
25
+ gem.add_development_dependency("assert", ["~>1.0"])
26
+ gem.add_development_dependency("assert-mocha", ["~>1.0"])
27
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,20 @@
1
+ ENV['SANFORD_PROTOCOL_DEBUG'] = 'yes'
2
+
3
+ require 'ostruct'
4
+
5
+ ROOT = File.expand_path('../..', __FILE__)
6
+
7
+ require 'sanford'
8
+
9
+ Sanford.configure do |config|
10
+ config.services_config = File.join(ROOT, 'test/support/services')
11
+ end
12
+ Sanford.init
13
+
14
+ require 'test/support/service_handlers'
15
+ require 'test/support/simple_client'
16
+ require 'test/support/helpers'
17
+
18
+ if defined?(Assert)
19
+ require 'assert-mocha'
20
+ end
@@ -0,0 +1,72 @@
1
+ module Test
2
+
3
+ module Environment
4
+
5
+ def self.store_and_clear_hosts
6
+ @previous_hosts = Sanford.config.hosts.dup
7
+ Sanford.config.hosts.clear
8
+ end
9
+
10
+ def self.restore_hosts
11
+ Sanford.config.hosts = @previous_hosts
12
+ @previous_hosts = nil
13
+ end
14
+
15
+ end
16
+
17
+
18
+
19
+ module ForkServerHelper
20
+
21
+ def start_server(server, &block)
22
+ begin
23
+ pid = fork do
24
+ trap("TERM"){ server.stop }
25
+ server.start
26
+ server.join_thread
27
+ end
28
+ sleep 0.3 # Give time for the socket to start listening.
29
+ yield
30
+ ensure
31
+ if pid
32
+ Process.kill("TERM", pid)
33
+ Process.wait(pid)
34
+ end
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+
41
+
42
+ module ForkManagerHelper
43
+
44
+ # start a Sanford server using Sanford's manager in a forked process
45
+ def call_sanford_manager(*args, &block)
46
+ pid = fork do
47
+ STDOUT.reopen('/dev/null')
48
+ trap("TERM"){ exit }
49
+ Sanford::Manager.call(*args)
50
+ end
51
+ sleep 1 # give time for the command to run
52
+ yield
53
+ ensure
54
+ if pid
55
+ Process.kill("TERM", pid)
56
+ Process.wait(pid)
57
+ end
58
+ end
59
+
60
+ def open_socket(host, port)
61
+ socket = TCPSocket.new(host, port)
62
+ ensure
63
+ socket.close rescue false
64
+ end
65
+
66
+ def expected_pid_file(host, ip, port)
67
+ host.config.pid_dir.join("#{ip}_#{port}_#{host}.pid")
68
+ end
69
+
70
+ end
71
+
72
+ end
@@ -0,0 +1,115 @@
1
+ # A bunch of service handler examples. These are defined to implement certain
2
+ # edge cases and are for specific tests within the test suite.
3
+ #
4
+
5
+ class StaticServiceHandler
6
+ include Sanford::ServiceHandler
7
+
8
+ # builds with the same request and logger always, just for convenience
9
+
10
+ def initialize(logger = nil, request = nil)
11
+ request ||= Sanford::Protocol::Request.new('v1', 'name', {})
12
+ super(logger || Sanford::NullLogger.new, request)
13
+ end
14
+
15
+ end
16
+
17
+ class ManualThrowServiceHandler < StaticServiceHandler
18
+
19
+ def run!
20
+ throw(:halt, 'halted!')
21
+ end
22
+
23
+ end
24
+
25
+ class HaltWithServiceHandler < StaticServiceHandler
26
+
27
+ def initialize(halt_with)
28
+ request = Sanford::Protocol::Request.new('v1', 'name', {
29
+ 'halt_with' => halt_with.dup
30
+ })
31
+ super(Sanford::NullLogger.new, request)
32
+ end
33
+
34
+ def run!
35
+ params['halt_with'].tap do |halt_with|
36
+ halt(halt_with.delete('code'), halt_with)
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ class NoopServiceHandler < StaticServiceHandler
43
+
44
+ # simply overwrites the default `run!` so it doesn't error
45
+
46
+ def run!
47
+ # nothing!
48
+ end
49
+
50
+ end
51
+
52
+ class FlaggedServiceHandler < NoopServiceHandler
53
+
54
+ # flags a bunch of methods as they are run by setting instance variables
55
+
56
+ FLAGGED_METHODS = {
57
+ 'init' => :init_called,
58
+ 'init!' => :init_bang_called,
59
+ 'run!' => :run_bang_called,
60
+ 'before_run' => :before_run_called,
61
+ 'after_run' => :after_run_called
62
+ }
63
+ FLAGS = FLAGGED_METHODS.values
64
+
65
+ attr_reader *FLAGS
66
+
67
+ def initialize(*passed)
68
+ super
69
+ FLAGS.each{|name| self.instance_variable_set("@#{name}", false) }
70
+ end
71
+
72
+ FLAGGED_METHODS.each do |method_name, instance_variable_name|
73
+
74
+ # def before_run
75
+ # super
76
+ # @before_run_called = true
77
+ # end
78
+ define_method(method_name) do
79
+ super
80
+ self.instance_variable_set("@#{instance_variable_name}", true)
81
+ end
82
+
83
+ end
84
+
85
+ end
86
+
87
+ class ConfigurableServiceHandler < FlaggedServiceHandler
88
+
89
+ def initialize(options = {})
90
+ @options = options
91
+ super
92
+ end
93
+
94
+ def before_run
95
+ super
96
+ if @options[:before_run]
97
+ self.instance_eval(&@options[:before_run])
98
+ end
99
+ end
100
+
101
+ def run!
102
+ super
103
+ if @options[:run!]
104
+ self.instance_eval(&@options[:run!])
105
+ end
106
+ end
107
+
108
+ def after_run
109
+ super
110
+ if @options[:after_run]
111
+ self.instance_eval(&@options[:after_run])
112
+ end
113
+ end
114
+
115
+ end