swiftiply 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. data/CONTRIBUTORS +2 -0
  2. data/README +1 -1
  3. data/bin/echo_client +26 -0
  4. data/bin/swiftiply +21 -8
  5. data/ext/fastfilereader/extconf.rb +161 -0
  6. data/ext/fastfilereader/mapper.cpp +200 -0
  7. data/ext/fastfilereader/mapper.h +59 -0
  8. data/ext/fastfilereader/rubymain.cpp +127 -0
  9. data/external/test_support.rb +13 -0
  10. data/setup.rb +7 -2
  11. data/src/fastfilereader.rb +109 -0
  12. data/src/swiftcore/Swiftiply.rb +432 -103
  13. data/src/swiftcore/Swiftiply/support_pagecache.rb +56 -0
  14. data/src/swiftcore/Swiftiply/swiftiply_client.rb +57 -0
  15. data/src/swiftcore/evented_mongrel.rb +32 -4
  16. data/src/swiftcore/swiftiplied_mongrel.rb +50 -38
  17. data/src/swiftcore/types.rb +1583 -0
  18. data/swiftiply.gemspec +4 -4
  19. data/test/TC_ProxyBag.rb +185 -0
  20. data/test/TC_Swiftiply.rb +458 -0
  21. data/test/TC_Swiftiply/mongrel/evented_hello.rb +25 -0
  22. data/test/TC_Swiftiply/mongrel/swiftiplied_hello.rb +25 -0
  23. data/test/TC_Swiftiply/mongrel/threaded_hello.rb +25 -0
  24. data/test/TC_Swiftiply/slow_echo_client +26 -0
  25. metadata +34 -121
  26. data/src/ramaze/adapter/evented_mongrel.rb +0 -2
  27. data/src/ramaze/adapter/swiftiplied_mongrel.rb +0 -2
  28. data/test/rails/README +0 -182
  29. data/test/rails/Rakefile +0 -10
  30. data/test/rails/app/controllers/application.rb +0 -6
  31. data/test/rails/app/controllers/tests_controller.rb +0 -15
  32. data/test/rails/app/helpers/application_helper.rb +0 -3
  33. data/test/rails/config/boot.rb +0 -45
  34. data/test/rails/config/database.yml +0 -36
  35. data/test/rails/config/environment.rb +0 -60
  36. data/test/rails/config/environments/development.rb +0 -21
  37. data/test/rails/config/environments/production.rb +0 -18
  38. data/test/rails/config/environments/production_no_caching.rb +0 -18
  39. data/test/rails/config/environments/test.rb +0 -19
  40. data/test/rails/config/routes.rb +0 -23
  41. data/test/rails/doc/README_FOR_APP +0 -2
  42. data/test/rails/observe_ram.rb +0 -10
  43. data/test/rails/public/404.html +0 -30
  44. data/test/rails/public/500.html +0 -30
  45. data/test/rails/public/dispatch.cgi +0 -10
  46. data/test/rails/public/dispatch.fcgi +0 -24
  47. data/test/rails/public/dispatch.rb +0 -10
  48. data/test/rails/public/favicon.ico +0 -0
  49. data/test/rails/public/images/rails.png +0 -0
  50. data/test/rails/public/index.html +0 -277
  51. data/test/rails/public/javascripts/application.js +0 -2
  52. data/test/rails/public/javascripts/controls.js +0 -833
  53. data/test/rails/public/javascripts/dragdrop.js +0 -942
  54. data/test/rails/public/javascripts/effects.js +0 -1088
  55. data/test/rails/public/javascripts/prototype.js +0 -2515
  56. data/test/rails/public/robots.txt +0 -1
  57. data/test/rails/script/about +0 -3
  58. data/test/rails/script/breakpointer +0 -3
  59. data/test/rails/script/console +0 -3
  60. data/test/rails/script/destroy +0 -3
  61. data/test/rails/script/generate +0 -3
  62. data/test/rails/script/performance/benchmarker +0 -3
  63. data/test/rails/script/performance/profiler +0 -3
  64. data/test/rails/script/plugin +0 -3
  65. data/test/rails/script/process/inspector +0 -3
  66. data/test/rails/script/process/reaper +0 -3
  67. data/test/rails/script/process/spawner +0 -3
  68. data/test/rails/script/runner +0 -3
  69. data/test/rails/script/server +0 -3
  70. data/test/rails/test/test_helper.rb +0 -28
  71. data/test/ramaze/conf/benchmark.yaml +0 -35
  72. data/test/ramaze/conf/debug.yaml +0 -34
  73. data/test/ramaze/conf/live.yaml +0 -33
  74. data/test/ramaze/conf/silent.yaml +0 -31
  75. data/test/ramaze/conf/stage.yaml +0 -33
  76. data/test/ramaze/main.rb +0 -18
  77. data/test/ramaze/public/404.jpg +0 -0
  78. data/test/ramaze/public/css/coderay.css +0 -105
  79. data/test/ramaze/public/css/ramaze_error.css +0 -42
  80. data/test/ramaze/public/error.zmr +0 -77
  81. data/test/ramaze/public/favicon.ico +0 -0
  82. data/test/ramaze/public/js/jquery.js +0 -1923
  83. data/test/ramaze/public/ramaze.png +0 -0
  84. data/test/ramaze/src/controller/main.rb +0 -8
  85. data/test/ramaze/src/element/page.rb +0 -17
  86. data/test/ramaze/src/model.rb +0 -6
  87. data/test/ramaze/template/index.xhtml +0 -6
  88. data/test/ramaze/yaml.db +0 -0
@@ -0,0 +1,59 @@
1
+ /*****************************************************************************
2
+
3
+ $Id: mapper.h 4529 2007-07-04 11:32:22Z francis $
4
+
5
+ File: mapper.h
6
+ Date: 02Jul07
7
+
8
+ Copyright (C) 2007 by Francis Cianfrocca. All Rights Reserved.
9
+ Gmail: garbagecat10
10
+
11
+ This program is free software; you can redistribute it and/or modify
12
+ it under the terms of either: 1) the GNU General Public License
13
+ as published by the Free Software Foundation; either version 2 of the
14
+ License, or (at your option) any later version; or 2) Ruby's License.
15
+
16
+ See the file COPYING for complete licensing information.
17
+
18
+ *****************************************************************************/
19
+
20
+
21
+ #ifndef __Mapper__H_
22
+ #define __Mapper__H_
23
+
24
+
25
+ /**************
26
+ class Mapper_t
27
+ **************/
28
+
29
+ class Mapper_t
30
+ {
31
+ public:
32
+ Mapper_t (const string&);
33
+ virtual ~Mapper_t();
34
+
35
+ const char *GetChunk (unsigned);
36
+ void Close();
37
+ size_t GetFileSize() {return FileSize;}
38
+
39
+ private:
40
+ size_t FileSize;
41
+
42
+ #ifdef OS_UNIX
43
+ private:
44
+ int Fd;
45
+ const char *MapPoint;
46
+ #endif // OS_UNIX
47
+
48
+ #ifdef OS_WIN32
49
+ private:
50
+ HANDLE hFile;
51
+ HANDLE hMapping;
52
+ const char *MapPoint;
53
+ #endif // OS_WIN32
54
+
55
+ };
56
+
57
+
58
+ #endif // __Mapper__H_
59
+
@@ -0,0 +1,127 @@
1
+ /*****************************************************************************
2
+
3
+ $Id: rubymain.cpp 4529 2007-07-04 11:32:22Z francis $
4
+
5
+ File: rubymain.cpp
6
+ Date: 02Jul07
7
+
8
+ Copyright (C) 2007 by Francis Cianfrocca. All Rights Reserved.
9
+ Gmail: garbagecat10
10
+
11
+ This program is free software; you can redistribute it and/or modify
12
+ it under the terms of either: 1) the GNU General Public License
13
+ as published by the Free Software Foundation; either version 2 of the
14
+ License, or (at your option) any later version; or 2) Ruby's License.
15
+
16
+ See the file COPYING for complete licensing information.
17
+
18
+ *****************************************************************************/
19
+
20
+
21
+
22
+ #include <iostream>
23
+ #include <stdexcept>
24
+ using namespace std;
25
+
26
+ #include <ruby.h>
27
+ #include "mapper.h"
28
+
29
+ static VALUE EmModule;
30
+ static VALUE FastFileReader;
31
+ static VALUE Mapper;
32
+
33
+
34
+
35
+ /*********
36
+ mapper_dt
37
+ *********/
38
+
39
+ static void mapper_dt (void *ptr)
40
+ {
41
+ if (ptr)
42
+ delete (Mapper_t*) ptr;
43
+ }
44
+
45
+ /**********
46
+ mapper_new
47
+ **********/
48
+
49
+ static VALUE mapper_new (VALUE self, VALUE filename)
50
+ {
51
+ Mapper_t *m = new Mapper_t (StringValuePtr (filename));
52
+ if (!m)
53
+ rb_raise (rb_eException, "No Mapper Object");
54
+ VALUE v = Data_Wrap_Struct (Mapper, 0, mapper_dt, (void*)m);
55
+ return v;
56
+ }
57
+
58
+
59
+ /****************
60
+ mapper_get_chunk
61
+ ****************/
62
+
63
+ static VALUE mapper_get_chunk (VALUE self, VALUE start, VALUE length)
64
+ {
65
+ Mapper_t *m = NULL;
66
+ Data_Get_Struct (self, Mapper_t, m);
67
+ if (!m)
68
+ rb_raise (rb_eException, "No Mapper Object");
69
+
70
+ // TODO, what if some moron sends us a negative start value?
71
+ unsigned _start = NUM2INT (start);
72
+ unsigned _length = NUM2INT (length);
73
+ if ((_start + _length) > m->GetFileSize())
74
+ rb_raise (rb_eException, "Mapper Range Error");
75
+
76
+ const char *chunk = m->GetChunk (_start);
77
+ if (!chunk)
78
+ rb_raise (rb_eException, "No Mapper Chunk");
79
+ return rb_str_new (chunk, _length);
80
+ }
81
+
82
+ /************
83
+ mapper_close
84
+ ************/
85
+
86
+ static VALUE mapper_close (VALUE self)
87
+ {
88
+ Mapper_t *m = NULL;
89
+ Data_Get_Struct (self, Mapper_t, m);
90
+ if (!m)
91
+ rb_raise (rb_eException, "No Mapper Object");
92
+ m->Close();
93
+ return Qnil;
94
+ }
95
+
96
+ /***********
97
+ mapper_size
98
+ ***********/
99
+
100
+ static VALUE mapper_size (VALUE self)
101
+ {
102
+ Mapper_t *m = NULL;
103
+ Data_Get_Struct (self, Mapper_t, m);
104
+ if (!m)
105
+ rb_raise (rb_eException, "No Mapper Object");
106
+ return INT2NUM (m->GetFileSize());
107
+ }
108
+
109
+
110
+ /**********************
111
+ Init_fastfilereaderext
112
+ **********************/
113
+
114
+ extern "C" void Init_fastfilereaderext()
115
+ {
116
+ EmModule = rb_define_module ("EventMachine");
117
+ FastFileReader = rb_define_class_under (EmModule, "FastFileReader", rb_cObject);
118
+ Mapper = rb_define_class_under (FastFileReader, "Mapper", rb_cObject);
119
+
120
+ rb_define_module_function (Mapper, "new", (VALUE(*)(...))mapper_new, 1);
121
+ rb_define_method (Mapper, "size", (VALUE(*)(...))mapper_size, 0);
122
+ rb_define_method (Mapper, "close", (VALUE(*)(...))mapper_close, 0);
123
+ rb_define_method (Mapper, "get_chunk", (VALUE(*)(...))mapper_get_chunk, 2);
124
+ }
125
+
126
+
127
+
@@ -56,3 +56,16 @@ module SwiftcoreTestSupport
56
56
  end
57
57
 
58
58
  end
59
+
60
+ class EMConnectionMock
61
+ attr_accessor :uri, :name
62
+
63
+ def send_data data
64
+ end
65
+
66
+ def send_file_data filename
67
+ end
68
+
69
+ def close_connection; end
70
+ def close_connection_after_writing; end
71
+ end
data/setup.rb CHANGED
@@ -13,6 +13,11 @@ Dir.chdir(basedir)
13
13
  Package.setup("1.0") {
14
14
  name "Swiftcore Swiftiply"
15
15
 
16
+ build_ext "fastfilereader"
17
+ translate(:ext, 'ext/fastfilereader/' => '/')
18
+ #translate(:ext, 'ext/http11/' => 'iowa/')
19
+ ext "ext/fastfilereader/fastfilereaderext.so"
20
+
16
21
  translate(:lib, 'src/' => '')
17
22
  translate(:bin, 'bin/' => '')
18
23
  lib(*Dir["src/swiftcore/**/*.rb"])
@@ -25,7 +30,7 @@ Package.setup("1.0") {
25
30
  #File.rename("#{Config::CONFIG["bindir"]}/mongrel_rails","#{Config::CONFIG["bindir"]}/mongrel_rails.orig")
26
31
  bin "bin/mongrel_rails"
27
32
 
28
- # unit_test "test/TC_Swiftiply.rb"
29
-
33
+ unit_test "test/TC_ProxyBag.rb"
34
+ unit_test "test/TC_Swiftiply.rb"
30
35
  true
31
36
  }
@@ -0,0 +1,109 @@
1
+ # Written by Francis Cianfrocca (garbagecat10@gmail.com) with contributions
2
+ # from Kirk Haines (wyhaines@gmail.com).
3
+
4
+ begin
5
+ load_attempted ||= false
6
+ require 'eventmachine'
7
+ require 'fastfilereaderext'
8
+ rescue LoadError => e
9
+ unless load_attempted
10
+ load_attempted = true
11
+ require 'eventmachine'
12
+ require 'fastfilereaderext'
13
+ retry
14
+ end
15
+ raise e
16
+ end
17
+
18
+ module EventMachine
19
+ class FastFileReader
20
+ include EventMachine::Deferrable
21
+
22
+ attr_reader :size
23
+
24
+ # TODO, make these constants tunable parameters
25
+ ChunkSize = 16384
26
+ BackpressureLevel = 50000
27
+ MappingThreshold = 32768
28
+ Crn = "\r\n".freeze
29
+ C0rnrn = "0\r\n\r\n".freeze
30
+
31
+ class << self
32
+ # Return a newly-created instance of this class, or nil on error.
33
+ #
34
+ def open filename
35
+ FastFileReader.new(filename)
36
+ rescue
37
+ nil
38
+ end
39
+
40
+ end
41
+
42
+ # This constructor can throw exceptions. Use #open to avoid that fate.
43
+ #
44
+ def initialize filename
45
+ # Throw an exception if we can't open the file.
46
+ # TODO, perhaps we should throw a different exception?
47
+ raise "no file" unless File.exist?(filename)
48
+
49
+ @size = File.size?(filename)
50
+ if @size >= MappingThreshold
51
+ @mapping = Mapper.new( filename )
52
+ else
53
+ @content = File.read( filename )
54
+ end
55
+ end
56
+
57
+
58
+ # This is a no-op for small files that have a @content
59
+ # member. For large files with a @mapping, we call #close
60
+ # on the mapping. In general, this will be done by the
61
+ # finalizer when the GC runs, but there will be cases
62
+ # when we will want to know that the underlying file mapping
63
+ # is closed. This matters particularly on Windows, because
64
+ # we're holding some HANDLE objects open that can cause
65
+ # trouble for Ruby.
66
+ def close
67
+ @mapping.close if @mapping
68
+ end
69
+
70
+ # We expect to receive something like an EventMachine::Connection object.
71
+ # We also expect to be running inside a reaactor loop, because we call
72
+ # EventMachine#next_tick when we have too much data to send.
73
+ def stream_as_http_chunks sink
74
+ if @content
75
+ if @content.length > 0
76
+ sink.send_data( "#{@content.length.to_s(16)}\r\n#{@content}#{Crn}" )
77
+ end
78
+ sink.send_data( C0rnrn )
79
+ set_deferred_success
80
+ else
81
+ @position = 0
82
+ @sink = sink
83
+ stream_one_http_chunk
84
+ end
85
+ end
86
+
87
+ def stream_one_http_chunk
88
+ loop {
89
+ if @position < @size
90
+ if @sink.get_outbound_data_size > BackpressureLevel
91
+ EventMachine::next_tick {stream_one_http_chunk}
92
+ break
93
+ else
94
+ len = @size - @position
95
+ len = ChunkSize if (len > ChunkSize)
96
+ @sink.send_data( "#{len.to_s(16)}\r\n#{@mapping.get_chunk( @position, len))}\r\n" )
97
+ @position += len
98
+ end
99
+ else
100
+ @sink.send_data( C0rnrn )
101
+ set_deferred_success
102
+ break
103
+ end
104
+ }
105
+ end
106
+ private :stream_one_http_chunk
107
+ end
108
+ end
109
+
@@ -2,33 +2,66 @@ begin
2
2
  load_attempted ||= false
3
3
  require 'digest/sha2'
4
4
  require 'eventmachine'
5
+ require 'fastfilereaderext'
6
+ require 'swiftcore/types'
5
7
  rescue LoadError => e
6
8
  unless load_attempted
7
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.
8
13
  require 'rubygems'
14
+ retry
9
15
  end
10
16
  raise e
11
17
  end
12
18
 
13
19
  module Swiftcore
14
20
  module Swiftiply
15
- Version = '0.5.1'
16
-
21
+ Version = '0.6.0'
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
17
32
  Ccluster_address = 'cluster_address'.freeze
18
33
  Ccluster_port = 'cluster_port'.freeze
34
+ Ccluster_server = 'cluster_server'.freeze
19
35
  CBackendAddress = 'BackendAddress'.freeze
20
36
  CBackendPort = 'BackendPort'.freeze
21
- Cmap = 'map'.freeze
22
- Cincoming = 'incoming'.freeze
23
- Ckeepalive = 'keepalive'.freeze
37
+ Cchunked_encoding_threshold = 'chunked_encoding_threshold'.freeze
24
38
  Cdaemonize = 'daemonize'.freeze
25
- Curl = 'url'.freeze
39
+ Cdefault = 'default'.freeze
40
+ Cdocroot = 'docroot'.freeze
41
+ Cepoll = 'epoll'.freeze
42
+ Cepoll_descriptors = 'epoll_descriptors'.freeze
43
+ Cgroup = 'group'.freeze
26
44
  Chost = 'host'.freeze
27
- Cport = 'port'.freeze
45
+ Cincoming = 'incoming'.freeze
46
+ Ckeepalive = 'keepalive'.freeze
47
+ Ckey = 'key'.freeze
48
+ Cmap = 'map'.freeze
49
+ Cmsg_expired = 'browser connection expired'.freeze
28
50
  Coutgoing = 'outgoing'.freeze
51
+ Cport = 'port'.freeze
52
+ Credeployable = 'redeployable'.freeze
53
+ Credeployment_sizelimit = 'redeployment_sizelimit'.freeze
54
+ Cswiftclient = 'swiftclient'.freeze
29
55
  Ctimeout = 'timeout'.freeze
30
- Cdefault = 'default'.freeze
56
+ Curl = 'url'.freeze
57
+ Cuser = 'user'.freeze
58
+
59
+ C_fsep = File::SEPARATOR
60
+
61
+ RunningConfig = {}
31
62
 
63
+ class EMStartServerError < RuntimeError; end
64
+
32
65
  # The ProxyBag is a class that holds the client and the server queues,
33
66
  # and that is responsible for managing them, matching them, and expiring
34
67
  # them, if necessary.
@@ -41,7 +74,14 @@ module Swiftcore
41
74
  @id_map = {}
42
75
  @reverse_id_map = {}
43
76
  @incoming_map = {}
77
+ @docroot_map = {}
78
+ @log_map = {}
79
+ @redeployable_map = {}
80
+ @keys = {}
44
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__')
45
85
 
46
86
  class << self
47
87
 
@@ -53,14 +93,12 @@ module Swiftcore
53
93
  # connections must send the correct access key before being added to
54
94
  # the cluster as a valid backend.
55
95
 
56
- def key
57
- @key
96
+ def get_key(h)
97
+ @keys[h] || C_empty
58
98
  end
59
99
 
60
- # Sets the access key.
61
-
62
- def key=(val)
63
- @key = val
100
+ def set_key(h,val)
101
+ @keys[h] = val
64
102
  end
65
103
 
66
104
  def add_id(who,what)
@@ -73,10 +111,41 @@ module Swiftcore
73
111
  @reverse_id_map.delete(what)
74
112
  end
75
113
 
114
+ def incoming_mapping(name)
115
+ @incoming_map[name]
116
+ end
117
+
76
118
  def add_incoming_mapping(hashcode,name)
77
119
  @incoming_map[name] = hashcode
78
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
79
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
+
80
149
  def default_name
81
150
  @default_name
82
151
  end
@@ -86,29 +155,127 @@ module Swiftcore
86
155
  end
87
156
 
88
157
  # This timeout is the amount of time a connection will sit in queue
89
- # waiting for a backend to process it.
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.
90
161
 
91
162
  def server_unavailable_timeout
92
163
  @server_unavailable_timeout
93
164
  end
94
165
 
95
- # Sets the server unavailable timeout value.
96
-
97
166
  def server_unavailable_timeout=(val)
98
167
  @server_unavailable_timeout = val
99
168
  end
100
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)
236
+ end
237
+
101
238
  # Pushes a front end client (web browser) into the queue of clients
102
239
  # waiting to be serviced if there's no server available to handle
103
240
  # it right now.
104
241
 
105
- def add_frontend_client clnt
242
+ def add_frontend_client(clnt,data_q,data)
106
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
+
107
269
  unless match_client_to_server_now(clnt)
108
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.
109
276
  @demanding_clients[$1].unshift clnt
110
277
  else
111
- @client_q[clnt.name].unshift(clnt)
278
+ @client_q[@incoming_map[clnt.name]].unshift(clnt)
112
279
  end
113
280
  end
114
281
  end
@@ -127,38 +294,28 @@ module Swiftcore
127
294
  end
128
295
 
129
296
  # Removes the named client from the client queue.
130
- # TODO: Try replacing this with a linked list. Performance
131
- # here has to suck when the list is long.
132
-
297
+ # TODO: Try replacing this with ...something. Performance
298
+ # here has to be bad when the list is long.
299
+
133
300
  def remove_client clnt
134
301
  @client_q[clnt.name].delete clnt
135
302
  end
136
303
 
137
- # Walks through the client and server queues, matching
138
- # waiting clients with waiting servers until the queue
139
- # runs out of one or the other. DEPRECATED
140
-
141
- #def match_clients_to_servers
142
- # while @server_q.first && @client_q.first
143
- # server = @server_q.pop
144
- # client = @client_q.pop
145
- # server.associate = client
146
- # client.associate = server
147
- # client.push
148
- # end
149
- #end
150
-
151
- # Tries to match the client passed as an argument to a
304
+ # Tries to match the client (passed as an argument) to a
152
305
  # server.
153
306
 
154
307
  def match_client_to_server_now(client)
155
308
  sq = @server_q[@incoming_map[client.name]]
156
- if client.uri =~ /\w+-\w+-\w+\.\w+\.[\w\.]+-(\w+)?$/ and sidx = sq.index(@reverse_id_map[$1])
157
- server = sq.delete_at(sidx)
158
- server.associate = client
159
- client.associate = server
160
- client.push
161
- true
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
162
319
  elsif server = sq.pop
163
320
  server.associate = client
164
321
  client.associate = server
@@ -169,7 +326,7 @@ module Swiftcore
169
326
  end
170
327
  end
171
328
 
172
- # Tries to match the server passed as an argument to a
329
+ # Tries to match the server (passed as an argument) to a
173
330
  # client.
174
331
 
175
332
  def match_server_to_client_now(server)
@@ -192,10 +349,13 @@ module Swiftcore
192
349
  # available to process clients and expire any clients that
193
350
  # have been waiting longer than @server_unavailable_timeout
194
351
  # seconds. Clients which are expired will receive a 503
195
- # response.
352
+ # response. If this is happening, either you need more
353
+ # backend processes, or you @server_unavailable_timeout is
354
+ # too short.
196
355
 
197
356
  def expire_clients
198
357
  now = Time.now
358
+
199
359
  @server_q.each_key do |name|
200
360
  unless @server_q[name].first
201
361
  while c = @client_q[name].pop
@@ -225,12 +385,10 @@ module Swiftcore
225
385
 
226
386
  class ClusterProtocol < EventMachine::Connection
227
387
 
228
- attr_accessor :create_time, :associate, :name
388
+ attr_accessor :create_time, :associate, :name, :redeployable, :data_pos, :data_len
229
389
 
230
390
  Crn = "\r\n".freeze
231
391
  Crnrn = "\r\n\r\n".freeze
232
- Rrnrn = /\r\n\r\n/
233
- R_colon = /:/
234
392
  C_blank = ''.freeze
235
393
 
236
394
  # Initialize the @data array, which is the temporary storage for blocks
@@ -239,26 +397,28 @@ module Swiftcore
239
397
 
240
398
  def initialize *args
241
399
  @data = []
242
- @name = nil
243
- @uri = nil
400
+ @data_pos = 0
401
+ @name = @uri = nil
244
402
  super
245
403
  end
246
404
 
247
405
  def receive_data data
248
- @data.unshift data
249
406
  if @name
407
+ @data.unshift data
250
408
  push
251
409
  else
252
- data =~ /\s([^\s\?]*)/
253
- @uri ||= $1
254
- if data =~ /^Host:\s*([^\r\n:]*)/
255
- @name = $1
256
- ProxyBag.add_frontend_client self
257
- push
258
- elsif data.index(/\r\n\r\n/)
410
+ if data =~ /^Host:\s*([^\r:]*)/
411
+ # NOTE: Should I be using intern for this? It might not
412
+ # be a good idea.
413
+ @name = $1.intern
414
+
415
+ data =~ /\s([^\s\?]*)/
416
+ @uri = $1
417
+ @name = ProxyBag.default_name unless ProxyBag.incoming_mapping(@name)
418
+ ProxyBag.add_frontend_client(self,@data,data)
419
+ elsif data =~ /\r\n\r\n/
259
420
  @name = ProxyBag.default_name
260
- ProxyBag.add_frontend_client self
261
- push
421
+ ProxyBag.add_frontend_client(self,@data,data)
262
422
  end
263
423
  end
264
424
  end
@@ -281,8 +441,26 @@ module Swiftcore
281
441
 
282
442
  def push
283
443
  if @associate
284
- while data = @data.pop
285
- @associate.send_data data
444
+ unless @redeployable
445
+ # normal data push
446
+ data = nil
447
+ @associate.send_data data while data = @data.pop
448
+ else
449
+ # redeployable data push; just send the stuff that has
450
+ # not already been sent.
451
+ (@data.length - 1 - @data_pos).downto(0) do |p|
452
+ d = @data[p]
453
+ @associate.send_data d
454
+ @data_len += d.length
455
+ end
456
+ @data_pos = @data.length
457
+
458
+ # If the request size crosses the size limit, then
459
+ # disallow redeployent of this request.
460
+ if @data_len > @redeployable
461
+ @redeployable = false
462
+ @data.clear
463
+ end
286
464
  end
287
465
  end
288
466
  end
@@ -301,8 +479,11 @@ module Swiftcore
301
479
  @uri
302
480
  end
303
481
 
304
- end
482
+ def setup_for_redeployment
483
+ @data_pos = 0
484
+ end
305
485
 
486
+ end
306
487
 
307
488
  # The BackendProtocol is the EventMachine::Connection subclass that
308
489
  # handles the communications between Swiftiply and the backend process
@@ -311,8 +492,8 @@ module Swiftcore
311
492
  class BackendProtocol < EventMachine::Connection
312
493
  attr_accessor :associate, :id
313
494
 
495
+ C0rnrn = "0\r\n\r\n".freeze
314
496
  Crnrn = "\r\n\r\n".freeze
315
- Rrnrn = /\r\n\r\n/
316
497
 
317
498
  def initialize *args
318
499
  @name = self.class.bname
@@ -336,31 +517,47 @@ module Swiftcore
336
517
  def setup
337
518
  @headers = ''
338
519
  @headers_completed = false
339
- @content_length = nil
520
+ #@content_length = nil
340
521
  @content_sent = 0
341
522
  end
342
-
523
+
343
524
  # Receive data from the backend process. Headers are parsed from
344
- # the rest of the content, and the Content-Length header used to
345
- # determine when the complete response has been read. The proxy
346
- # will attempt to maintain a persistent connection with the backend,
347
- # allowing for greater throughput.
525
+ # the rest of the content. If a Content-Length header is present,
526
+ # that is used to determine how much data to expect. Otherwise,
527
+ # if 'Transfer-encoding: chunked' is present, assume chunked
528
+ # encoding. Otherwise be paranoid; something isn't the way we like
529
+ # it to be.
348
530
 
349
531
  def receive_data data
350
532
  unless @initialized
351
- @id = data.slice!(0..11)
352
- ProxyBag.add_id(self,@id)
353
- @initialized = true
533
+ preamble = data.slice!(0..24)
534
+
535
+ keylen = preamble[23..24].to_i(16)
536
+ keylen = 0 if keylen < 0
537
+ key = keylen > 0 ? data.slice!(0..(keylen - 1)) : C_empty
538
+ if preamble[0..10] == Cswiftclient and key == ProxyBag.get_key(@name)
539
+ @id = preamble[11..22]
540
+ ProxyBag.add_id(self,@id)
541
+ @initialized = true
542
+ else
543
+ close_connection
544
+ return
545
+ end
354
546
  end
547
+
355
548
  unless @headers_completed
356
- if data.index(Crnrn)
549
+ if data =~ /\r\n\r\n/
357
550
  @headers_completed = true
358
- h,d = data.split(Rrnrn)
551
+ h,data = data.split(/\r\n\r\n/,2)
359
552
  @headers << h << Crnrn
360
- @headers =~ /Content-Length:\s*([^\r\n]+)/
361
- @content_length = $1.to_i
553
+ if @headers =~ /Content-[Ll]ength:\s*([^\r]+)/
554
+ @content_length = $1.to_i
555
+ elsif @headers =~ /Transfer-encoding:\s*chunked/
556
+ @content_length = nil
557
+ else
558
+ @content_length = 0
559
+ end
362
560
  @associate.send_data @headers
363
- data = d
364
561
  else
365
562
  @headers << data
366
563
  end
@@ -369,15 +566,20 @@ module Swiftcore
369
566
  if @headers_completed
370
567
  @associate.send_data data
371
568
  @content_sent += data.length
372
- if @content_sent >= @content_length
569
+ if @content_length and @content_sent >= @content_length or data[-6..-1] == C0rnrn
373
570
  @associate.close_connection_after_writing
374
571
  @associate = nil
375
- setup
572
+ @headers = ''
573
+ @headers_completed = false
574
+ #@content_length = nil
575
+ @content_sent = 0
576
+ #setup
376
577
  ProxyBag.add_server self
377
578
  end
378
579
  end
580
+ # TODO: Log these errors!
379
581
  rescue
380
- @associate.close_connection_after_writing
582
+ @associate.close_connection_after_writing if @associate
381
583
  @associate = nil
382
584
  setup
383
585
  ProxyBag.add_server self
@@ -390,7 +592,13 @@ module Swiftcore
390
592
 
391
593
  def unbind
392
594
  if @associate
393
- @associate.close_connection_after_writing
595
+ if !@associate.redeployable or @content_length
596
+ @associate.close_connection_after_writing
597
+ else
598
+ @associate.associate = nil
599
+ @associate.setup_for_redeployment
600
+ ProxyBag.rebind_frontend_client(@associate)
601
+ end
394
602
  else
395
603
  ProxyBag.remove_server(self)
396
604
  end
@@ -410,36 +618,157 @@ module Swiftcore
410
618
  # handlers, then create the timers that are used to expire unserviced
411
619
  # clients and to update the Proxy's clock.
412
620
 
413
- def self.run(config, key = nil)
414
- existing_backends = {}
621
+ def self.run(config)
622
+ @existing_backends = {}
623
+
624
+ # Default is to assume we want to try to turn epoll support on. EM
625
+ # ignores this on platforms that don't support it, so this is safe.
626
+ EventMachine.epoll unless config.has_key?(Cepoll) and !config[Cepoll]
627
+ EventMachine.set_descriptor_table_size(4096 || config[Cepoll_descriptors]) if config[Cepoll]
415
628
  EventMachine.run do
416
- EventMachine.start_server(config[Ccluster_address], config[Ccluster_port], ClusterProtocol)
417
- config[Cmap].each do |m|
418
- if m[Ckeepalive]
419
- incoming_hash = Digest::SHA512.hexdigest(m[Cincoming].join('|'))
420
- m[Cincoming].each do |p|
421
- ProxyBag.add_incoming_mapping(incoming_hash,p)
422
- m[Coutgoing].each do |o|
423
- ProxyBag.default_name = p if m[Cdefault]
424
- if existing_backends.has_key?(o)
425
- # Do we need to do anything here other than skip?
426
- next
427
- else
428
- existing_backends[o] = true
429
- backend_class = Class.new(BackendProtocol)
430
- backend_class.bname = incoming_hash
431
- host, port = o.split(/:/,2)
432
- EventMachine.start_server(host, port.to_i, backend_class)
629
+ trap("HUP") {em_config(Swiftcore::SwiftiplyExec.parse_options); GC.start}
630
+ trap("INT") {EventMachine.stop_event_loop}
631
+ em_config(config)
632
+ GC.start
633
+ end
634
+ end
635
+
636
+ def self.em_config(config)
637
+ new_config = {}
638
+ if RunningConfig[Ccluster_address] != config[Ccluster_address] or RunningConfig[Ccluster_port] != config[Ccluster_port]
639
+ begin
640
+ new_config[Ccluster_server] = EventMachine.start_server(
641
+ config[Ccluster_address],
642
+ config[Ccluster_port],
643
+ ClusterProtocol)
644
+ rescue RuntimeError => e
645
+ advice = ''
646
+ if config[Ccluster_port] < 1024
647
+ advice << 'Make sure you have the correct permissions to use that port, and make sure there is nothing else running on that port.'
648
+ else
649
+ advice << 'Make sure there is nothing else running on that port.'
650
+ end
651
+ advice << " The original error was: #{e}\n"
652
+ raise EMStartServerError.new("The listener on #{config[Ccluster_address]}:#{config[Ccluster_port]} could not be started.\n#{advice}")
653
+ end
654
+ new_config[Ccluster_address] = config[Ccluster_address]
655
+ new_config[Ccluster_port] = config[Ccluster_port]
656
+ RunningConfig[Ccluster_server].stop_server if RunningConfig.has_key?(Ccluster_server)
657
+ else
658
+ new_config[Ccluster_server] = RunningConfig[Ccluster_server]
659
+ new_config[Ccluster_address] = RunningConfig[Ccluster_address]
660
+ new_config[Ccluster_port] = RunningConfig[Ccluster_port]
661
+ end
662
+
663
+ new_config[Coutgoing] = {}
664
+
665
+ config[Cmap].each do |m|
666
+ if m[Ckeepalive]
667
+ # keepalive requests are standard Swiftiply requests.
668
+
669
+ # The hash of the "outgoing" config section. It is used to
670
+ # uniquely identify a section.
671
+ hash = Digest::SHA256.hexdigest(m[Cincoming].sort.join('|')).intern
672
+
673
+ # For each incoming entry, do setup.
674
+ new_config[Cincoming] = {}
675
+ m[Cincoming].each do |p_|
676
+ p = p_.intern
677
+ new_config[Cincoming][p] = {}
678
+ ProxyBag.add_incoming_mapping(hash,p)
679
+
680
+ if m.has_key?(Cdocroot)
681
+ ProxyBag.add_incoming_docroot(m[Cdocroot],p)
682
+ else
683
+ ProxyBag.remove_incoming_docroot(p)
684
+ end
685
+
686
+ if m[Credeployable]
687
+ ProxyBag.add_incoming_redeployable(m[Credeployment_sizelimit] || 16384,p)
688
+ else
689
+ ProxyBag.remove_incoming_redeployable(p)
690
+ end
691
+
692
+ if m.has_key?(Ckey)
693
+ ProxyBag.set_key(hash,m[Ckey])
694
+ else
695
+ ProxyBag.set_key(hash,C_empty)
696
+ end
697
+
698
+ if m.has_key?(Ccache_extensions) or m.has_key?(Ccache_directory)
699
+ require 'swiftcore/Swiftiply/support_pagecache'
700
+ ProxyBag.add_suffix_list((m[Ccache_extensions] || ProxyBag.const_get(:DefaultSuffixes)),p)
701
+ ProxyBag.add_cache_dir((m[Ccache_directory] || ProxyBag.const_get(:DefaultCacheDir)),p)
702
+ else
703
+ ProxyBag.remove_suffix_list(p) if ProxyBag.respond_to?(:remove_suffix_list)
704
+ ProxyBag.remove_cache_dir(p) if ProxyBag.respond_to?(:remove_cache_dir)
705
+ end
706
+
707
+ m[Coutgoing].each do |o|
708
+ ProxyBag.default_name = p if m[Cdefault]
709
+ if @existing_backends.has_key?(o)
710
+ new_config[Coutgoing][o] ||= RunningConfig[Coutgoing][o]
711
+ next
712
+ else
713
+ @existing_backends[o] = true
714
+ backend_class = Class.new(BackendProtocol)
715
+ backend_class.bname = hash
716
+ host, port = o.split(/:/,2)
717
+ begin
718
+ new_config[Coutgoing][o] = EventMachine.start_server(host, port.to_i, backend_class)
719
+ rescue RuntimeError => e
720
+ advice = ''
721
+ if port.to_i < 1024
722
+ advice << 'Make sure you have the correct permissions to use that port, and make sure there is nothing else running on that port.'
723
+ else
724
+ advice << 'Make sure there is nothing else running on that port.'
725
+ end
726
+ advice << " The original error was: #{e}\n"
727
+ raise EMStartServerError.new("The listener on #{host}:#{port} could not be started.\n#{advice}")
433
728
  end
434
729
  end
435
730
  end
731
+
732
+ # Now stop everything that is still running but which isn't needed.
733
+ if RunningConfig.has_key?(Coutgoing)
734
+ (RunningConfig[Coutgoing].keys - new_config[Coutgoing].keys).each do |unneeded_server_key|
735
+ RunningConfig[Coutgoing][unneeded_server_key].stop_server
736
+ end
737
+ end
436
738
  end
739
+ else
740
+ # This is where the code goes that sets up traditional proxy destinations.
741
+ # This is a future TODO item.
437
742
  end
438
- ProxyBag.server_unavailable_timeout ||= config[Ctimeout]
439
- ProxyBag.key = key
743
+ end
744
+
745
+ #EventMachine.set_effective_user = config[Cuser] if config[Cuser] and RunningConfig[Cuser] != config[Cuser]
746
+ run_as(config[Cuser],config[Cgroup]) if (config[Cuser] and RunningConfig[Cuser] != config[Cuser]) or (config[Cgroup] and RunningConfig[Cgroup] != config[Cgroup])
747
+ new_config[Cuser] = config[Cuser]
748
+ new_config[Cgroup] = config[Cgroup]
749
+
750
+ ProxyBag.server_unavailable_timeout ||= config[Ctimeout]
751
+ ProxyBag.chunked_encoding_threshold = config[Cchunked_encoding_threshold] || 16384
752
+
753
+ unless RunningConfig[:initialized]
440
754
  EventMachine.add_periodic_timer(2) { ProxyBag.expire_clients }
441
755
  EventMachine.add_periodic_timer(1) { ProxyBag.update_ctime }
756
+ new_config[:initialized] = true
442
757
  end
758
+
759
+ RunningConfig.replace new_config
760
+ end
761
+
762
+
763
+ # This can be used to change the effective user and group that
764
+ # Swiftiply is running as.
765
+
766
+ def self.run_as(user = "nobody", group = "nobody")
767
+ Process.initgroups(user,Etc.getgrnam(group).gid) if user and group
768
+ ::Process::GID.change_privilege(Etc.getgrnam(group).gid) if group
769
+ ::Process::UID.change_privilege(Etc.getpwnam(user).uid) if user
770
+ rescue Errno::EPERM
771
+ raise "Failed to change the effective user to #{user} and the group to #{group}"
443
772
  end
444
773
  end
445
774
  end