swiftiply 0.5.1 → 0.6.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 (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