swiftiply 0.6.1.1 → 1.0.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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/CONTRIBUTORS +2 -0
  3. data/README.md +62 -0
  4. data/bin/{mongrel_rails → evented_mongrel_rails} +6 -14
  5. data/bin/swiftiplied_mongrel_rails +246 -0
  6. data/bin/swiftiply +136 -116
  7. data/bin/swiftiply_mongrel_rails +2 -2
  8. data/bin/swiftiplyctl +283 -0
  9. data/cleanup.sh +5 -0
  10. data/ext/deque/extconf.rb +162 -0
  11. data/ext/deque/swiftcore/rubymain.cpp +435 -0
  12. data/ext/fastfilereader/extconf.rb +2 -2
  13. data/ext/fastfilereader/mapper.cpp +2 -0
  14. data/ext/map/extconf.rb +161 -0
  15. data/ext/map/rubymain.cpp +500 -0
  16. data/ext/splaytree/extconf.rb +161 -0
  17. data/ext/splaytree/swiftcore/rubymain.cpp +580 -0
  18. data/ext/splaytree/swiftcore/splay_map.h +635 -0
  19. data/ext/splaytree/swiftcore/splay_set.h +575 -0
  20. data/ext/splaytree/swiftcore/splay_tree.h +1127 -0
  21. data/external/httpclient.rb +231 -0
  22. data/external/package.rb +13 -13
  23. data/setup.rb +18 -2
  24. data/src/swiftcore/Swiftiply.rb +417 -773
  25. data/src/swiftcore/Swiftiply/backend_protocol.rb +213 -0
  26. data/src/swiftcore/Swiftiply/cache_base.rb +49 -0
  27. data/src/swiftcore/Swiftiply/cache_base_mixin.rb +52 -0
  28. data/src/swiftcore/Swiftiply/cluster_managers/rest_based_cluster_manager.rb +9 -0
  29. data/src/swiftcore/Swiftiply/cluster_protocol.rb +70 -0
  30. data/src/swiftcore/Swiftiply/config.rb +370 -0
  31. data/src/swiftcore/Swiftiply/config/rest_updater.rb +26 -0
  32. data/src/swiftcore/Swiftiply/constants.rb +101 -0
  33. data/src/swiftcore/Swiftiply/content_cache_entry.rb +44 -0
  34. data/src/swiftcore/Swiftiply/content_response.rb +45 -0
  35. data/src/swiftcore/Swiftiply/control_protocol.rb +49 -0
  36. data/src/swiftcore/Swiftiply/dynamic_request_cache.rb +41 -0
  37. data/src/swiftcore/Swiftiply/etag_cache.rb +64 -0
  38. data/src/swiftcore/Swiftiply/file_cache.rb +46 -0
  39. data/src/swiftcore/Swiftiply/hash_cache_base.rb +22 -0
  40. data/src/swiftcore/Swiftiply/http_recognizer.rb +267 -0
  41. data/src/swiftcore/Swiftiply/loggers/Analogger.rb +21 -0
  42. data/src/swiftcore/Swiftiply/loggers/stderror.rb +13 -0
  43. data/src/swiftcore/Swiftiply/mocklog.rb +10 -0
  44. data/src/swiftcore/Swiftiply/proxy.rb +15 -0
  45. data/src/swiftcore/Swiftiply/proxy_backends/keepalive.rb +286 -0
  46. data/src/swiftcore/Swiftiply/proxy_backends/traditional.rb +286 -0
  47. data/src/swiftcore/Swiftiply/proxy_backends/traditional/redis_directory.rb +87 -0
  48. data/src/swiftcore/Swiftiply/proxy_backends/traditional/static_directory.rb +69 -0
  49. data/src/swiftcore/Swiftiply/proxy_bag.rb +716 -0
  50. data/src/swiftcore/Swiftiply/rest_based_cluster_manager.rb +15 -0
  51. data/src/swiftcore/Swiftiply/splay_cache_base.rb +21 -0
  52. data/src/swiftcore/Swiftiply/support_pagecache.rb +6 -3
  53. data/src/swiftcore/Swiftiply/swiftiply_2_http_proxy.rb +7 -0
  54. data/src/swiftcore/Swiftiply/swiftiply_client.rb +20 -5
  55. data/src/swiftcore/Swiftiply/version.rb +5 -0
  56. data/src/swiftcore/evented_mongrel.rb +26 -8
  57. data/src/swiftcore/hash.rb +43 -0
  58. data/src/swiftcore/method_builder.rb +28 -0
  59. data/src/swiftcore/streamer.rb +46 -0
  60. data/src/swiftcore/swiftiplied_mongrel.rb +91 -23
  61. data/src/swiftcore/types.rb +20 -3
  62. data/swiftiply.gemspec +14 -8
  63. data/test/TC_Deque.rb +152 -0
  64. data/test/TC_ProxyBag.rb +147 -166
  65. data/test/TC_Swiftiply.rb +576 -169
  66. data/test/TC_Swiftiply/mongrel/evented_hello.rb +1 -1
  67. data/test/TC_Swiftiply/mongrel/swiftiplied_hello.rb +1 -1
  68. data/test/TC_Swiftiply/test_serve_static_file_xsendfile/sendfile_client.rb +27 -0
  69. data/test/TC_Swiftiply/test_ssl/bin/validate_ssl_capability.rb +21 -0
  70. data/test/TC_Swiftiply/test_ssl/test.cert +16 -0
  71. data/test/TC_Swiftiply/test_ssl/test.key +15 -0
  72. data/{bin → test/bin}/echo_client +0 -0
  73. metadata +136 -94
  74. data/README +0 -126
  75. data/ext/swiftiply_parse/parse.rl +0 -90
@@ -0,0 +1,231 @@
1
+ # This is a replacement for the regular EventMachine HttpClient library.
2
+ # It removes a few lines that seek to protect the user against himself.
3
+ # This makes it a much more useful tool for sending wacky request lines
4
+ # to a web server.
5
+
6
+
7
+ module EventMachine
8
+ module Protocols
9
+
10
+ class HttpClient < Connection
11
+ include EventMachine::Deferrable
12
+
13
+ remove_const :MaxPostContentLength if MaxPostContentLength
14
+ MaxPostContentLength = 20 * 1024 * 1024
15
+
16
+ # USAGE SAMPLE:
17
+ #
18
+ # EventMachine.run {
19
+ # http = EventMachine::Protocols::HttpClient.request(
20
+ # :host => server,
21
+ # :port => 80,
22
+ # :request => "/index.html",
23
+ # :query_string => "parm1=value1&parm2=value2"
24
+ # )
25
+ # http.callback {|response|
26
+ # puts response[:status]
27
+ # puts response[:headers]
28
+ # puts response[:content]
29
+ # }
30
+ # }
31
+ #
32
+
33
+ # TODO:
34
+ # Add streaming so we can support enormous POSTs. Current max is 20meg.
35
+ # Timeout for connections that run too long or hang somewhere in the middle.
36
+ # Persistent connections (HTTP/1.1), may need a associated delegate object.
37
+ # DNS: Some way to cache DNS lookups for hostnames we connect to. Ruby's
38
+ # DNS lookups are unbelievably slow.
39
+ # HEAD requests.
40
+ # Chunked transfer encoding.
41
+ # Convenience methods for requests. get, post, url, etc.
42
+ # SSL.
43
+ # Handle status codes like 304, 100, etc.
44
+ # Refactor this code so that protocol errors all get handled one way (an exception?),
45
+ # instead of sprinkling set_deferred_status :failed calls everywhere.
46
+
47
+ def self.request( args = {} )
48
+ args[:port] ||= 80
49
+ EventMachine.connect( args[:host], args[:port], self ) {|c|
50
+ # According to the docs, we will get here AFTER post_init is called.
51
+ c.instance_eval {@args = args}
52
+ }
53
+ end
54
+
55
+ def post_init
56
+ @start_time = Time.now
57
+ @data = ""
58
+ @read_state = :base
59
+ end
60
+
61
+ # We send the request when we get a connection.
62
+ # AND, we set an instance variable to indicate we passed through here.
63
+ # That allows #unbind to know whether there was a successful connection.
64
+ # NB: This naive technique won't work when we have to support multiple
65
+ # requests on a single connection.
66
+ def connection_completed
67
+ @connected = true
68
+ send_request @args
69
+ end
70
+
71
+ def send_request args
72
+ args[:verb] ||= args[:method] # Support :method as an alternative to :verb.
73
+ args[:verb] ||= :get # IS THIS A GOOD IDEA, to default to GET if nothing was specified?
74
+
75
+ verb = args[:verb].to_s.upcase
76
+ unless ["GET", "POST", "PUT", "DELETE", "HEAD"].include?(verb)
77
+ set_deferred_status :failed, {:status => 0} # TODO, not signalling the error type
78
+ return # NOTE THE EARLY RETURN, we're not sending any data.
79
+ end
80
+
81
+ request = args[:request] || "/"
82
+ # unless request[0,1] == "/"
83
+ # request = "/" + request
84
+ # end
85
+
86
+ qs = args[:query_string] || ""
87
+ if qs.length > 0 and qs[0,1] != '?'
88
+ qs = "?" + qs
89
+ end
90
+
91
+ # Allow an override for the host header if it's not the connect-string.
92
+ host = args[:host_header] || args[:host] || "_"
93
+ # For now, ALWAYS tuck in the port string, although we may want to omit it if it's the default.
94
+ port = args[:port]
95
+
96
+ # POST items.
97
+ postcontenttype = args[:contenttype] || "application/octet-stream"
98
+ postcontent = args[:content] || ""
99
+ raise "oversized content in HTTP POST" if postcontent.length > MaxPostContentLength
100
+
101
+ # ESSENTIAL for the request's line-endings to be CRLF, not LF. Some servers misbehave otherwise.
102
+ # TODO: We ASSUME the caller wants to send a 1.1 request. May not be a good assumption.
103
+ req = [
104
+ "#{verb} #{request}#{qs} HTTP/1.1",
105
+ "Host: #{host}:#{port}",
106
+ "User-agent: Ruby EventMachine",
107
+ ]
108
+
109
+ if verb == "POST" || verb == "PUT"
110
+ req << "Content-type: #{postcontenttype}"
111
+ req << "Content-length: #{postcontent.length}"
112
+ end
113
+
114
+ # TODO, this cookie handler assumes it's getting a single, semicolon-delimited string.
115
+ # Eventually we will want to deal intelligently with arrays and hashes.
116
+ if args[:cookie]
117
+ req << "Cookie: #{args[:cookie]}"
118
+ end
119
+
120
+ req << ""
121
+ reqstring = req.map {|l| "#{l}\r\n"}.join
122
+ send_data reqstring
123
+
124
+ if verb == "POST" || verb == "PUT"
125
+ send_data postcontent
126
+ end
127
+ end
128
+
129
+
130
+ def receive_data data
131
+ while data and data.length > 0
132
+ case @read_state
133
+ when :base
134
+ # Perform any per-request initialization here and don't consume any data.
135
+ @data = ""
136
+ @headers = []
137
+ @content_length = nil # not zero
138
+ @content = ""
139
+ @status = nil
140
+ @read_state = :header
141
+ when :header
142
+ ary = data.split( /\r?\n/m, 2 )
143
+ if ary.length == 2
144
+ data = ary.last
145
+ if ary.first == ""
146
+ if @content_length and @content_length > 0
147
+ @read_state = :content
148
+ else
149
+ dispatch_response
150
+ @read_state = :base
151
+ end
152
+ else
153
+ @headers << ary.first
154
+ if @headers.length == 1
155
+ parse_response_line
156
+ elsif ary.first =~ /\Acontent-length:\s*/i
157
+ # Only take the FIRST content-length header that appears,
158
+ # which we can distinguish because @content_length is nil.
159
+ # TODO, it's actually a fatal error if there is more than one
160
+ # content-length header, because the caller is presumptively
161
+ # a bad guy. (There is an exploit that depends on multiple
162
+ # content-length headers.)
163
+ @content_length ||= $'.to_i
164
+ end
165
+ end
166
+ else
167
+ @data << data
168
+ data = ""
169
+ end
170
+ when :content
171
+ # If there was no content-length header, we have to wait until the connection
172
+ # closes. Everything we get until that point is content.
173
+ # TODO: Must impose a content-size limit, and also must implement chunking.
174
+ # Also, must support either temporary files for large content, or calling
175
+ # a content-consumer block supplied by the user.
176
+ if @content_length
177
+ bytes_needed = @content_length - @content.length
178
+ @content += data[0, bytes_needed]
179
+ data = data[bytes_needed..-1] || ""
180
+ if @content_length == @content.length
181
+ dispatch_response
182
+ @read_state = :base
183
+ end
184
+ else
185
+ @content << data
186
+ data = ""
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+
193
+ # We get called here when we have received an HTTP response line.
194
+ # It's an opportunity to throw an exception or trigger other exceptional
195
+ # handling.
196
+ def parse_response_line
197
+ if @headers.first =~ /\AHTTP\/1\.[01] ([\d]{3})/
198
+ @status = $1.to_i
199
+ else
200
+ set_deferred_status :failed, {
201
+ :status => 0 # crappy way of signifying an unrecognized response. TODO, find a better way to do this.
202
+ }
203
+ close_connection
204
+ end
205
+ end
206
+ private :parse_response_line
207
+
208
+ def dispatch_response
209
+ @read_state = :base
210
+ set_deferred_status :succeeded, {
211
+ :content => @content,
212
+ :headers => @headers,
213
+ :status => @status
214
+ }
215
+ # TODO, we close the connection for now, but this is wrong for persistent clients.
216
+ close_connection
217
+ end
218
+
219
+ def unbind
220
+ if !@connected
221
+ set_deferred_status :failed, {:status => 0} # YECCCCH. Find a better way to signal no-connect/network error.
222
+ elsif (@read_state == :content and @content_length == nil)
223
+ dispatch_response
224
+ end
225
+ end
226
+ end
227
+
228
+ end
229
+ end
230
+
231
+
data/external/package.rb CHANGED
@@ -36,9 +36,9 @@ end
36
36
 
37
37
  def self.config(name)
38
38
  # XXX use pathname
39
- prefix = Regexp.quote(Config::CONFIG["prefix"])
40
- exec_prefix = Regexp.quote(Config::CONFIG["exec_prefix"])
41
- Config::CONFIG[name].gsub(/\A\/?(#{prefix}|#{exec_prefix})\/?/, '')
39
+ prefix = Regexp.quote(RbConfig::CONFIG["prefix"])
40
+ exec_prefix = Regexp.quote(RbConfig::CONFIG["exec_prefix"])
41
+ RbConfig::CONFIG[name].gsub(/\A\/?(#{prefix}|#{exec_prefix})\/?/, '')
42
42
  end
43
43
 
44
44
  SITE_DIRS = {
@@ -70,7 +70,7 @@ MODES = {
70
70
 
71
71
 
72
72
  SETUP_OPTIONS = {:parse_cmdline => true, :load_conf => true, :run_tasks => true}
73
- RUBY_BIN = File.join(::Config::CONFIG['bindir'],::Config::CONFIG['ruby_install_name']) << ::Config::CONFIG['EXEEXT']
73
+ RUBY_BIN = File.join(::RbConfig::CONFIG['bindir'],::RbConfig::CONFIG['ruby_install_name']) << ::RbConfig::CONFIG['EXEEXT']
74
74
 
75
75
  def self.setup(version, options = {}, &instructions)
76
76
  prefixes = dirs = nil
@@ -118,7 +118,7 @@ module Actions
118
118
  d = File.expand_path(File.join(dirs)).w32
119
119
  FileUtils.install @source, d,
120
120
  {:verbose => @options.verbose,
121
- :noop => @options.noop, :mode => @mode }
121
+ :noop => @options.noop, :mode => @mode } rescue Errno::ENOENT
122
122
  end
123
123
 
124
124
  def hash
@@ -197,9 +197,9 @@ module Actions
197
197
  File.open(tmpfile, 'w', 0755) do |w|
198
198
  first = r.gets
199
199
  return unless SHEBANG_RE =~ first
200
- ruby = File.join(::Config::CONFIG['bindir'],::Config::CONFIG['ruby_install_name'])
201
- ruby << ::Config::CONFIG['EXEEXT']
202
- #w.print first.sub(SHEBANG_RE, '#!' + Config::CONFIG['ruby-prog'])
200
+ ruby = File.join(::RbConfig::CONFIG['bindir'],::RbConfig::CONFIG['ruby_install_name'])
201
+ ruby << ::RbConfig::CONFIG['EXEEXT']
202
+ #w.print first.sub(SHEBANG_RE, '#!' + RbConfig::CONFIG['ruby-prog'])
203
203
  w.print first.sub(SHEBANG_RE, '#!' + ruby)
204
204
  w.write r.read
205
205
  end
@@ -308,7 +308,7 @@ class PackageSpecification_1_0
308
308
  end
309
309
  #TODO: refactor
310
310
  self.class.declare_file_type(args) do |files, ignore_p, opt_rename_info|
311
- files.each do |file|
311
+ (Array === files ? files : [files]).each do |file|
312
312
  next if ignore_p && IGNORE_FILES.any?{|re| re.match(file)}
313
313
  add_file(kind, file, opt_rename_info, &bin_callback)
314
314
  end
@@ -397,7 +397,7 @@ class PackageSpecification_1_0
397
397
  end
398
398
 
399
399
  def initialize(prefixes = nil, dirs = nil)
400
- @prefix = Config::CONFIG["prefix"].gsub(/\A\//, '')
400
+ @prefix = RbConfig::CONFIG["prefix"].gsub(/\A\//, '')
401
401
  @translate = {}
402
402
  @prefixes = (prefixes || {}).dup
403
403
  KINDS.each do |kind|
@@ -429,7 +429,7 @@ class PackageSpecification_1_0
429
429
  __send__ kind, Dir["#{kind}/**/*"]
430
430
  end
431
431
  translate(:ext, "ext/*" => "", :inherit => true)
432
- ext Dir["ext/**/*.#{Config::CONFIG['DLEXT']}"]
432
+ ext Dir["ext/**/*.#{RbConfig::CONFIG['DLEXT']}"]
433
433
  end
434
434
 
435
435
  # Builds any needed extensions.
@@ -660,7 +660,7 @@ class PackageSpecification_1_0
660
660
  end
661
661
 
662
662
  def run_tasks
663
- @tasks.each { |task| __send__ task }
663
+ @tasks.each { |task| puts "Doing #{task}"; __send__ task }
664
664
  end
665
665
  end
666
666
 
@@ -668,5 +668,5 @@ end # module Package
668
668
 
669
669
  require 'rbconfig'
670
670
  def config(x)
671
- Config::CONFIG[x]
671
+ RbConfig::CONFIG[x]
672
672
  end
data/setup.rb CHANGED
@@ -11,12 +11,25 @@ end
11
11
 
12
12
  Dir.chdir(basedir)
13
13
  Package.setup("1.0") {
14
- name "Swiftcore Swiftiply"
14
+ # TODO pull version right from the code's version.rb.
15
+ name "Swiftcore Swiftiply v. 0.6.5"
15
16
 
16
17
  build_ext "fastfilereader"
17
18
  translate(:ext, 'ext/fastfilereader/' => '/')
18
19
  #translate(:ext, 'ext/http11/' => 'iowa/')
20
+
19
21
  ext "ext/fastfilereader/fastfilereaderext.so"
22
+ ext "ext/fastfilereader/fastfilereaderext.bundle"
23
+
24
+ build_ext "deque"
25
+ translate(:ext, 'ext/deque/' => '/swiftcore/')
26
+ ext "ext/deque/deque.so"
27
+ ext "ext/deque/deque.bundle"
28
+
29
+ build_ext "splaytree"
30
+ translate(:ext, 'ext/splaytree/' => '/swiftcore/')
31
+ ext "ext/splaytree/splaytreemap.so"
32
+ ext "ext/splaytree/splaytreemap.bundle"
20
33
 
21
34
  translate(:lib, 'src/' => '')
22
35
  translate(:bin, 'bin/' => '')
@@ -28,9 +41,12 @@ Package.setup("1.0") {
28
41
  bin "bin/swiftiply"
29
42
  bin "bin/swiftiply_mongrel_rails"
30
43
  #File.rename("#{Config::CONFIG["bindir"]}/mongrel_rails","#{Config::CONFIG["bindir"]}/mongrel_rails.orig")
31
- bin "bin/mongrel_rails"
44
+ bin "bin/swiftiplied_mongrel_rails"
45
+ bin "bin/evented_mongrel_rails"
46
+ bin "bin/swiftiplyctl"
32
47
 
33
48
  unit_test "test/TC_ProxyBag.rb"
34
49
  unit_test "test/TC_Swiftiply.rb"
50
+ unit_test "test/TC_Deque.rb"
35
51
  true
36
52
  }
@@ -1,776 +1,420 @@
1
- begin
2
- load_attempted ||= false
3
- require 'digest/sha2'
4
- require 'eventmachine'
5
- require 'fastfilereaderext'
6
- require 'swiftcore/types'
7
- rescue LoadError => e
8
- unless load_attempted
9
- load_attempted = true
10
- # Ugh. Everything gets slower once rubygems are used. So, for the
11
- # best speed possible, don't install EventMachine or Swiftiply via
12
- # gems.
13
- require 'rubygems'
14
- retry
15
- end
16
- raise e
17
- end
18
-
19
1
  module Swiftcore
20
- module Swiftiply
21
- Version = '0.6.1.1'
22
-
23
- # Yeah, these constants look kind of tacky. Inside of tight loops,
24
- # though, using them makes a small but measurable difference, and those
25
- # small differences add up....
26
- C_empty = ''.freeze
27
- C_slash = '/'.freeze
28
- C_slashindex_html = '/index.html'.freeze
29
- Caos = 'application/octet-stream'.freeze
30
- Ccache_directory = 'cache_directory'.freeze
31
- Ccache_extensions = 'cache_extensions'.freeze
32
- Ccluster_address = 'cluster_address'.freeze
33
- Ccluster_port = 'cluster_port'.freeze
34
- Ccluster_server = 'cluster_server'.freeze
35
- CBackendAddress = 'BackendAddress'.freeze
36
- CBackendPort = 'BackendPort'.freeze
37
- Cchunked_encoding_threshold = 'chunked_encoding_threshold'.freeze
38
- Cdaemonize = 'daemonize'.freeze
39
- Cdefault = 'default'.freeze
40
- Cdocroot = 'docroot'.freeze
41
- Cepoll = 'epoll'.freeze
42
- Cepoll_descriptors = 'epoll_descriptors'.freeze
43
- Cgroup = 'group'.freeze
44
- Chost = 'host'.freeze
45
- Cincoming = 'incoming'.freeze
46
- Ckeepalive = 'keepalive'.freeze
47
- Ckey = 'key'.freeze
48
- Cmap = 'map'.freeze
49
- Cmsg_expired = 'browser connection expired'.freeze
50
- Coutgoing = 'outgoing'.freeze
51
- Cport = 'port'.freeze
52
- Credeployable = 'redeployable'.freeze
53
- Credeployment_sizelimit = 'redeployment_sizelimit'.freeze
54
- Cswiftclient = 'swiftclient'.freeze
55
- Ctimeout = 'timeout'.freeze
56
- Curl = 'url'.freeze
57
- Cuser = 'user'.freeze
58
-
59
- C_fsep = File::SEPARATOR
60
-
61
- RunningConfig = {}
62
-
63
- class EMStartServerError < RuntimeError; end
64
-
65
- # The ProxyBag is a class that holds the client and the server queues,
66
- # and that is responsible for managing them, matching them, and expiring
67
- # them, if necessary.
68
-
69
- class ProxyBag
70
- @client_q = Hash.new {|h,k| h[k] = []}
71
- @server_q = Hash.new {|h,k| h[k] = []}
72
- @ctime = Time.now
73
- @server_unavailable_timeout = 6
74
- @id_map = {}
75
- @reverse_id_map = {}
76
- @incoming_map = {}
77
- @docroot_map = {}
78
- @log_map = {}
79
- @redeployable_map = {}
80
- @keys = {}
81
- @demanding_clients = Hash.new {|h,k| h[k] = []}
82
- @hitcounters = Hash.new {|h,k| h[k] = 0}
83
- # Kids, don't do this at home. It's gross.
84
- @typer = MIME::Types.instance_variable_get('@__types__')
85
-
86
- class << self
87
-
88
- def now
89
- @ctime
90
- end
91
-
92
- # Returns the access key. If an access key is set, then all new backend
93
- # connections must send the correct access key before being added to
94
- # the cluster as a valid backend.
95
-
96
- def get_key(h)
97
- @keys[h] || C_empty
98
- end
99
-
100
- def set_key(h,val)
101
- @keys[h] = val
102
- end
103
-
104
- def add_id(who,what)
105
- @id_map[who] = what
106
- @reverse_id_map[what] = who
107
- end
108
-
109
- def remove_id(who)
110
- what = @id_map.delete(who)
111
- @reverse_id_map.delete(what)
112
- end
113
-
114
- def incoming_mapping(name)
115
- @incoming_map[name]
116
- end
117
-
118
- def add_incoming_mapping(hashcode,name)
119
- @incoming_map[name] = hashcode
120
- end
121
-
122
- def remove_incoming_mapping(name)
123
- @incoming_map.delete(name)
124
- end
125
-
126
- def add_incoming_docroot(path,name)
127
- @docroot_map[name] = path
128
- end
129
-
130
- def remove_incoming_docroot(name)
131
- @docroot_map.delete(name)
132
- end
133
-
134
- def add_incoming_redeployable(limit,name)
135
- @redeployable_map[name] = limit
136
- end
137
-
138
- def remove_incoming_redeployable(name)
139
- @redeployable_map.delete(name)
140
- end
141
-
142
- def add_log(log,name)
143
- @log_map[name] = log
144
- end
145
-
146
- # Sets the default proxy destination, if requests are received
147
- # which do not match a defined destination.
148
-
149
- def default_name
150
- @default_name
151
- end
152
-
153
- def default_name=(val)
154
- @default_name = val
155
- end
156
-
157
- # This timeout is the amount of time a connection will sit in queue
158
- # waiting for a backend to process it. A client connection that
159
- # sits for longer than this timeout receives a 503 response and
160
- # is dropped.
161
-
162
- def server_unavailable_timeout
163
- @server_unavailable_timeout
164
- end
165
-
166
- def server_unavailable_timeout=(val)
167
- @server_unavailable_timeout = val
168
- end
169
-
170
- # The chunked_encoding_threshold is a file size limit. Files
171
- # which fall below this limit are sent in one chunk of data.
172
- # Files which hit or exceed this limit are delivered via chunked
173
- # encoding.
174
-
175
- def chunked_encoding_threshold
176
- @chunked_enconding_threshold
177
- end
178
-
179
- def chunked_encoding_threshold=(val)
180
- @chunked_encoding_threshold = val
181
- end
182
-
183
- # Handle static files. It employs an extension to efficiently
184
- # handle large files, and depends on an addition to
185
- # EventMachine, send_file_data(), to efficiently handle small
186
- # files. In my tests, it streams in excess of 120 megabytes of
187
- # data per second for large files, and does 8000+ to 9000+
188
- # requests per second with small files (i.e. under 4k). I think
189
- # this can still be improved upon for small files.
190
- #
191
- # Todo for 0.7.0 -- add etag/if-modified/if-modified-since
192
- # support.
193
- #
194
- # TODO: Add support for logging static file delivery if wanted.
195
- # The ideal logging would probably be to Analogger since it'd
196
- # limit the performance impact of the the logging.
197
- #
198
-
199
- def serve_static_file(clnt)
200
- path_info = clnt.uri
201
- client_name = clnt.name
202
- dr = @docroot_map[client_name]
203
- if path = find_static_file(dr,path_info,client_name)
204
- #ct = ::MIME::Types.type_for(path).first || Caos
205
- ct = @typer.simple_type_for(path) || Caos
206
- fsize = File.size?(path)
207
- if fsize > @chunked_encoding_threshold
208
- clnt.send_data "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Type: #{ct}\r\nTransfer-encoding: chunked\r\n\r\n"
209
- EM::Deferrable.future(clnt.stream_file_data(path, :http_chunks=>true)) {clnt.close_connection_after_writing}
210
- else
211
- clnt.send_data "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Type: #{ct}\r\nContent-length: #{fsize}\r\n\r\n"
212
- clnt.send_file_data path
213
- clnt.close_connection_after_writing
214
- end
215
- true
216
- else
217
- false
218
- end
219
- # The exception is going to be eaten here, because some
220
- # dumb file IO error shouldn't take Swiftiply down.
221
- # TODO: It should log these errors, though.
222
- rescue Object
223
- clnt.close_connection_after_writing
224
- false
225
- end
226
-
227
- # Determine if the requested file, in the given docroot, exists
228
- # and is a file (i.e. not a directory).
229
- #
230
- # If Rails style page caching is enabled, this method will be
231
- # dynamically replaced by a more sophisticated version.
232
-
233
- def find_static_file(docroot,path_info,client_name)
234
- path = File.join(docroot,path_info)
235
- path if FileTest.exist?(path) and FileTest.file?(path) and File.expand_path(path).index(docroot) == 0
236
- end
237
-
238
- # Pushes a front end client (web browser) into the queue of clients
239
- # waiting to be serviced if there's no server available to handle
240
- # it right now.
241
-
242
- def add_frontend_client(clnt,data_q,data)
243
- clnt.create_time = @ctime
244
- clnt.data_pos = clnt.data_len = 0 if clnt.redeployable = @redeployable_map[clnt.name]
245
-
246
- unless @docroot_map.has_key?(clnt.name) and serve_static_file(clnt)
247
- data_q.unshift data
248
- unless match_client_to_server_now(clnt)
249
- if clnt.uri =~ /\w+-\w+-\w+\.\w+\.[\w\.]+-(\w+)?$/
250
- # NOTE: I hate using unshift and delete on arrays
251
- # in this code. Look at switching to something
252
- # with a faster profile for push/pull from both
253
- # ends as well as deletes. There has to be
254
- # something. A linked list solves the push/pull
255
- # which might be good enough.
256
- @demanding_clients[$1].unshift clnt
257
- else
258
- @client_q[@incoming_map[clnt.name]].unshift(clnt)
259
- end
260
- end
261
- #clnt.push ## wasted call, yes?
262
- end
263
- end
264
-
265
- def rebind_frontend_client(clnt)
266
- clnt.create_time = @ctime
267
- clnt.data_pos = clnt.data_len = 0
268
-
269
- unless match_client_to_server_now(clnt)
270
- if clnt.uri =~ /\w+-\w+-\w+\.\w+\.[\w\.]+-(\w+)?$/
271
- # NOTE: I hate using unshift and delete on arrays
272
- # in this code. Look at switching to something
273
- # with a faster profile for push/pull from both
274
- # ends as well as deletes. There has to be
275
- # something.
276
- @demanding_clients[$1].unshift clnt
277
- else
278
- @client_q[@incoming_map[clnt.name]].unshift(clnt)
279
- end
280
- end
281
- end
282
-
283
- # Pushes a backend server into the queue of servers waiting for a
284
- # client to service if there are no clients waiting to be serviced.
285
-
286
- def add_server srvr
287
- @server_q[srvr.name].unshift(srvr) unless match_server_to_client_now(srvr)
288
- end
289
-
290
- # Deletes the provided server from the server queue.
291
-
292
- def remove_server srvr
293
- @server_q[srvr.name].delete srvr
294
- end
295
-
296
- # Removes the named client from the client queue.
297
- # TODO: Try replacing this with ...something. Performance
298
- # here has to be bad when the list is long.
299
-
300
- def remove_client clnt
301
- @client_q[clnt.name].delete clnt
302
- end
303
-
304
- # Tries to match the client (passed as an argument) to a
305
- # server.
306
-
307
- def match_client_to_server_now(client)
308
- sq = @server_q[@incoming_map[client.name]]
309
- if client.uri =~ /\w+-\w+-\w+\.\w+\.[\w\.]+-(\w+)?$/
310
- if sidx = sq.index(@reverse_id_map[$1])
311
- server = sq.delete_at(sidx)
312
- server.associate = client
313
- client.associate = server
314
- client.push
315
- true
316
- else
317
- false
318
- end
319
- elsif server = sq.pop
320
- server.associate = client
321
- client.associate = server
322
- client.push
323
- true
324
- else
325
- false
326
- end
327
- end
328
-
329
- # Tries to match the server (passed as an argument) to a
330
- # client.
331
-
332
- def match_server_to_client_now(server)
333
- if client = @demanding_clients[server.id].pop
334
- server.associate = client
335
- client.associate = server
336
- client.push
337
- true
338
- elsif client = @client_q[server.name].pop
339
- server.associate = client
340
- client.associate = server
341
- client.push
342
- true
343
- else
344
- false
345
- end
346
- end
347
-
348
- # Walk through the waiting clients if there is no server
349
- # available to process clients and expire any clients that
350
- # have been waiting longer than @server_unavailable_timeout
351
- # seconds. Clients which are expired will receive a 503
352
- # response. If this is happening, either you need more
353
- # backend processes, or you @server_unavailable_timeout is
354
- # too short.
355
-
356
- def expire_clients
357
- now = Time.now
358
-
359
- @server_q.each_key do |name|
360
- unless @server_q[name].first
361
- while c = @client_q[name].pop
362
- if (now - c.create_time) >= @server_unavailable_timeout
363
- c.send_503_response
364
- else
365
- @client_q[name].push c
366
- break
367
- end
368
- end
369
- end
370
- end
371
- end
372
-
373
- # This is called by a periodic timer once a second to update
374
- # the time.
375
-
376
- def update_ctime
377
- @ctime = Time.now
378
- end
379
-
380
- end
381
- end
382
-
383
- # The ClusterProtocol is the subclass of EventMachine::Connection used
384
- # to communicate between Swiftiply and the web browser clients.
385
-
386
- class ClusterProtocol < EventMachine::Connection
387
-
388
- attr_accessor :create_time, :associate, :name, :redeployable, :data_pos, :data_len
389
-
390
- Crn = "\r\n".freeze
391
- Crnrn = "\r\n\r\n".freeze
392
- C_blank = ''.freeze
393
-
394
- # Initialize the @data array, which is the temporary storage for blocks
395
- # of data received from the web browser client, then invoke the superclass
396
- # initialization.
397
-
398
- def initialize *args
399
- @data = []
400
- @data_pos = 0
401
- @name = @uri = nil
402
- super
403
- end
404
-
405
- def receive_data data
406
- if @name
407
- @data.unshift data
408
- push
409
- else
410
- # Note the \0 below. intern() blows up when passed a \0. People who are trying to break a server like to pass \0s. This should cope with that.
411
- if data =~ /^Host:\s*([^\r\0:]*)/
412
- # NOTE: Should I be using intern for this? It might not
413
- # be a good idea.
414
- @name = $1.intern
415
-
416
- data =~ /\s([^\s\?]*)/
417
- @uri = $1
418
- @name = ProxyBag.default_name unless ProxyBag.incoming_mapping(@name)
419
- ProxyBag.add_frontend_client(self,@data,data)
420
- elsif data =~ /\r\n\r\n/
421
- @name = ProxyBag.default_name
422
- ProxyBag.add_frontend_client(self,@data,data)
423
- end
424
- end
425
- end
426
-
427
- # Hardcoded 503 response that is sent if a connection is timed out while
428
- # waiting for a backend to handle it.
429
-
430
- def send_503_response
431
- send_data [
432
- "HTTP/1.0 503 Server Unavailable\r\n",
433
- "Content-type: text/plain\r\n",
434
- "Connection: close\r\n",
435
- "\r\n",
436
- "Server Unavailable"
437
- ].join
438
- close_connection_after_writing
439
- end
440
-
441
- # Push data from the web browser client to the backend server process.
442
-
443
- def push
444
- if @associate
445
- unless @redeployable
446
- # normal data push
447
- data = nil
448
- @associate.send_data data while data = @data.pop
449
- else
450
- # redeployable data push; just send the stuff that has
451
- # not already been sent.
452
- (@data.length - 1 - @data_pos).downto(0) do |p|
453
- d = @data[p]
454
- @associate.send_data d
455
- @data_len += d.length
456
- end
457
- @data_pos = @data.length
458
-
459
- # If the request size crosses the size limit, then
460
- # disallow redeployent of this request.
461
- if @data_len > @redeployable
462
- @redeployable = false
463
- @data.clear
464
- end
465
- end
466
- end
467
- end
468
-
469
- # The connection with the web browser client has been closed, so the
470
- # object must be removed from the ProxyBag's queue if it is has not
471
- # been associated with a backend. If it has already been associated
472
- # with a backend, then it will not be in the queue and need not be
473
- # removed.
474
-
475
- def unbind
476
- ProxyBag.remove_client(self) unless @associate
477
- end
478
-
479
- def uri
480
- @uri
481
- end
482
-
483
- def setup_for_redeployment
484
- @data_pos = 0
485
- end
486
-
487
- end
488
-
489
- # The BackendProtocol is the EventMachine::Connection subclass that
490
- # handles the communications between Swiftiply and the backend process
491
- # it is proxying to.
492
-
493
- class BackendProtocol < EventMachine::Connection
494
- attr_accessor :associate, :id
495
-
496
- C0rnrn = "0\r\n\r\n".freeze
497
- Crnrn = "\r\n\r\n".freeze
498
-
499
- def initialize *args
500
- @name = self.class.bname
501
- super
502
- end
503
-
504
- def name
505
- @name
506
- end
507
-
508
- # Call setup() and add the backend to the ProxyBag queue.
509
-
510
- def post_init
511
- setup
512
- @initialized = nil
513
- ProxyBag.add_server self
514
- end
515
-
516
- # Setup the initial variables for receiving headers and content.
517
-
518
- def setup
519
- @headers = ''
520
- @headers_completed = false
521
- #@content_length = nil
522
- @content_sent = 0
523
- end
524
-
525
- # Receive data from the backend process. Headers are parsed from
526
- # the rest of the content. If a Content-Length header is present,
527
- # that is used to determine how much data to expect. Otherwise,
528
- # if 'Transfer-encoding: chunked' is present, assume chunked
529
- # encoding. Otherwise be paranoid; something isn't the way we like
530
- # it to be.
531
-
532
- def receive_data data
533
- unless @initialized
534
- preamble = data.slice!(0..24)
535
-
536
- keylen = preamble[23..24].to_i(16)
537
- keylen = 0 if keylen < 0
538
- key = keylen > 0 ? data.slice!(0..(keylen - 1)) : C_empty
539
- if preamble[0..10] == Cswiftclient and key == ProxyBag.get_key(@name)
540
- @id = preamble[11..22]
541
- ProxyBag.add_id(self,@id)
542
- @initialized = true
543
- else
544
- close_connection
545
- return
546
- end
547
- end
548
-
549
- unless @headers_completed
550
- if data =~ /\r\n\r\n/
551
- @headers_completed = true
552
- h,data = data.split(/\r\n\r\n/,2)
553
- @headers << h << Crnrn
554
- if @headers =~ /Content-[Ll]ength:\s*([^\r]+)/
555
- @content_length = $1.to_i
556
- elsif @headers =~ /Transfer-encoding:\s*chunked/
557
- @content_length = nil
558
- else
559
- @content_length = 0
560
- end
561
- @associate.send_data @headers
562
- else
563
- @headers << data
564
- end
565
- end
566
-
567
- if @headers_completed
568
- @associate.send_data data
569
- @content_sent += data.length
570
- if @content_length and @content_sent >= @content_length or data[-6..-1] == C0rnrn
571
- @associate.close_connection_after_writing
572
- @associate = nil
573
- @headers = ''
574
- @headers_completed = false
575
- #@content_length = nil
576
- @content_sent = 0
577
- #setup
578
- ProxyBag.add_server self
579
- end
580
- end
581
- # TODO: Log these errors!
582
- rescue
583
- @associate.close_connection_after_writing if @associate
584
- @associate = nil
585
- setup
586
- ProxyBag.add_server self
587
- end
588
-
589
- # This is called when the backend disconnects from the proxy.
590
- # If the backend is currently associated with a web browser client,
591
- # that connection will be closed. Otherwise, the backend will be
592
- # removed from the ProxyBag's backend queue.
593
-
594
- def unbind
595
- if @associate
596
- if !@associate.redeployable or @content_length
597
- @associate.close_connection_after_writing
598
- else
599
- @associate.associate = nil
600
- @associate.setup_for_redeployment
601
- ProxyBag.rebind_frontend_client(@associate)
602
- end
603
- else
604
- ProxyBag.remove_server(self)
605
- end
606
- ProxyBag.remove_id(self)
607
- end
608
-
609
- def self.bname=(val)
610
- @bname = val
611
- end
612
-
613
- def self.bname
614
- @bname
615
- end
616
- end
617
-
618
- # Start the EventMachine event loop and create the front end and backend
619
- # handlers, then create the timers that are used to expire unserviced
620
- # clients and to update the Proxy's clock.
621
-
622
- def self.run(config)
623
- @existing_backends = {}
624
-
625
- # Default is to assume we want to try to turn epoll support on. EM
626
- # ignores this on platforms that don't support it, so this is safe.
627
- EventMachine.epoll unless config.has_key?(Cepoll) and !config[Cepoll]
628
- EventMachine.set_descriptor_table_size(4096 || config[Cepoll_descriptors]) if config[Cepoll]
629
- EventMachine.run do
630
- trap("HUP") {em_config(Swiftcore::SwiftiplyExec.parse_options); GC.start}
631
- trap("INT") {EventMachine.stop_event_loop}
632
- em_config(config)
633
- GC.start
634
- end
635
- end
636
-
637
- def self.em_config(config)
638
- new_config = {}
639
- if RunningConfig[Ccluster_address] != config[Ccluster_address] or RunningConfig[Ccluster_port] != config[Ccluster_port]
640
- begin
641
- new_config[Ccluster_server] = EventMachine.start_server(
642
- config[Ccluster_address],
643
- config[Ccluster_port],
644
- ClusterProtocol)
645
- rescue RuntimeError => e
646
- advice = ''
647
- if config[Ccluster_port] < 1024
648
- advice << 'Make sure you have the correct permissions to use that port, and make sure there is nothing else running on that port.'
649
- else
650
- advice << 'Make sure there is nothing else running on that port.'
651
- end
652
- advice << " The original error was: #{e}\n"
653
- raise EMStartServerError.new("The listener on #{config[Ccluster_address]}:#{config[Ccluster_port]} could not be started.\n#{advice}")
654
- end
655
- new_config[Ccluster_address] = config[Ccluster_address]
656
- new_config[Ccluster_port] = config[Ccluster_port]
657
- RunningConfig[Ccluster_server].stop_server if RunningConfig.has_key?(Ccluster_server)
658
- else
659
- new_config[Ccluster_server] = RunningConfig[Ccluster_server]
660
- new_config[Ccluster_address] = RunningConfig[Ccluster_address]
661
- new_config[Ccluster_port] = RunningConfig[Ccluster_port]
662
- end
663
-
664
- new_config[Coutgoing] = {}
665
-
666
- config[Cmap].each do |m|
667
- if m[Ckeepalive]
668
- # keepalive requests are standard Swiftiply requests.
669
-
670
- # The hash of the "outgoing" config section. It is used to
671
- # uniquely identify a section.
672
- hash = Digest::SHA256.hexdigest(m[Cincoming].sort.join('|')).intern
673
-
674
- # For each incoming entry, do setup.
675
- new_config[Cincoming] = {}
676
- m[Cincoming].each do |p_|
677
- p = p_.intern
678
- new_config[Cincoming][p] = {}
679
- ProxyBag.add_incoming_mapping(hash,p)
680
-
681
- if m.has_key?(Cdocroot)
682
- ProxyBag.add_incoming_docroot(m[Cdocroot],p)
683
- else
684
- ProxyBag.remove_incoming_docroot(p)
685
- end
686
-
687
- if m[Credeployable]
688
- ProxyBag.add_incoming_redeployable(m[Credeployment_sizelimit] || 16384,p)
689
- else
690
- ProxyBag.remove_incoming_redeployable(p)
691
- end
692
-
693
- if m.has_key?(Ckey)
694
- ProxyBag.set_key(hash,m[Ckey])
695
- else
696
- ProxyBag.set_key(hash,C_empty)
697
- end
698
-
699
- if m.has_key?(Ccache_extensions) or m.has_key?(Ccache_directory)
700
- require 'swiftcore/Swiftiply/support_pagecache'
701
- ProxyBag.add_suffix_list((m[Ccache_extensions] || ProxyBag.const_get(:DefaultSuffixes)),p)
702
- ProxyBag.add_cache_dir((m[Ccache_directory] || ProxyBag.const_get(:DefaultCacheDir)),p)
703
- else
704
- ProxyBag.remove_suffix_list(p) if ProxyBag.respond_to?(:remove_suffix_list)
705
- ProxyBag.remove_cache_dir(p) if ProxyBag.respond_to?(:remove_cache_dir)
706
- end
707
-
708
- m[Coutgoing].each do |o|
709
- ProxyBag.default_name = p if m[Cdefault]
710
- if @existing_backends.has_key?(o)
711
- new_config[Coutgoing][o] ||= RunningConfig[Coutgoing][o]
712
- next
713
- else
714
- @existing_backends[o] = true
715
- backend_class = Class.new(BackendProtocol)
716
- backend_class.bname = hash
717
- host, port = o.split(/:/,2)
718
- begin
719
- new_config[Coutgoing][o] = EventMachine.start_server(host, port.to_i, backend_class)
720
- rescue RuntimeError => e
721
- advice = ''
722
- if port.to_i < 1024
723
- advice << 'Make sure you have the correct permissions to use that port, and make sure there is nothing else running on that port.'
724
- else
725
- advice << 'Make sure there is nothing else running on that port.'
726
- end
727
- advice << " The original error was: #{e}\n"
728
- raise EMStartServerError.new("The listener on #{host}:#{port} could not be started.\n#{advice}")
729
- end
730
- end
731
- end
732
-
733
- # Now stop everything that is still running but which isn't needed.
734
- if RunningConfig.has_key?(Coutgoing)
735
- (RunningConfig[Coutgoing].keys - new_config[Coutgoing].keys).each do |unneeded_server_key|
736
- RunningConfig[Coutgoing][unneeded_server_key].stop_server
737
- end
738
- end
739
- end
740
- else
741
- # This is where the code goes that sets up traditional proxy destinations.
742
- # This is a future TODO item.
743
- end
744
- end
745
-
746
- #EventMachine.set_effective_user = config[Cuser] if config[Cuser] and RunningConfig[Cuser] != config[Cuser]
747
- run_as(config[Cuser],config[Cgroup]) if (config[Cuser] and RunningConfig[Cuser] != config[Cuser]) or (config[Cgroup] and RunningConfig[Cgroup] != config[Cgroup])
748
- new_config[Cuser] = config[Cuser]
749
- new_config[Cgroup] = config[Cgroup]
750
-
751
- ProxyBag.server_unavailable_timeout ||= config[Ctimeout]
752
- ProxyBag.chunked_encoding_threshold = config[Cchunked_encoding_threshold] || 16384
753
-
754
- unless RunningConfig[:initialized]
755
- EventMachine.add_periodic_timer(2) { ProxyBag.expire_clients }
756
- EventMachine.add_periodic_timer(1) { ProxyBag.update_ctime }
757
- new_config[:initialized] = true
758
- end
759
-
760
- RunningConfig.replace new_config
761
- end
762
-
763
-
764
- # This can be used to change the effective user and group that
765
- # Swiftiply is running as.
766
-
767
- def self.run_as(user = "nobody", group = "nobody")
768
- Process.initgroups(user,Etc.getgrnam(group).gid) if user and group
769
- ::Process::GID.change_privilege(Etc.getgrnam(group).gid) if group
770
- ::Process::UID.change_privilege(Etc.getpwnam(user).uid) if user
771
- rescue Errno::EPERM
772
- raise "Failed to change the effective user to #{user} and the group to #{group}"
773
- end
774
- end
2
+ # TODO:
3
+ #
4
+ # 1) Basic HTTP Authentication
5
+ # 2) Stats
6
+ # Stats will be recorded in aggregate and for each incoming section, and may
7
+ # accessed through a separate stats port via a RESTful HTTP request which
8
+ # identifies the section to pull stats for, and the authentication key for
9
+ # access to those stats.
10
+ # http://127.0.0.1:8082
11
+ #
12
+ # To track:
13
+ # Total connections
14
+ # 400s served
15
+ # 404s served
16
+ #
17
+ # Per config section:
18
+ # backends connected
19
+ # backends busy
20
+ # backend disconnects
21
+ # backend errors
22
+ # static bytes served
23
+ # static requests handled
24
+ # static requests 304'd
25
+ # cache hits for static files
26
+ # dynamic bytes returned
27
+ # dynamic requests handled
28
+ #
29
+ #
30
+ #
31
+ # 3) Maintenance Page Support
32
+ # This is a path to a static file which will be returned on a 503 error.
33
+ # 4) GZip compression
34
+ # Can be toggled on or off. Configure mime types to compress. Implemented
35
+ # via an extension.
36
+ # 5) Make one "SwiftiplyCplusplus" and one "SwiftiplC" extension that,
37
+ # respectively, encapsulate all of the C++ and C extensions into just
38
+ # two.
39
+
40
+ # A little statemachine for loading requirements. The intention is to
41
+ # only load rubygems if necessary, and to load the Deque and SplayTreeMap
42
+ # classes if they are available, setting a constant accordingly so that
43
+ # the fallbacks (Array and Hash) can be used if they are not.
44
+
45
+ begin
46
+ load_state ||= :start
47
+ rubygems_loaded ||= false
48
+ require 'socket'
49
+ require 'digest/sha2'
50
+ require 'eventmachine'
51
+ require 'swiftcore/hash'
52
+ require 'swiftcore/types'
53
+ require 'swiftcore/Swiftiply/mocklog'
54
+ require 'swiftcore/Swiftiply/version'
55
+
56
+ load_state = :deque
57
+ require 'swiftcore/deque' unless const_defined?(:HasDeque)
58
+ HasDeque = true unless const_defined?(:HasDeque)
59
+
60
+ load_state = :splaytreemap
61
+ require 'swiftcore/splaytreemap' unless const_defined?(:HasSplayTree)
62
+ HasSplayTree = true unless const_defined?(:HasSplayTree)
63
+
64
+ load_state = :helpers
65
+ require 'swiftcore/streamer'
66
+ require 'swiftcore/Swiftiply/etag_cache'
67
+ require 'swiftcore/Swiftiply/file_cache'
68
+ require 'swiftcore/Swiftiply/dynamic_request_cache'
69
+ require 'time'
70
+
71
+ load_state = :core
72
+ require 'swiftcore/Swiftiply/constants'
73
+ require 'swiftcore/Swiftiply/proxy_bag'
74
+ require 'swiftcore/Swiftiply/cluster_protocol'
75
+ require 'swiftcore/Swiftiply/proxy'
76
+
77
+ rescue LoadError => e
78
+ unless rubygems_loaded
79
+ # Everything gets slower once rubygems are used (though this
80
+ # effect is not so profound as it once was). So, for the
81
+ # best speed possible, don't install EventMachine or Swiftiply via
82
+ # gems.
83
+ begin
84
+ require 'rubygems'
85
+ rubygems_loaded = true
86
+ rescue LoadError
87
+ raise e
88
+ end
89
+ retry
90
+ end
91
+ case load_state
92
+ when :deque
93
+ HasDeque = false unless const_defined?(:HasDeque)
94
+ retry
95
+ when :splaytreemap
96
+ HasSplayTree = false unless const_defined?(:HasSplayTree)
97
+ retry
98
+ end
99
+ raise e
100
+ end
101
+
102
+ GC.start
103
+
104
+ module Swiftiply
105
+
106
+ Updaters = {
107
+ 'rest' => ['swiftcore/Swiftiply/config/rest_updater','::Swiftcore::Swiftiply::Config::RestUpdater']
108
+ }
109
+
110
+ def self.existing_backends
111
+ @existing_backends
112
+ end
113
+
114
+ def self.existing_backends=(val)
115
+ @existing_backends = val
116
+ end
117
+
118
+ # Start the EventMachine event loop and create the front end and backend
119
+ # handlers, then create the timers that are used to expire unserviced
120
+ # clients and to update the Proxy's clock.
121
+
122
+ def self.run(config)
123
+ self.existing_backends = {}
124
+
125
+ # Default is to assume we want to try to turn epoll/kqueue support on.
126
+ EventMachine.epoll unless config.has_key?(Cepoll) and !config[Cepoll] rescue nil
127
+ EventMachine.kqueue unless config.has_key?(Ckqueue) and !config[Ckqueue] rescue nil
128
+ EventMachine.set_descriptor_table_size(config[Cepoll_descriptors] || config[Cdescriptors] || 4096) rescue nil
129
+
130
+ EventMachine.run do
131
+ EM.set_timer_quantum(5)
132
+ trap("HUP") {em_config(Swiftcore::SwiftiplyExec.parse_options); GC.start}
133
+ trap("INT") {EventMachine.stop_event_loop}
134
+ GC.start
135
+ em_config(config)
136
+ GC.start # We just want to make sure all the junk created during
137
+ # configuration is purged prior to real work starting.
138
+ #RubyProf.start
139
+ end
140
+ #result = RubyProf.stop
141
+
142
+ #printer = RubyProf::TextPrinter.new(result)
143
+ #File.open('/tmp/swprof','w+') {|fh| printer = printer.print(fh,0)}
144
+ end
145
+
146
+ def self.log_level
147
+ @log_level
148
+ end
149
+
150
+ def self.log_level=(val)
151
+ @log_level = val
152
+ end
153
+
154
+ # TODO: This method is absurdly long, and should be refactored.
155
+ def self.em_config(config)
156
+ new_config = {Ccluster_address => [],Ccluster_port => [],Ccluster_server => {}}
157
+ defaults = config['defaults'] || {}
158
+
159
+ new_log = _config_loggers(config,defaults)
160
+ self.log_level = ProxyBag.log_level
161
+ ssl_addresses = _config_determine_ssl_addresses(config)
162
+
163
+ addresses = (Array === config[Ccluster_address]) ? config[Ccluster_address] : [config[Ccluster_address]]
164
+ ports = (Array === config[Ccluster_port]) ? config[Ccluster_port] : [config[Ccluster_port]]
165
+ addrports = []
166
+
167
+ addresses.each do |address|
168
+ ports.each do |port|
169
+ addrport = "#{address}:#{port}"
170
+ addrports << addrport
171
+
172
+ if (!RunningConfig.has_key?(Ccluster_address)) ||
173
+ (RunningConfig.has_key?(Ccluster_address) && !RunningConfig[Ccluster_address].include?(address)) ||
174
+ (RunningConfig.has_key?(Ccluster_port) && !RunningConfig[Ccluster_port].include?(port))
175
+ begin
176
+ # If this particular address/port runs SSL, check that the certificate and the
177
+ # key files exist and are readable, then create a special protocol class
178
+ # that embeds the certificate and key information.
179
+
180
+ if ssl_addresses.has_key?(addrport)
181
+ # TODO: LOG that the certfiles are missing instead of silently ignoring it.
182
+ next unless exists_and_is_readable(ssl_addresses[addrport][Ccertfile])
183
+ next unless exists_and_is_readable(ssl_addresses[addrport][Ckeyfile])
184
+
185
+ # Create a customized protocol object for each different address/port combination.
186
+ ssl_protocol = Class.new(ClusterProtocol)
187
+ ssl_protocol.class_eval <<EOC
188
+ def post_init
189
+ start_tls({:cert_chain_file => "#{ssl_addresses[addrport][Ccertfile]}", :private_key_file => "#{ssl_addresses[addrport][Ckeyfile]}"})
190
+ end
191
+ EOC
192
+ ProxyBag.logger.log(Cinfo,"Opening SSL server on #{address}:#{port}") if log_level > 0 and log_level < 3
193
+ ProxyBag.logger.log(Cinfo,"Opening SSL server on #{address}:#{port} using key at #{ssl_addresses[addrport][Ckeyfile]} and certificate at #{ssl_addresses[addrport][Ccertfile]}")
194
+ new_config[Ccluster_server][addrport] = EventMachine.start_server(
195
+ address,
196
+ port,
197
+ ssl_protocol)
198
+ else
199
+ standard_protocol = Class.new(ClusterProtocol)
200
+ standard_protocol.init_class_variables
201
+ ProxyBag.logger.log(Cinfo,"Opening server on #{address}:#{port}") if ProxyBag.log_level > 0
202
+ new_config[Ccluster_server][addrport] = EventMachine.start_server(
203
+ address,
204
+ port,
205
+ standard_protocol)
206
+ end
207
+ rescue RuntimeError => e
208
+ advice = ''
209
+ if port < 1024
210
+ advice << 'Make sure you have the correct permissions to use that port, and make sure there is nothing else running on that port.'
211
+ else
212
+ advice << 'Make sure there is nothing else running on that port.'
213
+ end
214
+ advice << " The original error was: #{e}\n"
215
+ msg = "The listener on #{address}:#{port} could not be started.\n#{advice}\n"
216
+ ProxyBag.logger.log('fatal',msg)
217
+ raise EMStartServerError.new(msg)
218
+ end
219
+
220
+ new_config[Ccluster_address] << address
221
+ new_config[Ccluster_port] << port unless new_config[Ccluster_port].include?(port)
222
+ else
223
+ new_config[Ccluster_server][addrport] = RunningConfig[Ccluster_server][addrport]
224
+ new_config[Ccluster_address] << address
225
+ new_config[Ccluster_port] << port unless new_config[Ccluster_port].include?(port)
226
+ end
227
+ end
228
+ end
229
+
230
+ # Stop anything that is no longer in the config.
231
+ if RunningConfig.has_key?(Ccluster_server)
232
+ (RunningConfig[Ccluster_server].keys - addrports).each do |s|
233
+ ProxyBag.logger.log(Cinfo,"Stopping unused incoming server #{s.inspect} out of #{RunningConfig[Ccluster_server].keys.inspect - RunningConfig[Ccluster_server].keys.inspect}")
234
+ EventMachine.stop_server(s)
235
+ end
236
+ end
237
+
238
+ new_config[Coutgoing] = {}
239
+ config[Cmap].each do |mm|
240
+ m = defaults.dup
241
+ m.rmerge!(mm)
242
+ Swiftcore::Swiftiply::Proxy.config(m,new_config)
243
+ end
244
+
245
+ updater = nil
246
+ if config[Cupdates]
247
+ uconf = config[Cupdates]
248
+ require Updaters[uconf[Cupdater]].first
249
+ updater = Updaters[uconf[Cupdater]].last
250
+ end
251
+
252
+ updater_class = Swiftcore::Swiftiply::class_by_name(updater) if updater
253
+ self.class.const_set(:Updater, updater_class.new(uconf)) if updater
254
+
255
+ #EventMachine.set_effective_user = config[Cuser] if config[Cuser] and RunningConfig[Cuser] != config[Cuser]
256
+ run_as(config[Cuser],config[Cgroup]) if (config[Cuser] and RunningConfig[Cuser] != config[Cuser]) or (config[Cgroup] and RunningConfig[Cgroup] != config[Cgroup])
257
+ new_config[Cuser] = config[Cuser]
258
+ new_config[Cgroup] = config[Cgroup]
259
+
260
+ ProxyBag.server_unavailable_timeout ||= config[Ctimeout]
261
+
262
+ # By default any file over 16k will be sent via chunked encoding
263
+ # if the client supports HTTP 1.1. Generally there is no reason
264
+ # to change this, but it is configurable.
265
+
266
+ puts "CHUNKED ENCODING THRESHOLD: #{config[Cchunked_encoding_threshold] || 16384}"
267
+ ProxyBag.chunked_encoding_threshold = config[Cchunked_encoding_threshold] || 16384
268
+
269
+ # The default cache_threshold is set to 100k. Files above this size
270
+ # will not be cached. Customize this value in your configurations
271
+ # as necessary for the best performance on your site.
272
+
273
+ ProxyBag.cache_threshold = config['cache_threshold'] || 102400
274
+
275
+ unless RunningConfig[:initialized]
276
+ EventMachine.add_periodic_timer(0.1) { ProxyBag.recheck_or_expire_clients }
277
+ #EventMachine.next_tick { ProxyBag.do_and_requeue_recheck_or_expire_clients }
278
+ EventMachine.add_periodic_timer(1) { ProxyBag.update_ctime }
279
+ EventMachine.add_periodic_timer(1) { ProxyBag.request_worker_resources }
280
+ new_config[:initialized] = true
281
+ end
282
+
283
+ Updater.start if updater
284
+ RunningConfig.replace new_config
285
+ end
286
+
287
+ def self._config_loggers(config,defaults)
288
+ if defaults['logger']
289
+ if config['logger']
290
+ config['logger'].rmerge!(defaults['logger'])
291
+ else
292
+ config['logger'] = {}.rmerge!(defaults['logger'])
293
+ end
294
+ else
295
+ config['logger'] = {'log_level' => 0, 'type' => 'stderror'} unless config['logger']
296
+ end
297
+
298
+ new_log = handle_logger_config(config['logger']) if config['logger']
299
+ ProxyBag.logger = new_log[:logger] if new_log
300
+ ProxyBag.log_level = new_log[:log_level] if new_log
301
+ new_log
302
+ end
303
+
304
+ def self._config_determine_ssl_addresses(config)
305
+ ssl_addresses = {}
306
+ # Determine which address/port combos should be running SSL.
307
+ (config[Cssl] || []).each do |sa|
308
+ if sa.has_key?(Cat)
309
+ ssl_addresses[sa[Cat]] = {Ccertfile => sa[Ccertfile], Ckeyfile => sa[Ckeyfile]}
310
+ end
311
+ end
312
+ ssl_addresses
313
+ end
314
+
315
+ # This can be used to change the effective user and group that
316
+ # Swiftiply is running as.
317
+
318
+ def self.run_as(user = "nobody", group = "nobody")
319
+ Process.initgroups(user,Etc.getgrnam(group).gid) if user and group
320
+ ::Process::GID.change_privilege(Etc.getgrnam(group).gid) if group
321
+ ::Process::UID.change_privilege(Etc.getpwnam(user).uid) if user
322
+ rescue Errno::EPERM
323
+ raise "Failed to change the effective user to #{user} and the group to #{group}"
324
+ end
325
+
326
+ def self.exists_and_is_readable(file)
327
+ FileTest.exist?(file) and FileTest.readable?(file)
328
+ end
329
+
330
+ # There are 4 levels of logging supported.
331
+ # :disabled or 0 means no logging
332
+ # :minimal or 1 logs only essential items
333
+ # :normal or 2 logs everything useful/interesting
334
+ # :full or 3 logs all major events
335
+ #
336
+ def self.determine_log_level(lvl)
337
+ case lvl.to_s
338
+ when /^d|0/
339
+ 0
340
+ when /^m|1/
341
+ 1
342
+ when /^n|2/
343
+ 2
344
+ when /^f|3/
345
+ 3
346
+ else
347
+ 1
348
+ end
349
+ end
350
+
351
+ def self.get_const_from_name(name,space)
352
+ r = nil
353
+ space.constants.each do |c|
354
+ if c =~ /#{name}/i
355
+ r = c
356
+ break
357
+ end
358
+ end
359
+ "#{space.name}::#{r}".split('::').inject(Object) { |o,n| o.const_get n }
360
+ end
361
+
362
+ def self.class_by_name(name)
363
+ klass = Object
364
+ name.sub(/^::/,'').split('::').each {|n| klass = klass.const_get n}
365
+ klass
366
+ end
367
+
368
+ def self.handle_logger_config(logger_config = nil,handle_default = true)
369
+ new_logger = {}
370
+ if logger_config
371
+ type = logger_config['type'] || 'Analogger'
372
+ begin
373
+ load_attempted ||= false
374
+ require "swiftcore/Swiftiply/loggers/#{type}"
375
+ rescue LoadError
376
+ if load_attempted
377
+ raise SwiftiplyLoggerNotFound.new("The logger that was specified, #{type}, could not be found.")
378
+ else
379
+ load_attempted = true
380
+ require 'rubygems'
381
+ retry
382
+ end
383
+ end
384
+ new_logger[:log_level] = determine_log_level(logger_config['level'] || logger_config['log_level'])
385
+ begin
386
+ log_class = get_const_from_name(type,::Swiftcore::Swiftiply::Loggers)
387
+
388
+ new_logger[:logger] = log_class.new(logger_config)
389
+ new_logger[:logger].log(Cinfo,"Logger type #{type} started; log level is #{new_logger[:log_level]}.") if new_logger[:log_level] > 0
390
+ rescue NameError
391
+ raise SwiftiplyLoggerNameError.new("The logger class specified, Swiftcore::Swiftiply::Loggers::#{type} was not defined.")
392
+ end
393
+ elsif handle_default
394
+ # Default to the stderror logger with a log level of 0
395
+ begin
396
+ load_attempted ||= false
397
+ require "swiftcore/Swiftiply/loggers/stderror"
398
+ rescue LoadError
399
+ if load_attempted
400
+ raise SwiftiplyLoggerNotFound.new("The attempt to load the default logger (swiftcore/Swiftiply/loggers/stderror.rb) failed. This should not happen. Please double check your Swiftiply installation.")
401
+ else
402
+ load_attempted = true
403
+ require 'rubygems'
404
+ retry
405
+ end
406
+ end
407
+
408
+ log_class = get_const_from_name('stderror',::Swiftcore::Swiftiply::Loggers)
409
+ new_logger[:logger] = log_class.new(logger_config)
410
+ new_logger[:log_level] = log_level
411
+ else
412
+ new_logger = nil
413
+ end
414
+
415
+ new_logger
416
+ end
417
+
418
+ end
775
419
  end
776
420