sanford 0.4.0 → 0.6.0

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