sanford 0.4.0 → 0.6.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.
data/README.md CHANGED
@@ -12,7 +12,7 @@ class MyHost
12
12
  include Sanford::Host
13
13
 
14
14
  port 8000
15
- pid_dir '/path/to/pids'
15
+ pid_file '/path/to/host.pid'
16
16
 
17
17
  # define some services
18
18
  version 'v1' do
@@ -39,7 +39,7 @@ To define a Sanford host, include the mixin `Sanford::Host` on a class and use t
39
39
 
40
40
  * `ip` - (string) A hostname or IP address for the server to bind to; default: `'0.0.0.0'`.
41
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`.
42
+ * `pid_file` - (string) Path to where you want the pid file to be written.
43
43
  * `logger`- (logger) A logger for Sanford to use when handling requests; default: `Logger.new`.
44
44
 
45
45
  Any values specified using the DSL act as defaults for instances of the host. You can overwritten when creating new instances:
@@ -118,39 +118,35 @@ end
118
118
 
119
119
  ## Running Host Daemons
120
120
 
121
- Sanford comes with rake tasks for running hosts:
121
+ Sanford comes with a CLI for running hosts:
122
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.
123
+ * `sanford start` - spin up a background process running the host daemon.
124
+ * `sanford stop` - shutdown the background process running the host gracefully.
125
+ * `sanford restart` - "hot restart" the process running the host.
126
+ * `sanford run` - starts the server, but doesn't daemonize it (runs in the current ruby process). Convenient when using the server in a development environment.
127
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.
128
+ The basic commands 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
129
 
136
130
  ```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
131
+ sanford start # starts the first defined host
132
+ SANFORD_HOST=AnotherHost SANFORD_PORT=13001 sanford start # choose a specific host and port to run on with ENV vars
139
133
  ```
140
134
 
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`.
135
+ The CLI allow using environment variables for specifying which host to run the command against and for overriding the host's configuration. They recognize the a number of environment variables, but the main ones are: `SANFORD_HOST`, `SANFORD_IP`, and `SANFORD_PORT`.
136
+
137
+ 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 CLI. If no name is set, Sanford will use the host's class name.
142
138
 
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.
139
+ Alternatively, the CLI supports passing switches to override the host's configuration as well. Use `sanford --help` to see the options that are available.
144
140
 
145
141
  ### Loading An Application
146
142
 
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. to support this, Sanford provides a `setup` hook for hosts. The proc that is defined will be called before the Sanford server is started, properly running the server in your application's environment:
143
+ Typically, a Sanford host is part of a larger application and parts of the application need to be initialized or loaded when you start your Sanford server. To support this, Sanford provides an `init` hook for hosts. The proc that is defined will be called before the Sanford server is started, properly running the server in your application's environment:
148
144
 
149
145
  ```ruby
150
146
  class MyHost
151
147
  include Sanford::Host
152
148
 
153
- setup do
149
+ init do
154
150
  require File.expand_path("../config/environment", __FILE__)
155
151
  end
156
152
 
data/Rakefile CHANGED
@@ -1,7 +1,5 @@
1
1
  require "bundler/gem_tasks"
2
2
 
3
- ENV['SANFORD_SERVICES_FILE'] = 'bench/services'
4
- require 'sanford/rake'
5
3
  require 'bench/tasks'
6
4
 
7
5
  require "assert/rake_tasks"
data/bench/report.txt CHANGED
@@ -2,39 +2,37 @@ Running benchmark report...
2
2
 
3
3
  Hitting "simple" service with {}, 10000 times
4
4
  ....................................................................................................
5
- Total Time: 10515.3759ms
6
- Average Time: 1.0515ms
7
- Min Time: 0.7140ms
8
- Max Time: 114.8610ms
5
+ Total Time: 8960.5727ms
6
+ Average Time: 0.8960ms
7
+ Min Time: 0.5099ms
8
+ Max Time: 87.5380ms
9
9
 
10
10
  Distribution (number of requests):
11
- 0ms: 8614
12
- 0.7ms: 4945
13
- 0.8ms: 2641
14
- 0.9ms: 1028
15
- 1ms: 1339
16
- 1.0ms: 612
17
- 1.1ms: 370
18
- 1.2ms: 172
19
- 1.3ms: 90
20
- 1.4ms: 39
21
- 1.5ms: 19
22
- 1.6ms: 16
23
- 1.7ms: 10
24
- 1.8ms: 7
25
- 1.9ms: 4
26
- 2ms: 15
27
- 3ms: 10
28
- 4ms: 1
29
- 66ms: 1
30
- 86ms: 3
31
- 87ms: 4
32
- 89ms: 2
33
- 90ms: 3
34
- 91ms: 2
35
- 92ms: 2
36
- 93ms: 1
37
- 94ms: 2
38
- 114ms: 1
11
+ 0ms: 9775
12
+ 0.5ms: 5723
13
+ 0.6ms: 2810
14
+ 0.7ms: 733
15
+ 0.8ms: 364
16
+ 0.9ms: 145
17
+ 1ms: 153
18
+ 1.0ms: 57
19
+ 1.1ms: 18
20
+ 1.2ms: 42
21
+ 1.3ms: 13
22
+ 1.4ms: 8
23
+ 1.5ms: 7
24
+ 1.6ms: 2
25
+ 1.7ms: 3
26
+ 1.8ms: 2
27
+ 1.9ms: 1
28
+ 2ms: 16
29
+ 3ms: 1
30
+ 22ms: 1
31
+ 44ms: 10
32
+ 45ms: 25
33
+ 46ms: 9
34
+ 47ms: 4
35
+ 85ms: 3
36
+ 87ms: 3
39
37
 
40
38
  Done running benchmark report
data/bench/runner.rb CHANGED
@@ -7,10 +7,10 @@ module Bench
7
7
 
8
8
  class Runner
9
9
  # this should match up with bench/services host and port
10
- HOST_AND_PORT = [ '127.0.0.1', 12000 ]
10
+ HOST_AND_PORT = [ '127.0.0.1', 59284 ]
11
11
 
12
12
  REQUESTS = [
13
- [ 'v1', 'simple', {}, 10000 ]
13
+ [ 'v1', 'simple', {}, 10000 ]
14
14
  ]
15
15
 
16
16
  TIME_MODIFIER = 10 ** 4 # 4 decimal places
@@ -36,7 +36,7 @@ module Bench
36
36
 
37
37
  output "\nHitting #{name.inspect} service with #{params.inspect}, #{times} times"
38
38
  [*(1..times.to_i)].each do |index|
39
- benchmark = self.hit_service(name, version, params.merge({ :request_number => index }), show_result)
39
+ benchmark = self.hit_service(version, name, params.merge({ :request_number => index }), show_result)
40
40
  benchmarks << self.round_time(benchmark.real * 1000.to_f)
41
41
  output('.', false) if ((index - 1) % 100 == 0) && !show_result
42
42
  end
@@ -72,13 +72,12 @@ module Bench
72
72
  output "\n"
73
73
  end
74
74
 
75
- protected
76
75
 
77
76
  def hit_service(version, name, params, show_result)
78
77
  Benchmark.measure do
79
78
  begin
80
79
  client = Bench::Client.new(*HOST_AND_PORT)
81
- response = client.call(name, version, params)
80
+ response = client.call(version, name, params)
82
81
  if show_result
83
82
  output "Got a response:"
84
83
  output " #{response.status}"
@@ -91,6 +90,8 @@ module Bench
91
90
  end
92
91
  end
93
92
 
93
+ protected
94
+
94
95
  def output(message, puts = true)
95
96
  method = puts ? :puts : :print
96
97
  self.send(method, message)
data/bench/services.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  class BenchHost
2
2
  include Sanford::Host
3
3
 
4
- port 12000
5
- pid_dir File.expand_path("../../tmp", __FILE__)
4
+ port 59284
5
+ pid_file File.expand_path("../../tmp/bench_host.pid", __FILE__)
6
6
 
7
7
  logger Logger.new(STDOUT)
8
8
  verbose_logging false
data/bench/tasks.rb CHANGED
@@ -4,6 +4,29 @@ namespace :bench do
4
4
  require 'bench/runner'
5
5
  end
6
6
 
7
+ namespace :server do
8
+
9
+ task :load do
10
+ ENV['SANFORD_SERVICES_FILE'] = 'bench/services'
11
+ end
12
+
13
+ desc "Run the bench server"
14
+ task :run => :load do
15
+ Kernel.exec("bundle exec sanford run")
16
+ end
17
+
18
+ desc "Start a daemonized bench server"
19
+ task :start => :load do
20
+ Kernel.system("bundle exec sanford start")
21
+ end
22
+
23
+ desc "Stop the bench server"
24
+ task :stop => :load do
25
+ Kernel.system("bundle exec sanford stop")
26
+ end
27
+
28
+ end
29
+
7
30
  desc "Run a Benchmark report against the Benchmark server"
8
31
  task :report => :load do
9
32
  Bench::Runner.new.build_report
data/bin/sanford ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Copyright (c) 2013 Collin Redding & Kelly Redding
4
+ #
5
+
6
+ require 'sanford/cli'
7
+ Sanford::CLI.run ARGV
data/lib/sanford.rb CHANGED
@@ -5,6 +5,7 @@ require 'pathname'
5
5
  require 'set'
6
6
 
7
7
  require 'sanford/host'
8
+ require 'sanford/logger'
8
9
  require 'sanford/server'
9
10
  require 'sanford/service_handler'
10
11
  require 'sanford/version'
@@ -38,7 +39,7 @@ module Sanford
38
39
  module Config
39
40
  include NsOptions::Proxy
40
41
  option :services_file, Pathname, :default => ENV['SANFORD_SERVICES_FILE']
41
-
42
+ option :logger, :default => Sanford::NullLogger.new
42
43
  end
43
44
 
44
45
  class Hosts
@@ -0,0 +1,364 @@
1
+ require 'sanford'
2
+ require 'sanford/host_data'
3
+ require 'sanford/server'
4
+ require 'sanford/version'
5
+
6
+ module Sanford
7
+
8
+ class CLI
9
+
10
+ def self.run(*args)
11
+ self.new.run(*args)
12
+ end
13
+
14
+ def initialize
15
+ @cli = CLIRB.new do
16
+ option :host, "Name of the Host configuration", :value => String
17
+ option :ip, "IP address to bind to", :value => String
18
+ option :port, "Port number to bind to", :value => Integer
19
+ option :config, "File defining the configured Hosts", :value => String
20
+ end
21
+ end
22
+
23
+ def run(*args)
24
+ begin
25
+ @cli.parse!(*args)
26
+ @command = @cli.args.first || 'run'
27
+ Sanford.config.services_file = @cli.opts['config'] if @cli.opts['config']
28
+ Sanford.init
29
+ Sanford::Manager.call(@command, @cli.opts)
30
+ rescue CLIRB::HelpExit
31
+ puts help
32
+ rescue CLIRB::VersionExit
33
+ puts Sanford::VERSION
34
+ rescue CLIRB::Error => exception
35
+ puts "#{exception.message}\n\n"
36
+ puts help
37
+ exit(1)
38
+ rescue SystemExit
39
+ rescue Exception => exception
40
+ puts "#{exception.class}: #{exception.message}"
41
+ puts exception.backtrace.join("\n") if ENV['DEBUG']
42
+ exit(1)
43
+ end
44
+ exit(0)
45
+ end
46
+
47
+ def help
48
+ "Usage: sanford <command> <options> \n" \
49
+ "Commands: run, start, stop, restart \n" \
50
+ "#{@cli}"
51
+ end
52
+
53
+ end
54
+
55
+ module Manager
56
+
57
+ def self.call(action, options = nil)
58
+ get_handler_class(action).new(options).tap{ |manager| manager.send(action) }
59
+ end
60
+
61
+ def self.get_handler_class(action)
62
+ case action.to_sym
63
+ when :start, :run
64
+ ServerHandler
65
+ when :stop, :restart
66
+ SignalHandler
67
+ end
68
+ end
69
+
70
+ class ServerHandler
71
+
72
+ def initialize(options = nil)
73
+ @config = Config.new(options)
74
+ raise Sanford::NoHostError.new(@config.host_name) if !@config.found_host?
75
+ raise Sanford::InvalidHostError.new(@config.host) if !@config.has_listen_args?
76
+ @host = @config.host
77
+ @logger = @host.logger
78
+
79
+ @server_options = {}
80
+ # FUTURE allow passing through dat-tcp options (min/max workers)
81
+ # FUTURE merge in host options for verbose / keep_alive
82
+
83
+ @restart_cmd = RestartCmd.new(@config)
84
+ end
85
+
86
+ def run
87
+ self.run!
88
+ end
89
+
90
+ def start
91
+ self.run! true
92
+ end
93
+
94
+ protected
95
+
96
+ def run!(daemonize = false)
97
+ daemonize!(true) if daemonize
98
+ Sanford::Server.new(@host, @server_options).tap do |server|
99
+ log "Starting server for #{@host.name}"
100
+
101
+ server.listen(*@config.listen_args)
102
+ log "Listening on #{server.ip}:#{server.port}"
103
+ log "PID: #{Process.pid}"
104
+
105
+ $0 = ProcessName.new(@host.name, server.ip, server.port)
106
+ @config.pid_file.write
107
+
108
+ Signal.trap("TERM"){ self.stop!(server) }
109
+ Signal.trap("INT"){ self.halt!(server) }
110
+ Signal.trap("USR2"){ self.restart!(server) }
111
+
112
+ server.run(@config.client_file_descriptors).join
113
+ end
114
+ ensure
115
+ @config.pid_file.remove
116
+ end
117
+
118
+ def restart!(server)
119
+ log "Restarting the server..."
120
+ server.pause
121
+ log "server paused"
122
+
123
+ ENV['SANFORD_HOST'] = @host.name
124
+ ENV['SANFORD_SERVER_FD'] = server.file_descriptor.to_s
125
+ ENV['SANFORD_CLIENT_FDS'] = server.client_file_descriptors.join(',')
126
+
127
+ @logger.info "calling exec ..."
128
+ Dir.chdir @restart_cmd.dir
129
+ Kernel.exec(*@restart_cmd.argv)
130
+ end
131
+
132
+ def stop!(server)
133
+ log "Stopping the server..."
134
+ server.stop
135
+ log "Done"
136
+ end
137
+
138
+ def halt!(server)
139
+ log "Halting the server..."
140
+ server.halt false
141
+ log "Done"
142
+ end
143
+
144
+ # Full explanation: http://www.steve.org.uk/Reference/Unix/faq_2.html#SEC16
145
+ def daemonize!(no_chdir = false, no_close = false)
146
+ exit if fork
147
+ Process.setsid
148
+ exit if fork
149
+ Dir.chdir "/" unless no_chdir
150
+ if !no_close
151
+ null = File.open "/dev/null", 'w'
152
+ STDIN.reopen null
153
+ STDOUT.reopen null
154
+ STDERR.reopen null
155
+ end
156
+ return 0
157
+ end
158
+
159
+ def log(message)
160
+ @logger.info "[Sanford] #{message}"
161
+ end
162
+
163
+ end
164
+
165
+ class SignalHandler
166
+
167
+ def initialize(options = nil)
168
+ @config = Config.new(options)
169
+ raise Sanford::NoPIDError.new if !@config.pid
170
+ end
171
+
172
+ def stop
173
+ Process.kill("TERM", @config.pid)
174
+ end
175
+
176
+ def restart
177
+ Process.kill("USR2", @config.pid)
178
+ end
179
+
180
+ end
181
+
182
+ class Config
183
+ attr_reader :host_name, :host, :ip, :port, :file_descriptor
184
+ attr_reader :client_file_descriptors, :pid_file, :pid, :restart_dir
185
+
186
+ def initialize(opts = nil)
187
+ options = OpenStruct.new(opts || {})
188
+ @host_name = ENV['SANFORD_HOST'] || options.host
189
+
190
+ @host = @host_name ? Sanford.hosts.find(@host_name) : Sanford.hosts.first
191
+ @host ||= NullHost.new
192
+
193
+ @file_descriptor = ENV['SANFORD_SERVER_FD'] || options.file_descriptor
194
+ @file_descriptor = @file_descriptor.to_i if @file_descriptor
195
+ @ip = ENV['SANFORD_IP'] || options.ip || @host.ip
196
+ @port = ENV['SANFORD_PORT'] || options.port || @host.port
197
+ @port = @port.to_i if @port
198
+
199
+ client_fds_str = ENV['SANFORD_CLIENT_FDS'] || options.client_fds || ""
200
+ @client_file_descriptors = client_fds_str.split(',').map(&:to_i)
201
+
202
+ @pid_file = PIDFile.new(ENV['SANFORD_PID_FILE'] || options.pid_file || @host.pid_file)
203
+ @pid = options.pid || @pid_file.pid
204
+
205
+ @restart_dir = ENV['SANFORD_RESTART_DIR'] || options.restart_dir
206
+ end
207
+
208
+ def listen_args
209
+ @file_descriptor ? [ @file_descriptor ] : [ @ip, @port ]
210
+ end
211
+
212
+ def has_listen_args?
213
+ !!@file_descriptor || !!(@ip && @port)
214
+ end
215
+
216
+ def found_host?
217
+ !@host.kind_of?(NullHost)
218
+ end
219
+
220
+ end
221
+
222
+ class NullHost
223
+ [ :ip, :port, :pid_file ].each do |method_name|
224
+ define_method(method_name){ }
225
+ end
226
+ end
227
+
228
+ class ProcessName < String
229
+ def initialize(name, ip, port)
230
+ super "#{[ name, ip, port ].join('_')}"
231
+ end
232
+ end
233
+
234
+ class PIDFile
235
+ def initialize(path)
236
+ @path = (path || '/dev/null').to_s
237
+ end
238
+
239
+ def pid
240
+ pid = File.read(@path).strip if File.exists?(@path)
241
+ pid.to_i if pid && !pid.empty?
242
+ end
243
+
244
+ def write
245
+ File.open(@path, 'w'){|f| f.puts Process.pid }
246
+ end
247
+
248
+ def remove
249
+ FileUtils.rm_f(@path)
250
+ end
251
+
252
+ def to_s
253
+ @path
254
+ end
255
+ end
256
+
257
+ class RestartCmd
258
+ attr_reader :argv, :dir
259
+
260
+ def initialize(config = nil)
261
+ require 'rubygems'
262
+ config ||= OpenStruct.new
263
+ @dir = config.restart_dir || get_pwd
264
+ @argv = [ Gem.ruby, $0, ARGV.dup ].flatten
265
+ end
266
+
267
+ protected
268
+
269
+ # Trick from puma/unicorn. Favor PWD because it contains an unresolved
270
+ # symlink. This is useful when restarting after deploying; the original
271
+ # directory may be removed, but the symlink is pointing to a new
272
+ # directory.
273
+ def get_pwd
274
+ env_stat = File.stat(ENV['PWD'])
275
+ pwd_stat = File.stat(Dir.pwd)
276
+ if env_stat.ino == pwd_stat.ino && env_stat.dev == pwd_stat.dev
277
+ ENV['PWD']
278
+ else
279
+ Dir.pwd
280
+ end
281
+ end
282
+
283
+ end
284
+ end
285
+
286
+ class CLIRB # Version 1.0.0, https://github.com/redding/cli.rb
287
+ Error = Class.new(RuntimeError);
288
+ HelpExit = Class.new(RuntimeError); VersionExit = Class.new(RuntimeError)
289
+ attr_reader :argv, :args, :opts, :data
290
+
291
+ def initialize(&block)
292
+ @options = []; instance_eval(&block) if block
293
+ require 'optparse'
294
+ @data, @args, @opts = [], [], {}; @parser = OptionParser.new do |p|
295
+ p.banner = ''; @options.each do |o|
296
+ @opts[o.name] = o.value; p.on(*o.parser_args){ |v| @opts[o.name] = v }
297
+ end
298
+ p.on_tail('--version', ''){ |v| raise VersionExit, v.to_s }
299
+ p.on_tail('--help', ''){ |v| raise HelpExit, v.to_s }
300
+ end
301
+ end
302
+
303
+ def option(*args); @options << Option.new(*args); end
304
+ def parse!(argv)
305
+ @args = (argv || []).dup.tap do |args_list|
306
+ begin; @parser.parse!(args_list)
307
+ rescue OptionParser::ParseError => err; raise Error, err.message; end
308
+ end; @data = @args + [@opts]
309
+ end
310
+ def to_s; @parser.to_s; end
311
+ def inspect
312
+ "#<#{self.class}:#{'0x0%x' % (object_id << 1)} @data=#{@data.inspect}>"
313
+ end
314
+
315
+ class Option
316
+ attr_reader :name, :opt_name, :desc, :abbrev, :value, :klass, :parser_args
317
+
318
+ def initialize(name, *args)
319
+ settings, @desc = args.last.kind_of?(::Hash) ? args.pop : {}, args.pop || ''
320
+ @name, @opt_name, @abbrev = parse_name_values(name, settings[:abbrev])
321
+ @value, @klass = gvalinfo(settings[:value])
322
+ @parser_args = if [TrueClass, FalseClass, NilClass].include?(@klass)
323
+ ["-#{@abbrev}", "--[no-]#{@opt_name}", @desc]
324
+ else
325
+ ["-#{@abbrev}", "--#{@opt_name} #{@opt_name.upcase}", @klass, @desc]
326
+ end
327
+ end
328
+
329
+ private
330
+
331
+ def parse_name_values(name, custom_abbrev)
332
+ [ (processed_name = name.to_s.strip.downcase), processed_name.gsub('_', '-'),
333
+ custom_abbrev || processed_name.gsub(/[^a-z]/, '').chars.first || 'a'
334
+ ]
335
+ end
336
+ def gvalinfo(v); v.kind_of?(Class) ? [nil,gklass(v)] : [v,gklass(v.class)]; end
337
+ def gklass(k); k == Fixnum ? Integer : k; end
338
+ end
339
+ end
340
+
341
+ class NoHostError < CLIRB::Error
342
+ def initialize(host_name)
343
+ message = if Sanford.hosts.empty?
344
+ "No hosts have been defined. Please define a host before trying to run Sanford."
345
+ else
346
+ "A host couldn't be found with the name #{host_name.inspect}. "
347
+ end
348
+ super message
349
+ end
350
+ end
351
+
352
+ class InvalidHostError < CLIRB::Error
353
+ def initialize(host)
354
+ super "A port must be configured or provided to run a server for '#{host}'"
355
+ end
356
+ end
357
+
358
+ class NoPIDError < CLIRB::Error
359
+ def initialize
360
+ super "A PID or PID file is required"
361
+ end
362
+ end
363
+
364
+ end