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.
@@ -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