swiftiply 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. data/README +126 -0
  2. data/bin/mongrel_rails +254 -0
  3. data/bin/swiftiply +136 -0
  4. data/bin/swiftiply_mongrel_rails +54 -0
  5. data/external/package.rb +672 -0
  6. data/external/test_support.rb +58 -0
  7. data/setup.rb +40 -0
  8. data/src/ramaze/adapter/evented_mongrel.rb +2 -0
  9. data/src/ramaze/adapter/swiftiplied_mongrel.rb +2 -0
  10. data/src/swiftcore/Swiftiply.rb +444 -0
  11. data/src/swiftcore/Swiftiply.rb.orig +390 -0
  12. data/src/swiftcore/evented_mongrel.rb +182 -0
  13. data/src/swiftcore/swiftiplied_mongrel.rb +212 -0
  14. data/swiftiply.gemspec +41 -0
  15. data/test/rails/README +182 -0
  16. data/test/rails/Rakefile +10 -0
  17. data/test/rails/app/controllers/application.rb +6 -0
  18. data/test/rails/app/controllers/tests_controller.rb +15 -0
  19. data/test/rails/app/helpers/application_helper.rb +3 -0
  20. data/test/rails/config/boot.rb +45 -0
  21. data/test/rails/config/database.yml +36 -0
  22. data/test/rails/config/environment.rb +60 -0
  23. data/test/rails/config/environments/development.rb +21 -0
  24. data/test/rails/config/environments/production.rb +18 -0
  25. data/test/rails/config/environments/production_no_caching.rb +18 -0
  26. data/test/rails/config/environments/test.rb +19 -0
  27. data/test/rails/config/routes.rb +23 -0
  28. data/test/rails/doc/README_FOR_APP +2 -0
  29. data/test/rails/observe_ram.rb +10 -0
  30. data/test/rails/public/404.html +30 -0
  31. data/test/rails/public/500.html +30 -0
  32. data/test/rails/public/dispatch.cgi +10 -0
  33. data/test/rails/public/dispatch.fcgi +24 -0
  34. data/test/rails/public/dispatch.rb +10 -0
  35. data/test/rails/public/favicon.ico +0 -0
  36. data/test/rails/public/images/rails.png +0 -0
  37. data/test/rails/public/index.html +277 -0
  38. data/test/rails/public/javascripts/application.js +2 -0
  39. data/test/rails/public/javascripts/controls.js +833 -0
  40. data/test/rails/public/javascripts/dragdrop.js +942 -0
  41. data/test/rails/public/javascripts/effects.js +1088 -0
  42. data/test/rails/public/javascripts/prototype.js +2515 -0
  43. data/test/rails/public/robots.txt +1 -0
  44. data/test/rails/script/about +3 -0
  45. data/test/rails/script/breakpointer +3 -0
  46. data/test/rails/script/console +3 -0
  47. data/test/rails/script/destroy +3 -0
  48. data/test/rails/script/generate +3 -0
  49. data/test/rails/script/performance/benchmarker +3 -0
  50. data/test/rails/script/performance/profiler +3 -0
  51. data/test/rails/script/plugin +3 -0
  52. data/test/rails/script/process/inspector +3 -0
  53. data/test/rails/script/process/reaper +3 -0
  54. data/test/rails/script/process/spawner +3 -0
  55. data/test/rails/script/runner +3 -0
  56. data/test/rails/script/server +3 -0
  57. data/test/rails/test/test_helper.rb +28 -0
  58. data/test/ramaze/conf/benchmark.yaml +35 -0
  59. data/test/ramaze/conf/debug.yaml +34 -0
  60. data/test/ramaze/conf/live.yaml +33 -0
  61. data/test/ramaze/conf/silent.yaml +31 -0
  62. data/test/ramaze/conf/stage.yaml +33 -0
  63. data/test/ramaze/main.rb +18 -0
  64. data/test/ramaze/public/404.jpg +0 -0
  65. data/test/ramaze/public/css/coderay.css +105 -0
  66. data/test/ramaze/public/css/ramaze_error.css +42 -0
  67. data/test/ramaze/public/error.zmr +77 -0
  68. data/test/ramaze/public/favicon.ico +0 -0
  69. data/test/ramaze/public/js/jquery.js +1923 -0
  70. data/test/ramaze/public/ramaze.png +0 -0
  71. data/test/ramaze/src/controller/main.rb +8 -0
  72. data/test/ramaze/src/element/page.rb +17 -0
  73. data/test/ramaze/src/model.rb +6 -0
  74. data/test/ramaze/template/index.xhtml +6 -0
  75. data/test/ramaze/yaml.db +0 -0
  76. metadata +189 -0
@@ -0,0 +1,390 @@
1
+ begin
2
+ load_attempted ||= false
3
+ require 'eventmachine'
4
+ rescue LoadError => e
5
+ unless load_attempted
6
+ load_attempted = true
7
+ require 'rubygems'
8
+ end
9
+ raise e
10
+ end
11
+
12
+ module Swiftcore
13
+ module Swiftiply
14
+ Ccluster_address = 'cluster_address'.freeze
15
+ Ccluster_port = 'cluster_port'.freeze
16
+ CBackendAddress = 'BackendAddress'.freeze
17
+ CBackendPort = 'BackendPort'.freeze
18
+ Cmap = 'map'.freeze
19
+ Cincoming = 'incoming'.freeze
20
+ Ckeepalive = 'keepalive'.freeze
21
+ Cdaemonize = 'daemonize'.freeze
22
+ Curl = 'url'.freeze
23
+ Chost = 'host'.freeze
24
+ Cport = 'port'.freeze
25
+ Coutgoing = 'outgoing'.freeze
26
+ Ctimeout = 'timeout'.freeze
27
+ Cdefault = 'default'.freeze
28
+
29
+ # The ProxyBag is a class that holds the client and the server queues,
30
+ # and that is responsible for managing them, matching them, and expiring
31
+ # them, if necessary.
32
+
33
+ class ProxyBag
34
+ @client_q = Hash.new {|h,k| h[k] = []}
35
+ @server_q = Hash.new {|h,k| h[k] = []}
36
+ @ctime = Time.now
37
+ @server_unavailable_timeout = 6
38
+
39
+ class << self
40
+
41
+ def now
42
+ @ctime
43
+ end
44
+
45
+ # Returns the access key. If an access key is set, then all new backend
46
+ # connections must send the correct access key before being added to
47
+ # the cluster as a valid backend.
48
+
49
+ def key
50
+ @key
51
+ end
52
+
53
+ # Sets the access key.
54
+
55
+ def key=(val)
56
+ @key = val
57
+ end
58
+
59
+ def default_name
60
+ @default_name
61
+ end
62
+
63
+ def default_name=(val)
64
+ @default_name = val
65
+ end
66
+
67
+ # This timeout is the amount of time a connection will sit in queue
68
+ # waiting for a backend to process it.
69
+
70
+ def server_unavailable_timeout
71
+ @server_unavailable_timeout
72
+ end
73
+
74
+ # Sets the server unavailable timeout value.
75
+
76
+ def server_unavailable_timeout=(val)
77
+ @server_unavailable_timeout = val
78
+ end
79
+
80
+ # Pushes a front end client (web browser) into the queue of clients
81
+ # waiting to be serviced if there's no server available to handle
82
+ # it right now.
83
+
84
+ def add_frontend_client clnt
85
+ clnt.create_time = @ctime
86
+ @client_q[clnt.name].unshift(clnt) unless match_client_to_server_now(clnt)
87
+ end
88
+
89
+ # Pushes a backend server into the queue of servers waiting for a
90
+ # client to service if there are no clients waiting to be serviced.
91
+
92
+ def add_server srvr
93
+ @server_q[srvr.name].unshift(srvr) unless match_server_to_client_now(srvr)
94
+ end
95
+
96
+ # Deletes the provided server from the server queue.
97
+
98
+ def remove_server srvr
99
+ @server_q[srvr.name].delete srvr
100
+ end
101
+
102
+ # Removes the named client from the client queue.
103
+ # TODO: Try replacing this with a linked list. Performance
104
+ # here has to suck when the list is long.
105
+
106
+ def remove_client clnt
107
+ @client_q[clnt.name].delete clnt
108
+ end
109
+
110
+ # Walks through the client and server queues, matching
111
+ # waiting clients with waiting servers until the queue
112
+ # runs out of one or the other. DEPRECATED
113
+
114
+ def match_clients_to_servers
115
+ while @server_q.first && @client_q.first
116
+ server = @server_q.pop
117
+ client = @client_q.pop
118
+ server.associate = client
119
+ client.associate = server
120
+ client.push
121
+ end
122
+ end
123
+
124
+ # Tries to match the client passed as an argument to a
125
+ # server.
126
+
127
+ def match_client_to_server_now(client)
128
+ if server = @server_q[client.name].pop
129
+ #server = @server_q[client.name].pop
130
+ server.associate = client
131
+ client.associate = server
132
+ client.push
133
+ true
134
+ else
135
+ false
136
+ end
137
+ end
138
+
139
+ # Tries to match the server passed as an argument to a
140
+ # client.
141
+
142
+ def match_server_to_client_now(server)
143
+ if client = @client_q[server.name].pop
144
+ #client = @client_q[server.name].pop
145
+ server.associate = client
146
+ client.associate = server
147
+ client.push
148
+ true
149
+ else
150
+ false
151
+ end
152
+ end
153
+
154
+ # Walk through the waiting clients if there is no server
155
+ # available to process clients and expire any clients that
156
+ # have been waiting longer than @server_unavailable_timeout
157
+ # seconds. Clients which are expired will receive a 503
158
+ # response.
159
+
160
+ def expire_clients
161
+ now = Time.now
162
+ @server_q.each_key do |name|
163
+ unless @server_q[name].first
164
+ while c = @client_q[name].pop
165
+ if (now - c.create_time) >= @server_unavailable_timeout
166
+ c.send_503_response
167
+ else
168
+ @client_q[name].push c
169
+ break
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ # This is called by a periodic timer once a second to update
177
+ # the time.
178
+
179
+ def update_ctime
180
+ @ctime = Time.now
181
+ end
182
+
183
+ end
184
+ end
185
+
186
+ # The ClusterProtocol is the subclass of EventMachine::Connection used
187
+ # to communicate between Swiftiply and the web browser clients.
188
+
189
+ class ClusterProtocol < EventMachine::Connection
190
+ attr_accessor :create_time, :associate, :name
191
+
192
+ Crnrn = "\r\n\r\n".freeze
193
+ Rrnrn = /\r\n\r\n/
194
+ R_colon = /:/
195
+
196
+ # Initialize the @data array, which is the temporary storage for blocks
197
+ # of data received from the web browser client, then invoke the superclass
198
+ # initialization.
199
+
200
+ def initialize *args
201
+ @data = []
202
+ @name = nil
203
+ super
204
+ end
205
+
206
+ # Receive data from the web browser client, then immediately try to
207
+ # push it to a backend.
208
+
209
+ def receive_data data
210
+ @data.unshift data
211
+ if @name
212
+ push
213
+ else
214
+ # if data =~ /^Host:\s*(.*)\s*$/
215
+ # @name = $1.chomp.split(R_colon,2).first
216
+ if data =~ /^Host:\s*([^\r\n:]*)/
217
+ @name = $1
218
+ ProxyBag.add_frontend_client self
219
+ push
220
+ elsif data =~ /\r\n\r\n/
221
+ @name = ProxyBag.default_name
222
+ ProxyBag.add_frontend_client self
223
+ push
224
+ end
225
+ end
226
+ end
227
+
228
+ # Hardcoded 503 response that is sent if a connection is timed out while
229
+ # waiting for a backend to handle it.
230
+
231
+ def send_503_response
232
+ send_data [
233
+ "HTTP/1.0 503 Server Unavailable\r\n",
234
+ "Content-type: text/plain\r\n",
235
+ "Connection: close\r\n",
236
+ "\r\n",
237
+ "Server Unavailable"
238
+ ].join
239
+ close_connection_after_writing
240
+ end
241
+
242
+ # Push data from the web browser client to the backend server process.
243
+
244
+ def push
245
+ if @associate
246
+ while data = @data.pop
247
+ @associate.send_data data
248
+ end
249
+ end
250
+ end
251
+
252
+ # The connection with the web browser client has been closed, so the
253
+ # object must be removed from the ProxyBag's queue if it is has not
254
+ # been associated with a backend. If it has already been associated
255
+ # with a backend, then it will not be in the queue and need not be
256
+ # removed.
257
+
258
+ def unbind
259
+ ProxyBag.remove_client(self) unless @associate
260
+ end
261
+ end
262
+
263
+
264
+ # The BackendProtocol is the EventMachine::Connection subclass that
265
+ # handles the communications between Swiftiply and the backend process
266
+ # it is proxying to.
267
+
268
+ class BackendProtocol < EventMachine::Connection
269
+ attr_accessor :associate
270
+
271
+ Crnrn = "\r\n\r\n".freeze
272
+ Rrnrn = /\r\n\r\n/
273
+
274
+ def initialize *args
275
+ @name = self.class.bname
276
+ super
277
+ end
278
+
279
+ def name
280
+ @name
281
+ end
282
+
283
+ # Call setup() and add the backend to the ProxyBag queue.
284
+
285
+ def post_init
286
+ setup
287
+ @initialized = nil
288
+ ProxyBag.add_server self
289
+ end
290
+
291
+ # Setup the initial variables for receiving headers and content.
292
+
293
+ def setup
294
+ @headers = ''
295
+ @headers_completed = false
296
+ @content_length = nil
297
+ @content_sent = 0
298
+ end
299
+
300
+ # Receive data from the backend process. Headers are parsed from
301
+ # the rest of the content, and the Content-Length header used to
302
+ # determine when the complete response has been read. The proxy
303
+ # will attempt to maintain a persistent connection with the backend,
304
+ # allowing for greater throughput.
305
+
306
+ def receive_data data
307
+ # unless @initialized
308
+ # id = data.slice!(0..3).to_i(16)
309
+ # ProxyBag.add_id(self,id)
310
+ # @initialized = true
311
+ # end
312
+ unless @headers_completed
313
+ if data.index(Crnrn)
314
+ @headers_completed = true
315
+ h,d = data.split(Rrnrn)
316
+ @headers << h << Crnrn
317
+ @headers =~ /Content-Length:\s*([^\r\n]+)/
318
+ @content_length = $1.to_i
319
+ @associate.send_data @headers
320
+ @associate.send_data d
321
+ @content_sent += d.length
322
+ else
323
+ @headers << data
324
+ end
325
+ end
326
+
327
+ if @headers_completed
328
+ if @content_sent < @content_length
329
+ @associate.send_data data
330
+ @content_sent += data.length
331
+ else
332
+ @associate.close_connection_after_writing
333
+ @associate = nil
334
+ setup
335
+ ProxyBag.add_server self
336
+ end
337
+ end
338
+ end
339
+
340
+ # This is called when the backend disconnects from the proxy.
341
+ # If the backend is currently associated with a web browser client,
342
+ # that connection will be closed. Otherwise, the backend will be
343
+ # removed from the ProxyBag's backend queue.
344
+
345
+ def unbind
346
+ if @associate
347
+ @associate.close_connection_after_writing
348
+ else
349
+ ProxyBag.remove_server(self)
350
+ end
351
+ end
352
+
353
+ def self.bname=(val)
354
+ @bname = val
355
+ end
356
+
357
+ def self.bname
358
+ @bname
359
+ end
360
+ end
361
+
362
+ # Start the EventMachine event loop and create the front end and backend
363
+ # handlers, then create the timers that are used to expire unserviced
364
+ # clients and to update the Proxy's clock.
365
+
366
+ def self.run(config, key = nil)
367
+ EventMachine.run do
368
+ EventMachine.start_server(config[Ccluster_address], config[Ccluster_port], ClusterProtocol)
369
+ config[Cmap].each do |m|
370
+ if m[Ckeepalive]
371
+ m[Cincoming].each do |p|
372
+ m[Coutgoing].each do |o|
373
+ backend_class = Class.new(BackendProtocol)
374
+ backend_class.bname = p
375
+ ProxyBag.default_name = p if m[Cdefault]
376
+ host, port = o.split(/:/,2)
377
+ EventMachine.start_server(host, port.to_i, backend_class)
378
+ end
379
+ end
380
+ end
381
+ end
382
+ ProxyBag.server_unavailable_timeout ||= config[Ctimeout]
383
+ ProxyBag.key = key
384
+ EventMachine.add_periodic_timer(2) { ProxyBag.expire_clients }
385
+ EventMachine.add_periodic_timer(1) { ProxyBag.update_ctime }
386
+ end
387
+ end
388
+ end
389
+ end
390
+
@@ -0,0 +1,182 @@
1
+ # This module rewrites pieces of the very good Mongrel web server in
2
+ # order to change it from a threaded application to an event based
3
+ # application running inside an EventMachine event loop. It should
4
+ # be compatible with the existing Mongrel handlers for Rails,
5
+ # Camping, Nitro, etc....
6
+
7
+ begin
8
+ load_attempted ||= false
9
+ require 'eventmachine'
10
+ rescue LoadError
11
+ unless load_attempted
12
+ load_attempted = true
13
+ require 'rubygems'
14
+ retry
15
+ end
16
+ end
17
+
18
+ require 'mongrel'
19
+
20
+ module Mongrel
21
+ class MongrelProtocol < EventMachine::Connection
22
+ def post_init
23
+ @parser = HttpParser.new
24
+ @params = HttpParams.new
25
+ @nparsed = 0
26
+ @request = nil
27
+ @request_len = nil
28
+ @linebuffer = ''
29
+ end
30
+
31
+ def receive_data data
32
+ @linebuffer << data
33
+ @nparsed = @parser.execute(@params, @linebuffer, @nparsed) unless @parser.finished?
34
+ if @parser.finished?
35
+ if @request_len.nil?
36
+ #@request_len = @nparsed + @params[::Mongrel::Const::CONTENT_LENGTH].to_i
37
+ @request_len = @params[::Mongrel::Const::CONTENT_LENGTH].to_i
38
+ script_name, path_info, handlers = ::Mongrel::HttpServer::Instance.classifier.resolve(@params[::Mongrel::Const::REQUEST_PATH])
39
+ if handlers
40
+ @params[::Mongrel::Const::PATH_INFO] = path_info
41
+ @params[::Mongrel::Const::SCRIPT_NAME] = script_name
42
+ @params[::Mongrel::Const::REMOTE_ADDR] = @params[::Mongrel::Const::HTTP_X_FORWARDED_FOR] || ::Socket.unpack_sockaddr_in(get_peername)[1]
43
+ @notifiers = handlers.select { |h| h.request_notify }
44
+ end
45
+ if @request_len > ::Mongrel::Const::MAX_BODY
46
+ new_buffer = Tempfile.new(::Mongrel::Const::MONGREL_TMP_BASE)
47
+ new_buffer.binmode
48
+ new_buffer << @linebuffer[@nparsed..-1]
49
+ @linebuffer = new_buffer
50
+ else
51
+ @linebuffer = StringIO.new(@linebuffer[@nparsed..-1])
52
+ end
53
+ end
54
+ if @linebuffer.length >= @request_len
55
+ ::Mongrel::HttpServer::Instance.process_http_request(@params,@linebuffer,self)
56
+ end
57
+ elsif @linebuffer.length > ::Mongrel::Const::MAX_HEADER
58
+ raise ::Mongrel::HttpParserError.new("HEADER is longer than allowed, aborting client early.")
59
+ end
60
+ rescue Exception => e
61
+ close_connection
62
+ raise e
63
+ end
64
+
65
+ def write data
66
+ send_data data
67
+ end
68
+
69
+ def closed?
70
+ false
71
+ end
72
+
73
+ end
74
+
75
+ class HttpServer
76
+ def initialize(host, port, num_processors=(2**30-1), timeout=0)
77
+ @socket = nil
78
+ @classifier = URIClassifier.new
79
+ @host = host
80
+ @port = port
81
+ @workers = ThreadGroup.new
82
+ @timeout = timeout
83
+ @num_processors = num_processors
84
+ @death_time = 60
85
+ self.class.const_set(:Instance,self)
86
+ end
87
+
88
+ def run
89
+ trap('INT') { raise StopServer }
90
+ trap('TERM') { raise StopServer }
91
+ @acceptor = Thread.new do
92
+ EventMachine.run do
93
+ begin
94
+ EventMachine.start_server(@host,@port,MongrelProtocol)
95
+ rescue StopServer
96
+ EventMachine.stop_event_loop
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ def process_http_request(params,linebuffer,client)
103
+ if not params[Const::REQUEST_PATH]
104
+ uri = URI.parse(params[Const::REQUEST_URI])
105
+ params[Const::REQUEST_PATH] = uri.request_uri
106
+ end
107
+
108
+ raise "No REQUEST PATH" if not params[Const::REQUEST_PATH]
109
+
110
+ script_name, path_info, handlers = @classifier.resolve(params[Const::REQUEST_PATH])
111
+
112
+ if handlers
113
+ notifiers = handlers.select { |h| h.request_notify }
114
+ request = HttpRequest.new(params, linebuffer, notifiers)
115
+
116
+ # request is good so far, continue processing the response
117
+ response = HttpResponse.new(client)
118
+
119
+ # Process each handler in registered order until we run out or one finalizes the response.
120
+ handlers.each do |handler|
121
+ handler.process(request, response)
122
+ break if response.done
123
+ end
124
+
125
+ # And finally, if nobody closed the response off, we finalize it.
126
+ unless response.done
127
+ response.finished
128
+ else
129
+ response.close_connection_after_writing
130
+ end
131
+ else
132
+ # Didn't find it, return a stock 404 response.
133
+ client.send_data(Const::ERROR_404_RESPONSE)
134
+ client.close_connection_after_writing
135
+ end
136
+ end
137
+ end
138
+
139
+ class HttpRequest
140
+ def initialize(params, linebuffer, dispatchers)
141
+ @params = params
142
+ @dispatchers = dispatchers
143
+ @body = linebuffer
144
+ end
145
+ end
146
+
147
+ class HttpResponse
148
+ def send_file(path, small_file = false)
149
+ File.open(path, "rb") do |f|
150
+ while chunk = f.read(Const::CHUNK_SIZE) and chunk.length > 0
151
+ begin
152
+ write(chunk)
153
+ rescue Object => exc
154
+ break
155
+ end
156
+ end
157
+ end
158
+ @body_sent = true
159
+ end
160
+
161
+ def write(data)
162
+ @socket.send_data data
163
+ end
164
+
165
+ def close_connection_after_writing
166
+ @socket.close_connection_after_writing
167
+ end
168
+
169
+ def socket_error(details)
170
+ @socket.close_connection
171
+ done = true
172
+ raise details
173
+ end
174
+
175
+ def finished
176
+ send_status
177
+ send_header
178
+ send_body
179
+ @socket.close_connection_after_writing
180
+ end
181
+ end
182
+ end