swiftiply 0.5.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 (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