unicorn-cuba-base 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source "http://rubygems.org"
2
+
3
+ ruby "1.9.3"
4
+
5
+ gem "cuba", "~> 3.0"
6
+ gem "unicorn", ">= 4.6.2"
7
+ gem "raindrops", "~> 0.11"
8
+ gem "cli", "~> 1.1.0"
9
+ gem "facter", "~> 1.6.11"
10
+ gem "ruby-ip", "~> 0.9"
11
+
12
+ # Add dependencies to develop your gem here.
13
+ # Include everything needed to run rake, tests, features, etc.
14
+ group :development do
15
+ gem "rspec", "~> 2.13"
16
+ gem "rdoc", "~> 3.9"
17
+ gem "jeweler", "~> 1.8.4"
18
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,48 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ cli (1.1.1)
5
+ cuba (3.1.0)
6
+ rack
7
+ diff-lcs (1.2.4)
8
+ facter (1.6.18)
9
+ git (1.2.5)
10
+ jeweler (1.8.4)
11
+ bundler (~> 1.0)
12
+ git (>= 1.2.5)
13
+ rake
14
+ rdoc
15
+ json (1.7.7)
16
+ kgio (2.8.0)
17
+ rack (1.5.2)
18
+ raindrops (0.11.0)
19
+ rake (10.0.4)
20
+ rdoc (3.12.2)
21
+ json (~> 1.4)
22
+ rspec (2.13.0)
23
+ rspec-core (~> 2.13.0)
24
+ rspec-expectations (~> 2.13.0)
25
+ rspec-mocks (~> 2.13.0)
26
+ rspec-core (2.13.1)
27
+ rspec-expectations (2.13.0)
28
+ diff-lcs (>= 1.1.3, < 2.0)
29
+ rspec-mocks (2.13.1)
30
+ ruby-ip (0.9.1)
31
+ unicorn (4.6.2)
32
+ kgio (~> 2.6)
33
+ rack
34
+ raindrops (~> 0.7)
35
+
36
+ PLATFORMS
37
+ ruby
38
+
39
+ DEPENDENCIES
40
+ cli (~> 1.1.0)
41
+ cuba (~> 3.0)
42
+ facter (~> 1.6.11)
43
+ jeweler (~> 1.8.4)
44
+ raindrops (~> 0.11)
45
+ rdoc (~> 3.9)
46
+ rspec (~> 2.13)
47
+ ruby-ip (~> 0.9)
48
+ unicorn (>= 4.6.2)
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Jakub Pastuszek
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # unicorn-cuba-base
2
+
3
+ It is web application base powered by Unicorn HTTP server and based on Cuba framework extended with additional Rack middleware and Cuba plugins.
4
+
5
+ This gem was created to deduplicate code from [httpthumbnailer](http://github.com/jpastuszek/httpthumbnailer) and [httpimagestore](http://github.com/jpastuszek/httpimagestore).
6
+
7
+ ## Installing
8
+
9
+ gem install unicorn-cuba-base
10
+
11
+ ## Usage
12
+
13
+ Please see [httpthumbnailer main executable](https://github.com/jpastuszek/httpthumbnailer/blob/master/bin/httpthumbnailer) and [httpimagestore main executable](https://github.com/jpastuszek/httpimagestore/blob/master/bin/httpimagestore) for usage example.
14
+
15
+ ## Contributing to unicorn-cuba-base
16
+
17
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
18
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
19
+ * Fork the project
20
+ * Start a feature/bugfix branch
21
+ * Commit and push until you are happy with your contribution
22
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
23
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
24
+
25
+ ## Copyright
26
+
27
+ Copyright (c) 2013 Jakub Pastuszek. See LICENSE.txt for
28
+ further details.
29
+
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "unicorn-cuba-base"
18
+ gem.homepage = "http://github.com/jpastuszek/unicorn-cuba-base"
19
+ gem.license = "MIT"
20
+ gem.summary = "web appliaction base powered by Unicorn and Cuba"
21
+ gem.description = "web application base powered by Unicorn HTTP server and based on Cuba framework extended with additional Rack middleware and Cuba plugins"
22
+ gem.email = "jpastuszek@gmail.com"
23
+ gem.authors = ["Jakub Pastuszek"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rspec/core'
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec) do |spec|
31
+ spec.pattern = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ end
38
+
39
+ task :default => :spec
40
+
41
+ require 'rdoc/task'
42
+ RDoc::Task.new do |rdoc|
43
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
44
+
45
+ rdoc.rdoc_dir = 'rdoc'
46
+ rdoc.title = "unicorn-cuba-base #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,161 @@
1
+ require 'cli'
2
+ require 'cuba'
3
+ require 'unicorn'
4
+ require 'unicorn/launcher'
5
+ require 'facter'
6
+ require 'pathname'
7
+ require 'ip'
8
+
9
+ require_relative 'unicorn-cuba-base/stats'
10
+ require_relative 'unicorn-cuba-base/root_logger'
11
+ require_relative 'unicorn-cuba-base/plugin/error_matcher'
12
+ require_relative 'unicorn-cuba-base/plugin/logging'
13
+ require_relative 'unicorn-cuba-base/plugin/response_helpers'
14
+ require_relative 'unicorn-cuba-base/plugin/memory_limit'
15
+ require_relative 'unicorn-cuba-base/rack/error_handling'
16
+ require_relative 'unicorn-cuba-base/rack/unhandled_request'
17
+ require_relative 'unicorn-cuba-base/rack/memory_limit'
18
+
19
+ class Controler < Cuba
20
+ include ClassLogging
21
+ end
22
+
23
+ require_relative 'unicorn-cuba-base/stats_reporter'
24
+
25
+ class Application
26
+ def cli(&block)
27
+ @cli_setup = block
28
+ end
29
+
30
+ def settings(&block)
31
+ @settings_setup = block
32
+ end
33
+
34
+ def main(&block)
35
+ @main_setup = block
36
+ end
37
+
38
+ def after_fork(&block)
39
+ @after_fork = block
40
+ end
41
+
42
+ def initialize(program_name, defaults = {}, &block)
43
+ instance_eval &block
44
+
45
+ @cli = setup_cli(program_name, defaults, @cli_setup) or fail 'no cli defined'
46
+ @settings = @settings_setup ? setup_settings(@settings_setup) : @cli.parse!
47
+
48
+ root_logger = RootLogger.new(STDERR)
49
+ root_logger.level = RootLogger::WARN
50
+ root_logger.level = RootLogger::INFO if @settings.verbose
51
+ root_logger.level = RootLogger::DEBUG if @settings.debug
52
+ Controler.logger = root_logger
53
+ MemoryLimit.logger = Controler.logger_for(MemoryLimit)
54
+
55
+ unicorn_settings = {}
56
+ unicorn_settings[:logger] = root_logger.logger_for(Unicorn::HttpServer)
57
+ unicorn_settings[:pid] = @settings.pid_file.to_s
58
+ unicorn_settings[:worker_processes] = @settings.worker_processes
59
+ unicorn_settings[:timeout] = @settings.worker_timeout
60
+ unicorn_settings[:listeners] = @settings.listener
61
+ unicorn_settings[:user] = @settings.user if @settings.user
62
+ unicorn_settings[:rewindable_input] = false # don't keep the upload data in memory or on disk (tmp)
63
+ unicorn_settings[:after_fork] = @after_fork if @after_fork
64
+
65
+ unless @settings.foreground
66
+ unicorn_settings[:stderr_path] = @settings.log_file.to_s
67
+ unicorn_settings[:stdout_path] = @settings.log_file.to_s
68
+
69
+ Unicorn::Launcher.daemonize!(unicorn_settings)
70
+
71
+ # capture startup messages
72
+ @settings.log_file.open('ab') do |log|
73
+ log.sync = true
74
+ STDERR.reopen log
75
+ STDOUT.reopen log
76
+ end
77
+ end
78
+
79
+ Controler.settings[:listeners] = @settings.listener
80
+ Controler.settings[:access_log_file] = @settings.access_log_file
81
+
82
+ Controler.plugin Plugin::ErrorMatcher
83
+ Controler.plugin Plugin::Logging
84
+ Controler.plugin Plugin::ResponseHelpers
85
+ Controler.plugin Plugin::MemoryLimit
86
+
87
+ @main_setup or fail 'no main controler provided'
88
+ main_controler = setup_main(@main_setup) or fail 'no main controler class returned'
89
+
90
+ access_log_file = @settings.access_log_file.open('a+')
91
+ access_log_file.sync = true
92
+ main_controler.use Rack::CommonLogger, access_log_file
93
+ main_controler.use Rack::ErrorHandling
94
+ main_controler.use Rack::UnhandledRequest
95
+ main_controler.use Rack::MemoryLimit, @settings.limit_memory * 1024 ** 2
96
+
97
+ Unicorn::HttpServer.new(main_controler, unicorn_settings).start.join
98
+ end
99
+
100
+ private
101
+
102
+ def setup_cli(program_name, defaults, block)
103
+ CLI.new do
104
+ instance_eval &block
105
+ option :log_file,
106
+ short: :l,
107
+ cast: Pathname,
108
+ description: 'log file location',
109
+ default: "#{program_name}.log"
110
+ option :access_log_file,
111
+ short: :a,
112
+ cast: Pathname,
113
+ description: 'NCSA access log file location',
114
+ default: "#{program_name}_access.log"
115
+ option :pid_file,
116
+ short: :p,
117
+ cast: Pathname,
118
+ description: 'PID file location',
119
+ default: "#{program_name}.pid"
120
+ switch :foreground,
121
+ short: :f,
122
+ description: 'stay in foreground'
123
+ options :listener,
124
+ short: :L,
125
+ description: 'HTTP server listener (bind) address in format <ip>:<port> or unix:<file> or ~[<username>]/<file> for UNIX sockets',
126
+ default: '127.0.0.1:' + (defaults[:port] || 8080).to_s
127
+ option :user,
128
+ short: :u,
129
+ description: 'run worker processes as given user'
130
+ option :worker_processes,
131
+ short: :w,
132
+ cast: Integer,
133
+ description: 'start given number of worker processes',
134
+ default: (Facter.processorcount.to_i + 1) * (defaults[:processor_count_factor] || 1)
135
+ option :worker_timeout,
136
+ short: :t,
137
+ cast: Integer,
138
+ description: 'workers handling the request taking longer than this time period will be forcibly killed',
139
+ default: 300
140
+ option :limit_memory,
141
+ cast: Integer,
142
+ description: 'memory usage limit in MiB',
143
+ default: 128
144
+ switch :verbose,
145
+ short: :v,
146
+ description: 'enable verbose logging (INFO)'
147
+ switch :debug,
148
+ short: :d,
149
+ description: 'enable verbose and debug logging (DEBUG)'
150
+ end
151
+ end
152
+
153
+ def setup_settings(block)
154
+ @cli.parse!(&block)
155
+ end
156
+
157
+ def setup_main(block)
158
+ block.call @settings
159
+ end
160
+ end
161
+
@@ -0,0 +1,62 @@
1
+ require 'unicorn-cuba-base/root_logger'
2
+
3
+ class MemoryLimit
4
+ include ClassLogging
5
+
6
+ class MemoryLimitedExceededError < RuntimeError
7
+ def initialize
8
+ super "memory limit exceeded"
9
+ end
10
+ end
11
+
12
+ module IO
13
+ def root_limit(ml)
14
+ @root_limit = ml
15
+ end
16
+
17
+ def read(bytes = nil)
18
+ data = @root_limit.get do |max_read_bytes|
19
+ if not bytes or bytes > max_read_bytes
20
+ data = super max_read_bytes
21
+ raise MemoryLimitedExceededError unless eof?
22
+ data or '' # read() always returns '' on EOF
23
+ else
24
+ super bytes or ''
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ def initialize(bytes = 256 * 1024 ** 2)
31
+ log.info "using memory limit of #{bytes} bytes" if bytes
32
+ @limit = bytes
33
+ end
34
+
35
+ attr_reader :limit
36
+
37
+ def get
38
+ yield(@limit).tap do |data|
39
+ borrow data.bytesize if data
40
+ end
41
+ end
42
+
43
+ def borrow(bytes)
44
+ log.debug "borrowing #{bytes} from #{@limit} bytes of limit"
45
+ bytes > @limit and raise MemoryLimitedExceededError
46
+ @limit -= bytes
47
+ bytes
48
+ end
49
+
50
+ def return(bytes)
51
+ log.debug "returning #{bytes} to #{@limit} bytes of limit"
52
+ @limit += bytes
53
+ bytes
54
+ end
55
+
56
+ def io(io)
57
+ io.extend MemoryLimit::IO
58
+ io.root_limit self
59
+ io
60
+ end
61
+ end
62
+
@@ -0,0 +1,12 @@
1
+ module Plugin
2
+ module ErrorMatcher
3
+ def error(*klass)
4
+ lambda {klass.any?{|k| env["app.error"].is_a? k} and captures.push(env["app.error"])}
5
+ end
6
+
7
+ def error?
8
+ env.has_key? "app.error"
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,9 @@
1
+ module Plugin
2
+ module Logging
3
+ def log
4
+ return @logger if defined? @logger
5
+ @logger = self.class.logger_for(self.class)
6
+ end
7
+ end
8
+ end
9
+
@@ -0,0 +1,8 @@
1
+ module Plugin
2
+ module MemoryLimit
3
+ def memory_limit
4
+ env["app.memory_limit"] or fail 'Rack::MemoryLimit middleware not used!'
5
+ end
6
+ end
7
+ end
8
+
@@ -0,0 +1,83 @@
1
+ require 'securerandom'
2
+ require 'unicorn-cuba-base/stats'
3
+
4
+ module Plugin
5
+ module ResponseHelpers
6
+ extend Stats
7
+ def_stats(
8
+ :total_write_multipart,
9
+ :total_write,
10
+ :total_write_part,
11
+ :total_write_error,
12
+ :total_write_error_part
13
+ )
14
+
15
+ def write(code, content_type, body)
16
+ req.body.read # read all remaining upload before we send response so that client will read it
17
+ res.status = code
18
+ res["Content-Type"] = content_type
19
+ ResponseHelpers.stats.incr_total_write
20
+ res.write body
21
+ end
22
+
23
+ def write_plain(code, msg)
24
+ msg = msg.join("\r\n") if msg.is_a? Array
25
+ write code, 'text/plain', msg.gsub(/(?<!\r)\n/, "\r\n") + "\r\n"
26
+ end
27
+
28
+ def write_url_list(code, msg)
29
+ msg = msg.join("\r\n") if msg.is_a? Array
30
+ write code, 'text/uri-list', msg.gsub(/(?<!\r)\n/, "\r\n") + "\r\n"
31
+ end
32
+
33
+ def write_error(code, error)
34
+ msg = error.message
35
+ log.warn "sending #{code} error response: #{msg}"
36
+ ResponseHelpers.stats.incr_total_write_error
37
+ write_plain code, msg
38
+ end
39
+
40
+ def write_url_list(code, urls)
41
+ write code, 'text/uri-list', urls.join("\r\n") + "\r\n"
42
+ end
43
+
44
+ # Multipart
45
+ def write_preamble(code, headers = {})
46
+ res.status = code
47
+ @boundary = SecureRandom.uuid
48
+ res["Content-Type"] = "multipart/mixed; boundary=\"#{@boundary}\""
49
+ headers.each do |key, value|
50
+ res[key] = value
51
+ end
52
+ ResponseHelpers.stats.incr_total_write_multipart
53
+ end
54
+
55
+ def write_part(content_type, body, headers = {})
56
+ res.write "--#{@boundary}\r\n"
57
+ res.write "Content-Type: #{content_type}\r\n"
58
+ headers.each_pair do |name, value|
59
+ res.write "#{name}: #{value}\r\n"
60
+ end
61
+ res.write "\r\n"
62
+ ResponseHelpers.stats.incr_total_write_part
63
+ res.write body
64
+ res.write "\r\n"
65
+ end
66
+
67
+ def write_plain_part(msg, headers = {})
68
+ write_part 'text/plain', msg.to_s.gsub("\n", "\r\n"), headers
69
+ end
70
+
71
+ def write_error_part(code, error)
72
+ msg = error.message
73
+ log.warn "sending error in multipart response part: #{msg}"
74
+ ResponseHelpers.stats.incr_total_write_error_part
75
+ write_plain_part msg, 'Status' => code
76
+ end
77
+
78
+ def write_epilogue
79
+ res.write "--#{@boundary}--\r\n"
80
+ end
81
+ end
82
+ end
83
+
@@ -0,0 +1,31 @@
1
+ require 'pp'
2
+
3
+ module Rack
4
+ class ErrorHandling
5
+ def initialize(app, &block)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ # save original env
11
+ orig_env = env.dup
12
+ begin
13
+ return @app.call(env)
14
+ rescue => error
15
+ begin
16
+ # reset env to original since it could have been changed
17
+ env.clear
18
+ env.merge!(orig_env)
19
+
20
+ # set error so app can handle it
21
+ env["app.error"] = error
22
+
23
+ return @app.call(env)
24
+ rescue
25
+ raise
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
@@ -0,0 +1,20 @@
1
+ require_relative '../memory_limit'
2
+
3
+ module Rack
4
+ class MemoryLimit
5
+ def initialize(app, memory_limit)
6
+ @app = app
7
+ @memory_limit = memory_limit
8
+ end
9
+
10
+ def call(env)
11
+ memory_limit = ::MemoryLimit.new(@memory_limit)
12
+ env["app.memory_limit"] = memory_limit
13
+
14
+ # use up limit when reading request data
15
+ memory_limit.io env["rack.input"]
16
+ return @app.call(env)
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,22 @@
1
+ module Rack
2
+ class UnhandledRequest
3
+ class UnhandledRequestError < ArgumentError
4
+ attr_reader :uri
5
+ def initialize(uri)
6
+ @uri = uri
7
+ super "request for URI '#{uri}' was not handled by the server"
8
+ end
9
+ end
10
+
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env)
16
+ status, headers, body = @app.call(env)
17
+ raise UnhandledRequestError, env['SCRIPT_NAME'] + env['PATH_INFO'] if body == [] and (status == 200 or status == 404)
18
+ [status, headers, body]
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,110 @@
1
+ require 'logger'
2
+
3
+ class RootLogger < Logger
4
+ class ClassLogger
5
+ @@levels = [:debug, :info, :warn, :error, :fatal, :unknown]
6
+
7
+ def initialize(root_logger, class_obj)
8
+ @root_logger = root_logger
9
+ @progname = class_obj.name
10
+ @root_logger.formatter = proc do |severity, datetime, progname, msg|
11
+ "[#{datetime.utc.strftime "%Y-%m-%d %H:%M:%S.%6N %Z"}] [#{$$} #{progname}] #{severity}: #{msg}\n"
12
+ end
13
+ end
14
+
15
+ def respond_to?(method)
16
+ super or @root_logger.respond_to? method
17
+ end
18
+
19
+ def method_missing(name, *args, &block)
20
+ if @@levels.include? name
21
+ message = if block_given?
22
+ self.progname
23
+ else
24
+ args.map do |arg|
25
+ if arg.is_a? Exception
26
+ "#{arg.class.name}: #{arg.message}\n#{arg.backtrace.join("\n")}"
27
+ else
28
+ arg.to_s
29
+ end
30
+ end.join(': ')
31
+ end
32
+
33
+ # set program name to current class
34
+ @root_logger.progname = @progname
35
+ @root_logger.send(name, message.chomp, &block)
36
+ else
37
+ @root_logger.send(name, *args, &block)
38
+ end
39
+ end
40
+
41
+ attr_reader :root_logger
42
+
43
+ def inspect
44
+ "#<ClassLogger[#{@progname}] #{"0x%X" % object_id} root_logger=#{@root_logger.inspect}>"
45
+ end
46
+ end
47
+
48
+ def logger_for(class_obj)
49
+ ClassLogger.new(self, class_obj)
50
+ end
51
+
52
+ def root_logger
53
+ self
54
+ end
55
+
56
+ def inspect
57
+ "#<RootLogger #{"0x%X" % object_id}>"
58
+ end
59
+ end
60
+
61
+ module ClassLogging
62
+ module ClassMethods
63
+ def init_logger
64
+ @@logger = {} unless defined? @@logger
65
+ end
66
+
67
+ def logger=(logger)
68
+ @@logger[self] = logger
69
+ end
70
+
71
+ def log
72
+ unless @@logger[self]
73
+ new_root_logger = false
74
+ # use root logger from ancestor or create new one
75
+ root_logger =
76
+ if logging_class = ancestors.find{|an| an != self and an.respond_to? :log}
77
+ logging_class.log.respond_to?(:root_logger) ? logging_class.log.root_logger : logging_class.log
78
+ else
79
+ new_root_logger = true
80
+ Logger.new(STDERR)
81
+ end
82
+
83
+ root_logger.kind_of? RootLogger::ClassLogger and fail "got ClassLogger root logger: #{self}"
84
+
85
+ logger = RootLogger::ClassLogger.new(root_logger, self)
86
+ logger.warn "new default logger crated" if new_root_logger
87
+ @@logger[self] = logger
88
+ end
89
+ @@logger[self]
90
+ end
91
+
92
+ def logger_for(class_obj)
93
+ RootLogger::ClassLogger.new(log.root_logger, class_obj)
94
+ end
95
+ end
96
+
97
+ def log
98
+ self.class.log
99
+ end
100
+
101
+ def logger_for(class_obj)
102
+ self.class.logger_for(class_obj)
103
+ end
104
+
105
+ def self.included(class_obj)
106
+ class_obj.extend ClassMethods
107
+ class_obj.init_logger
108
+ end
109
+ end
110
+
@@ -0,0 +1,30 @@
1
+ require 'raindrops'
2
+
3
+ module Stats
4
+ class MyStruct < Raindrops::Struct
5
+ def self.new(*members)
6
+ klass = super(*members)
7
+
8
+ str = ''
9
+ # add support to increment by more than 1
10
+ members.map { |x| x.to_sym }.each_with_index do |member, i|
11
+ str << "def incr_#{member}(v = 1); @raindrops.incr(#{i}, v); end; "
12
+ str << "def decr_#{member}(v = 1); @raindrops.decr(#{i}, v); end; "
13
+ end
14
+
15
+ klass.class_eval(str)
16
+ klass
17
+ end
18
+ end
19
+
20
+ def def_stats(*stat_names)
21
+ @@local_stats ||= {}
22
+ stats_class = eval "MyStruct.new(#{stat_names.map{|s| ":#{s.to_s}"}.join(', ')})"
23
+ @@local_stats[self] = stats_class.new
24
+ end
25
+
26
+ def stats
27
+ @@local_stats[self]
28
+ end
29
+ end
30
+
@@ -0,0 +1,23 @@
1
+ class StatsReporter < Controler
2
+ def self.<<(stats)
3
+ (@@stats ||= []) << stats
4
+ end
5
+
6
+ self.define do
7
+ all_stats = {}
8
+ @@stats.each do |stats|
9
+ stats.class::MEMBERS.each.with_index.map do |stat, index|
10
+ all_stats[stat] = stats[index]
11
+ end
12
+ end
13
+
14
+ on :stat do |stat|
15
+ write_plain 200, all_stats[stat.to_sym].to_s || raise(ArgumentError, "unknown stat #{stat}")
16
+ end
17
+
18
+ on true do
19
+ write_plain 200, all_stats.map{|stat, value| "#{stat}: #{value}"}.join("\n")
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,66 @@
1
+ require_relative 'spec_helper'
2
+ require 'unicorn-cuba-base/memory_limit'
3
+ MemoryLimit.logger = Logger.new('/dev/null')
4
+
5
+ describe MemoryLimit do
6
+ subject do
7
+ MemoryLimit.new(10)
8
+ end
9
+
10
+ it 'should raise MemoryLimitedExceededError when too much memory is borrowed' do
11
+ subject.borrow 8
12
+ subject.return 3
13
+ subject.borrow 4
14
+ subject.borrow 1
15
+ expect {
16
+ subject.borrow 1
17
+ }.to raise_error MemoryLimit::MemoryLimitedExceededError, 'memory limit exceeded'
18
+ end
19
+
20
+ describe '#get' do
21
+ it 'should yield limit left and borrow as much as returned string bytesize' do
22
+ subject.get do |limit|
23
+ limit.should == 10
24
+ '123'
25
+ end.should == '123'
26
+ subject.limit.should == 7
27
+ end
28
+
29
+ it 'should raise MemoryLimit::MemoryLimitedExceededError if retruned string is longer than the limit' do
30
+ expect {
31
+ subject.get do |limit|
32
+ '12345678901'
33
+ end
34
+ }.to raise_error MemoryLimit::MemoryLimitedExceededError
35
+ end
36
+ end
37
+
38
+ describe MemoryLimit::IO do
39
+ it 'should limit reading from extended IO like object' do
40
+ test_file = Pathname.new('/tmp/memlimtest')
41
+ test_file.open('w+') { |io|
42
+ io.write '12345'
43
+ io.seek 0
44
+
45
+ subject.io io
46
+ io.read.should == '12345'
47
+
48
+ io.seek 0
49
+ io.truncate 0
50
+ io.write '678'
51
+ io.seek 0
52
+ io.read.should == '678'
53
+
54
+ io.seek 0
55
+ io.write '09123'
56
+ io.seek 0
57
+
58
+ expect {
59
+ io.read
60
+ }.to raise_error MemoryLimit::MemoryLimitedExceededError, 'memory limit exceeded'
61
+ }
62
+ test_file.unlink
63
+ end
64
+ end
65
+ end
66
+
@@ -0,0 +1,58 @@
1
+ require_relative 'spec_helper'
2
+
3
+ require 'unicorn-cuba-base/root_logger'
4
+ require 'stringio'
5
+
6
+ describe do
7
+ let! :log_out do
8
+ StringIO.new
9
+ end
10
+
11
+ subject do
12
+ RootLogger.new(log_out)
13
+ end
14
+
15
+ it 'should log to given logger' do
16
+ subject.info 'hello world'
17
+ log_out.string.should include 'INFO'
18
+ log_out.string.should include 'hello world'
19
+ end
20
+
21
+ describe 'support for different logging levels' do
22
+ it 'should log info' do
23
+ subject.info 'hello world'
24
+ log_out.string.should include 'INFO'
25
+ end
26
+
27
+ it 'should log warn' do
28
+ subject.warn 'hello world'
29
+ log_out.string.should include 'WARN'
30
+ end
31
+
32
+ it 'should log error' do
33
+ subject.error 'hello world'
34
+ log_out.string.should include 'ERROR'
35
+ end
36
+ end
37
+
38
+ describe 'class logger' do
39
+ it 'should report class name' do
40
+ TestApp = Class.new
41
+ subject.logger_for(TestApp).info 'hello world'
42
+ log_out.string.should include 'TestApp'
43
+
44
+ subject.logger_for(String).info 'hello world'
45
+ log_out.string.should include 'String'
46
+ end
47
+
48
+ it 'should log exceptions' do
49
+ begin
50
+ raise RuntimeError, 'bad luck'
51
+ rescue => error
52
+ subject.logger_for(String).error 'hello world', error
53
+ end
54
+ log_out.string.should include 'hello world: RuntimeError: bad luck'
55
+ end
56
+ end
57
+ end
58
+
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'unicorn-cuba-base'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end
@@ -0,0 +1,87 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "unicorn-cuba-base"
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Jakub Pastuszek"]
12
+ s.date = "2013-07-16"
13
+ s.description = "web application base powered by Unicorn HTTP server and based on Cuba framework extended with additional Rack middleware and Cuba plugins"
14
+ s.email = "jpastuszek@gmail.com"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".rspec",
22
+ "Gemfile",
23
+ "Gemfile.lock",
24
+ "LICENSE.txt",
25
+ "README.md",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "lib/unicorn-cuba-base.rb",
29
+ "lib/unicorn-cuba-base/memory_limit.rb",
30
+ "lib/unicorn-cuba-base/plugin/error_matcher.rb",
31
+ "lib/unicorn-cuba-base/plugin/logging.rb",
32
+ "lib/unicorn-cuba-base/plugin/memory_limit.rb",
33
+ "lib/unicorn-cuba-base/plugin/response_helpers.rb",
34
+ "lib/unicorn-cuba-base/rack/error_handling.rb",
35
+ "lib/unicorn-cuba-base/rack/memory_limit.rb",
36
+ "lib/unicorn-cuba-base/rack/unhandled_request.rb",
37
+ "lib/unicorn-cuba-base/root_logger.rb",
38
+ "lib/unicorn-cuba-base/stats.rb",
39
+ "lib/unicorn-cuba-base/stats_reporter.rb",
40
+ "spec/memory_limit_spec.rb",
41
+ "spec/root_logger_spec.rb",
42
+ "spec/spec_helper.rb",
43
+ "unicorn-cuba-base.gemspec"
44
+ ]
45
+ s.homepage = "http://github.com/jpastuszek/unicorn-cuba-base"
46
+ s.licenses = ["MIT"]
47
+ s.require_paths = ["lib"]
48
+ s.rubygems_version = "1.8.25"
49
+ s.summary = "web appliaction base powered by Unicorn and Cuba"
50
+
51
+ if s.respond_to? :specification_version then
52
+ s.specification_version = 3
53
+
54
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
55
+ s.add_runtime_dependency(%q<cuba>, ["~> 3.0"])
56
+ s.add_runtime_dependency(%q<unicorn>, [">= 4.6.2"])
57
+ s.add_runtime_dependency(%q<raindrops>, ["~> 0.11"])
58
+ s.add_runtime_dependency(%q<cli>, ["~> 1.1.0"])
59
+ s.add_runtime_dependency(%q<facter>, ["~> 1.6.11"])
60
+ s.add_runtime_dependency(%q<ruby-ip>, ["~> 0.9"])
61
+ s.add_development_dependency(%q<rspec>, ["~> 2.13"])
62
+ s.add_development_dependency(%q<rdoc>, ["~> 3.9"])
63
+ s.add_development_dependency(%q<jeweler>, ["~> 1.8.4"])
64
+ else
65
+ s.add_dependency(%q<cuba>, ["~> 3.0"])
66
+ s.add_dependency(%q<unicorn>, [">= 4.6.2"])
67
+ s.add_dependency(%q<raindrops>, ["~> 0.11"])
68
+ s.add_dependency(%q<cli>, ["~> 1.1.0"])
69
+ s.add_dependency(%q<facter>, ["~> 1.6.11"])
70
+ s.add_dependency(%q<ruby-ip>, ["~> 0.9"])
71
+ s.add_dependency(%q<rspec>, ["~> 2.13"])
72
+ s.add_dependency(%q<rdoc>, ["~> 3.9"])
73
+ s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
74
+ end
75
+ else
76
+ s.add_dependency(%q<cuba>, ["~> 3.0"])
77
+ s.add_dependency(%q<unicorn>, [">= 4.6.2"])
78
+ s.add_dependency(%q<raindrops>, ["~> 0.11"])
79
+ s.add_dependency(%q<cli>, ["~> 1.1.0"])
80
+ s.add_dependency(%q<facter>, ["~> 1.6.11"])
81
+ s.add_dependency(%q<ruby-ip>, ["~> 0.9"])
82
+ s.add_dependency(%q<rspec>, ["~> 2.13"])
83
+ s.add_dependency(%q<rdoc>, ["~> 3.9"])
84
+ s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
85
+ end
86
+ end
87
+
metadata ADDED
@@ -0,0 +1,219 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: unicorn-cuba-base
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jakub Pastuszek
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-07-16 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: cuba
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '3.0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: unicorn
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 4.6.2
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 4.6.2
46
+ - !ruby/object:Gem::Dependency
47
+ name: raindrops
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '0.11'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '0.11'
62
+ - !ruby/object:Gem::Dependency
63
+ name: cli
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 1.1.0
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 1.1.0
78
+ - !ruby/object:Gem::Dependency
79
+ name: facter
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 1.6.11
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 1.6.11
94
+ - !ruby/object:Gem::Dependency
95
+ name: ruby-ip
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: '0.9'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: '0.9'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rspec
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: '2.13'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ version: '2.13'
126
+ - !ruby/object:Gem::Dependency
127
+ name: rdoc
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ~>
132
+ - !ruby/object:Gem::Version
133
+ version: '3.9'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ~>
140
+ - !ruby/object:Gem::Version
141
+ version: '3.9'
142
+ - !ruby/object:Gem::Dependency
143
+ name: jeweler
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ~>
148
+ - !ruby/object:Gem::Version
149
+ version: 1.8.4
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ~>
156
+ - !ruby/object:Gem::Version
157
+ version: 1.8.4
158
+ description: web application base powered by Unicorn HTTP server and based on Cuba
159
+ framework extended with additional Rack middleware and Cuba plugins
160
+ email: jpastuszek@gmail.com
161
+ executables: []
162
+ extensions: []
163
+ extra_rdoc_files:
164
+ - LICENSE.txt
165
+ - README.md
166
+ files:
167
+ - .document
168
+ - .rspec
169
+ - Gemfile
170
+ - Gemfile.lock
171
+ - LICENSE.txt
172
+ - README.md
173
+ - Rakefile
174
+ - VERSION
175
+ - lib/unicorn-cuba-base.rb
176
+ - lib/unicorn-cuba-base/memory_limit.rb
177
+ - lib/unicorn-cuba-base/plugin/error_matcher.rb
178
+ - lib/unicorn-cuba-base/plugin/logging.rb
179
+ - lib/unicorn-cuba-base/plugin/memory_limit.rb
180
+ - lib/unicorn-cuba-base/plugin/response_helpers.rb
181
+ - lib/unicorn-cuba-base/rack/error_handling.rb
182
+ - lib/unicorn-cuba-base/rack/memory_limit.rb
183
+ - lib/unicorn-cuba-base/rack/unhandled_request.rb
184
+ - lib/unicorn-cuba-base/root_logger.rb
185
+ - lib/unicorn-cuba-base/stats.rb
186
+ - lib/unicorn-cuba-base/stats_reporter.rb
187
+ - spec/memory_limit_spec.rb
188
+ - spec/root_logger_spec.rb
189
+ - spec/spec_helper.rb
190
+ - unicorn-cuba-base.gemspec
191
+ homepage: http://github.com/jpastuszek/unicorn-cuba-base
192
+ licenses:
193
+ - MIT
194
+ post_install_message:
195
+ rdoc_options: []
196
+ require_paths:
197
+ - lib
198
+ required_ruby_version: !ruby/object:Gem::Requirement
199
+ none: false
200
+ requirements:
201
+ - - ! '>='
202
+ - !ruby/object:Gem::Version
203
+ version: '0'
204
+ segments:
205
+ - 0
206
+ hash: -3548490658984311397
207
+ required_rubygems_version: !ruby/object:Gem::Requirement
208
+ none: false
209
+ requirements:
210
+ - - ! '>='
211
+ - !ruby/object:Gem::Version
212
+ version: '0'
213
+ requirements: []
214
+ rubyforge_project:
215
+ rubygems_version: 1.8.25
216
+ signing_key:
217
+ specification_version: 3
218
+ summary: web appliaction base powered by Unicorn and Cuba
219
+ test_files: []