spider-gazelle 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,80 @@
1
+ require 'thread'
2
+
3
+ module SpiderGazelle
4
+ class Spider
5
+ class Binding
6
+ attr_reader :app_id
7
+ attr_accessor :tcp
8
+
9
+
10
+ def initialize(iterator, app_id, options)
11
+ if options[:mode] == :no_ipc
12
+ @delegate = method(:direct_delegate)
13
+ @gazelle = iterator
14
+ else
15
+ @delegate = method(:delegate)
16
+ @select_gazelle = iterator
17
+ end
18
+
19
+ @options = options
20
+
21
+ @logger = Logger.instance
22
+ @signaller = Signaller.instance
23
+ @thread = @signaller.thread
24
+
25
+ @port = @options[:port]
26
+ @indicator = app_id.to_s.freeze
27
+ end
28
+
29
+ # Bind the application to the selected port
30
+ def bind
31
+ # Bind the socket
32
+ @tcp = @thread.tcp
33
+ @tcp.bind @options[:host], @port, @delegate
34
+ @tcp.listen @options[:backlog]
35
+ @tcp.enable_simultaneous_accepts
36
+
37
+ @logger.info "Listening on tcp://#{@options[:host]}:#{@port}"
38
+
39
+ @tcp.catch do |error|
40
+ begin
41
+ @logger.print_error(error)
42
+ rescue
43
+ ensure
44
+ @signaller.general_failure
45
+ end
46
+ end
47
+ @tcp
48
+ end
49
+
50
+ # Close the bindings
51
+ def unbind
52
+ @tcp.close unless @tcp.nil?
53
+ @tcp
54
+ end
55
+
56
+
57
+ protected
58
+
59
+
60
+ DELEGATE_ERR = proc { |error|
61
+ client.close
62
+ begin
63
+ Logger.instance.print_error(error, "delegating socket to gazelle")
64
+ rescue
65
+ end
66
+ }
67
+ def delegate(client, retries = 0)
68
+ promise = @select_gazelle.next.write2(client, @indicator)
69
+ promise.then do
70
+ client.close
71
+ end
72
+ promise.catch DELEGATE_ERR
73
+ end
74
+
75
+ def direct_delegate(client)
76
+ @gazelle.__send__(:process_connection, client, @indicator)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,94 +1,98 @@
1
- require 'spider-gazelle/const'
2
1
  require 'websocket/driver'
3
2
  require 'forwardable'
4
3
 
5
4
  module SpiderGazelle
6
- class Websocket < ::Libuv::Q::DeferredPromise
7
- include Const
8
-
9
- attr_reader :env, :url, :loop
10
-
11
- extend Forwardable
12
- def_delegators :@driver, :start, :ping, :protocol, :ready_state, :set_header, :state, :close
13
- def_delegators :@socket, :write
14
-
15
- def initialize(tcp, env)
16
- @socket, @env = tcp, env
17
-
18
- # Initialise the promise
19
- super @socket.loop, @socket.loop.defer
20
-
21
- scheme = env[RACK_URL_SCHEME] == HTTPS ? 'wss://' : 'ws://'
22
- @url = scheme + env[HTTP_HOST] + env[REQUEST_URI]
23
- @driver = ::WebSocket::Driver.rack self
24
-
25
- # Pass data from the socket to the driver
26
- @socket.progress &method(:socket_read)
27
- @socket.finally &method(:socket_close)
28
-
29
-
30
- # Driver has indicated that it is closing
31
- # We'll close the socket after writing any remaining data
32
- @driver.on :close, &method(:on_close)
33
- @driver.on :message, &method(:on_message)
34
- @driver.on :error, &method(:on_error)
35
- end
36
-
37
- # Write some text to the websocket connection
38
- #
39
- # @param string [String] a string of data to be sent to the far end
40
- def text(string)
41
- @loop.schedule { @driver.text(string.to_s) }
42
- end
43
-
44
- # Write some binary data to the websocket connection
45
- #
46
- # @param array [Array] an array of bytes to be sent to the far end
47
- def binary(array)
48
- @loop.schedule { @driver.binary(array.to_a) }
49
- end
50
-
51
- # Used to define a callback when data is received from the client
52
- #
53
- # @param callback [Proc] the callback to be called when data is received
54
- def progress(callback = nil, &blk)
55
- @progress = callback || blk
56
- end
57
-
58
- # Used to define a callback when the websocket connection is established
59
- # Data sent before this callback is buffered.
60
- #
61
- # @param callback [Proc] the callback to be triggered on establishment
62
- def on_open(callback = nil, &blk)
63
- callback ||= blk
64
- @driver.on :open, &callback
65
- end
66
-
67
- protected
68
-
69
- def socket_read(data, tcp)
70
- @driver.parse data
71
- end
72
-
73
- def socket_close
74
- if @shutdown_called.nil?
75
- @defer.reject WebSocket::Driver::CloseEvent.new(1006, 'connection was closed unexpectedly')
76
- end
77
- end
78
-
79
-
80
- def on_message(event)
81
- @progress.call(event.data, self) unless @progress.nil?
82
- end
83
-
84
- def on_error(event)
85
- @defer.reject event
86
- end
87
-
88
- def on_close(event)
89
- @shutdown_called = true
90
- @socket.shutdown
91
- @defer.resolve event
5
+ class Websocket < ::Libuv::Q::DeferredPromise
6
+ attr_reader :env, :url, :loop
7
+
8
+
9
+ RACK_URL_SCHEME = "rack.url_scheme".freeze
10
+ HTTP_HOST = "HTTP_HOST".freeze
11
+ REQUEST_URI= "REQUEST_URI".freeze
12
+ HTTPS = 'https'.freeze
13
+
14
+
15
+ extend Forwardable
16
+ def_delegators :@driver, :start, :ping, :protocol, :ready_state, :set_header, :state, :close
17
+ def_delegators :@socket, :write
18
+
19
+ def initialize(tcp, env)
20
+ @socket, @env = tcp, env
21
+
22
+ # Initialise the promise
23
+ super tcp.loop, tcp.loop.defer
24
+
25
+ scheme = env[RACK_URL_SCHEME] == HTTPS ? 'wss://' : 'ws://'
26
+ @url = scheme + env[HTTP_HOST] + env[REQUEST_URI]
27
+ @driver = ::WebSocket::Driver.rack self
28
+
29
+ # Pass data from the socket to the driver
30
+ @socket.progress &method(:socket_read)
31
+ @socket.finally &method(:socket_close)
32
+
33
+
34
+ # Driver has indicated that it is closing
35
+ # We'll close the socket after writing any remaining data
36
+ @driver.on :close, &method(:on_close)
37
+ @driver.on :message, &method(:on_message)
38
+ @driver.on :error, &method(:on_error)
39
+ end
40
+
41
+ # Write some text to the websocket connection
42
+ #
43
+ # @param string [String] a string of data to be sent to the far end
44
+ def text(string)
45
+ @loop.schedule { @driver.text(string.to_s) }
46
+ end
47
+
48
+ # Write some binary data to the websocket connection
49
+ #
50
+ # @param array [Array] an array of bytes to be sent to the far end
51
+ def binary(array)
52
+ @loop.schedule { @driver.binary(array.to_a) }
53
+ end
54
+
55
+ # Used to define a callback when data is received from the client
56
+ #
57
+ # @param callback [Proc] the callback to be called when data is received
58
+ def progress(callback = nil, &blk)
59
+ @progress = callback || blk
60
+ end
61
+
62
+ # Used to define a callback when the websocket connection is established
63
+ # Data sent before this callback is buffered.
64
+ #
65
+ # @param callback [Proc] the callback to be triggered on establishment
66
+ def on_open(callback = nil, &blk)
67
+ callback ||= blk
68
+ @driver.on :open, &callback
69
+ end
70
+
71
+ protected
72
+
73
+ def socket_read(data, tcp)
74
+ @driver.parse data
75
+ end
76
+
77
+ def socket_close
78
+ if @shutdown_called.nil?
79
+ @defer.reject WebSocket::Driver::CloseEvent.new(1006, 'connection was closed unexpectedly')
80
+ end
81
+ end
82
+
83
+
84
+ def on_message(event)
85
+ @progress.call(event.data, self) unless @progress.nil?
86
+ end
87
+
88
+ def on_error(event)
89
+ @defer.reject event
90
+ end
91
+
92
+ def on_close(event)
93
+ @shutdown_called = true
94
+ @socket.shutdown
95
+ @defer.resolve event
96
+ end
92
97
  end
93
- end
94
98
  end
@@ -0,0 +1,173 @@
1
+ require 'spider-gazelle/gazelle/http1'
2
+
3
+
4
+ # TODO:: Mock logger
5
+
6
+ class MockSocket
7
+ def initialize
8
+ @stopped = false
9
+ @write_cb = proc {}
10
+ end
11
+
12
+ attr_reader :closed, :stopped
13
+ attr_accessor :storage, :write_cb, :shutdown_cb, :close_cb
14
+
15
+ def peername; ['127.0.0.1']; end
16
+ def finally(method = nil, &block); @on_close = method || block; end
17
+ def close; @close_cb.call; end
18
+ def shutdown; @shutdown_cb.call; end
19
+ def progress(_); end
20
+ def stop_read; @stopped = true; end
21
+
22
+ def write(data); @write_cb.call(data); end
23
+ end
24
+
25
+ class MockLogger
26
+ def initialize
27
+ @logged = []
28
+ end
29
+
30
+ attr_reader :logged
31
+
32
+ def method_missing(methId, *args)
33
+ @logged << methId
34
+ p "methId: #{args}"
35
+ end
36
+ end
37
+
38
+ describe ::SpiderGazelle::Gazelle::Http1 do
39
+ before :each do
40
+ @shutdown_called = 0
41
+ @close_called = 0
42
+
43
+ @loop = ::Libuv::Loop.default
44
+ @timeout = @loop.timer do
45
+ @loop.stop
46
+ @general_failure << "test timed out"
47
+ end
48
+ @timeout.start(1000)
49
+
50
+
51
+ @return ||= proc {|http1|
52
+ @returned = http1
53
+ }
54
+ @logger = MockLogger.new
55
+ @http1_callbacks ||= ::SpiderGazelle::Gazelle::Http1::Callbacks.new
56
+ @http1 = ::SpiderGazelle::Gazelle::Http1.new(@return, @http1_callbacks, @loop, @logger)
57
+
58
+ @socket = MockSocket.new
59
+ @socket.shutdown_cb = proc {
60
+ @shutdown_called += 1
61
+ @loop.stop
62
+ }
63
+ @socket.close_cb = proc {
64
+ @close_called += 1
65
+ @loop.stop
66
+ }
67
+
68
+ @app_mode = :thread_pool
69
+ @port = 80
70
+ @tls = false
71
+ end
72
+
73
+ after :each do
74
+ @timeout.close
75
+ end
76
+
77
+ it "should process a single request and close the connection", http1: true do
78
+ app = lambda do |env|
79
+ body = 'Hello, World!'
80
+ [200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
81
+ end
82
+ writes = []
83
+
84
+ @loop.run {
85
+ @http1.load(@socket, @port, app, @app_mode, @tls)
86
+ @http1.parse("GET / HTTP/1.1\r\nConnection: Close\r\n\r\n")
87
+
88
+ @socket.write_cb = proc { |data|
89
+ writes << data
90
+ }
91
+ }
92
+
93
+ expect(@shutdown_called).to be == 1
94
+ expect(@close_called).to be == 0
95
+ expect(writes).to eq([
96
+ "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\nConnection: close\r\n\r\n",
97
+ "Hello, World!"
98
+ ])
99
+ end
100
+
101
+ it "should process a single request and keep the connection open", http1: true do
102
+ app = lambda do |env|
103
+ body = 'Hello, World!'
104
+ [200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
105
+ end
106
+ writes = []
107
+
108
+ @loop.run {
109
+ @http1.load(@socket, @port, app, @app_mode, @tls)
110
+ @http1.parse("GET / HTTP/1.1\r\n\r\n")
111
+
112
+ @socket.write_cb = proc { |data|
113
+ writes << data
114
+ if writes.length == 2
115
+ @loop.next_tick do
116
+ @loop.stop
117
+ end
118
+ end
119
+ }
120
+ }
121
+
122
+ expect(@shutdown_called).to be == 0
123
+ expect(@close_called).to be == 0
124
+ expect(writes).to eq([
125
+ "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\n",
126
+ "Hello, World!"
127
+ ])
128
+ end
129
+
130
+ it "should fill out the environment properly", http1: true do
131
+ app = lambda do |env|
132
+ expect(env['REQUEST_URI']).to eq('/?test=ing')
133
+ expect(env['REQUEST_PATH']).to eq('/')
134
+ expect(env['QUERY_STRING']).to eq('test=ing')
135
+ expect(env['SERVER_NAME']).to eq('spider.gazelle.net')
136
+ expect(env['SERVER_PORT']).to eq(80)
137
+ expect(env['REMOTE_ADDR']).to eq('127.0.0.1')
138
+ expect(env['rack.url_scheme']).to eq('http')
139
+
140
+ body = 'Hello, World!'
141
+ [200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
142
+ end
143
+
144
+ @loop.run {
145
+ @http1.load(@socket, @port, app, @app_mode, @tls)
146
+ @http1.parse("GET /?test=ing HTTP/1.1\r\nHost: spider.gazelle.net\r\nConnection: Close\r\n\r\n")
147
+ }
148
+
149
+ expect(@shutdown_called).to be == 1
150
+ expect(@close_called).to be == 0
151
+ end
152
+
153
+ it "should respond with a chunked response", http1: true do
154
+ app = lambda do |env|
155
+ body = ['Hello', ',', ' World!']
156
+ [200, {'Content-Type' => 'text/plain'}, body]
157
+ end
158
+ writes = []
159
+
160
+ @loop.run {
161
+ @http1.load(@socket, @port, app, @app_mode, @tls)
162
+ @http1.parse("GET / HTTP/1.1\r\nConnection: Close\r\n\r\n")
163
+
164
+ @socket.write_cb = proc { |data|
165
+ writes << data
166
+ }
167
+ }
168
+
169
+ expect(@shutdown_called).to be == 1
170
+ expect(@close_called).to be == 0
171
+ expect(writes).to eq(["HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n", "5\r\nHello\r\n", "1\r\n,\r\n", "7\r\n World!\r\n", "0\r\n\r\n"])
172
+ end
173
+ end
@@ -6,121 +6,121 @@ require 'rack/lock_patch'
6
6
 
7
7
 
8
8
  module Rack
9
- class Lock
10
- def would_block
11
- @count > 0
9
+ class Lock
10
+ def would_block
11
+ @count > 0
12
+ end
12
13
  end
13
- end
14
14
  end
15
15
 
16
16
 
17
- def lock_app(app)
17
+ def lock_app(app)
18
18
  app = Rack::Lock.new(app)
19
19
  return app, Rack::Lint.new(app)
20
- end
20
+ end
21
21
 
22
22
 
23
23
  describe Rack::Lock do
24
24
 
25
- describe 'Proxy' do
26
-
27
- it 'delegate each' do
28
- env = Rack::MockRequest.env_for("/")
29
- response = Class.new {
30
- attr_accessor :close_called
31
- def initialize; @close_called = false; end
32
- def each; %w{ hi mom }.each { |x| yield x }; end
33
- }.new
34
-
35
- app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] })[1]
36
- response = app.call(env)[2]
37
- list = []
38
- response.each { |x| list << x }
39
- expect(list).to eq(%w{ hi mom })
40
- end
25
+ describe 'Proxy' do
26
+
27
+ it 'delegate each' do
28
+ env = Rack::MockRequest.env_for("/")
29
+ response = Class.new {
30
+ attr_accessor :close_called
31
+ def initialize; @close_called = false; end
32
+ def each; %w{ hi mom }.each { |x| yield x }; end
33
+ }.new
34
+
35
+ app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] })[1]
36
+ response = app.call(env)[2]
37
+ list = []
38
+ response.each { |x| list << x }
39
+ expect(list).to eq(%w{ hi mom })
40
+ end
41
+
42
+ it 'delegate to_path' do
43
+ env = Rack::MockRequest.env_for("/")
41
44
 
42
- it 'delegate to_path' do
43
- env = Rack::MockRequest.env_for("/")
45
+ res = ['Hello World']
46
+ def res.to_path ; "/tmp/hello.txt" ; end
44
47
 
45
- res = ['Hello World']
46
- def res.to_path ; "/tmp/hello.txt" ; end
48
+ app = Rack::Lock.new(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, res] })
49
+ body = app.call(env)[2]
47
50
 
48
- app = Rack::Lock.new(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, res] })
49
- body = app.call(env)[2]
51
+ expect(body.respond_to? :to_path).to eq(true)
52
+ expect(body.to_path).to eq("/tmp/hello.txt")
53
+ end
50
54
 
51
- expect(body.respond_to? :to_path).to eq(true)
52
- expect(body.to_path).to eq("/tmp/hello.txt")
55
+ it 'not delegate to_path if body does not implement it' do
56
+ env = Rack::MockRequest.env_for("/")
57
+
58
+ res = ['Hello World']
59
+
60
+ app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, res] })[1]
61
+ body = app.call(env)[2]
62
+
63
+ expect(body.respond_to? :to_path).to eq(false)
64
+ end
53
65
  end
54
66
 
55
- it 'not delegate to_path if body does not implement it' do
56
- env = Rack::MockRequest.env_for("/")
67
+ it 'call super on close' do
68
+ env = Rack::MockRequest.env_for("/")
69
+ response = Class.new {
70
+ attr_accessor :close_called
71
+ def initialize; @close_called = false; end
72
+ def close; @close_called = true; end
73
+ }.new
74
+
75
+ app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] })[1]
76
+ app.call(env)
77
+ expect(response.close_called).to eq(false)
78
+ response.close
79
+ expect(response.close_called).to eq(true)
80
+ end
57
81
 
58
- res = ['Hello World']
82
+ it "not unlock until body is closed" do
83
+ env = Rack::MockRequest.env_for("/")
84
+ response = Object.new
85
+ lock, app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] })
59
86
 
60
- app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, res] })[1]
61
- body = app.call(env)[2]
87
+ expect(lock.would_block).to eq(false)
88
+ response = app.call(env)[2]
89
+ expect(lock.would_block).to eq(true)
90
+ response.close
91
+ expect(lock.would_block).to eq(false)
92
+ end
93
+
94
+ it "return value from app" do
95
+ env = Rack::MockRequest.env_for("/")
96
+ body = [200, {"Content-Type" => "text/plain"}, %w{ hi mom }]
97
+ app = lock_app(lambda { |inner_env| body })[1]
98
+
99
+ res = app.call(env)
100
+ expect(res[0]).to eq(body[0])
101
+ expect(res[1]).to eq(body[1])
102
+ expect(res[2].to_enum.to_a).to eq(["hi", "mom"])
103
+ end
104
+
105
+ it "call synchronize on lock" do
106
+ env = Rack::MockRequest.env_for("/")
107
+ lock, app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, %w{ a b c }] })
108
+ expect(lock.would_block).to eq(false)
109
+ app.call(env)
110
+ expect(lock.would_block).to eq(true)
111
+ end
112
+
113
+ it "unlock if the app raises" do
114
+ env = Rack::MockRequest.env_for("/")
115
+ lock, app = lock_app(lambda { raise Exception })
116
+ expect { app.call(env) }.to raise_error(Exception)
117
+ expect(lock.would_block).to eq(false)
118
+ end
62
119
 
63
- expect(body.respond_to? :to_path).to eq(false)
120
+ it "unlock if the app throws" do
121
+ env = Rack::MockRequest.env_for("/")
122
+ lock, app = lock_app(lambda {|_| throw :bacon })
123
+ expect { app.call(env) }.to raise_error(ArgumentError)
124
+ expect(lock.would_block).to eq(false)
64
125
  end
65
- end
66
-
67
- it 'call super on close' do
68
- env = Rack::MockRequest.env_for("/")
69
- response = Class.new {
70
- attr_accessor :close_called
71
- def initialize; @close_called = false; end
72
- def close; @close_called = true; end
73
- }.new
74
-
75
- app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] })[1]
76
- app.call(env)
77
- expect(response.close_called).to eq(false)
78
- response.close
79
- expect(response.close_called).to eq(true)
80
- end
81
-
82
- it "not unlock until body is closed" do
83
- env = Rack::MockRequest.env_for("/")
84
- response = Object.new
85
- lock, app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] })
86
-
87
- expect(lock.would_block).to eq(false)
88
- response = app.call(env)[2]
89
- expect(lock.would_block).to eq(true)
90
- response.close
91
- expect(lock.would_block).to eq(false)
92
- end
93
-
94
- it "return value from app" do
95
- env = Rack::MockRequest.env_for("/")
96
- body = [200, {"Content-Type" => "text/plain"}, %w{ hi mom }]
97
- app = lock_app(lambda { |inner_env| body })[1]
98
-
99
- res = app.call(env)
100
- expect(res[0]).to eq(body[0])
101
- expect(res[1]).to eq(body[1])
102
- expect(res[2].to_enum.to_a).to eq(["hi", "mom"])
103
- end
104
-
105
- it "call synchronize on lock" do
106
- env = Rack::MockRequest.env_for("/")
107
- lock, app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, %w{ a b c }] })
108
- expect(lock.would_block).to eq(false)
109
- app.call(env)
110
- expect(lock.would_block).to eq(true)
111
- end
112
-
113
- it "unlock if the app raises" do
114
- env = Rack::MockRequest.env_for("/")
115
- lock, app = lock_app(lambda { raise Exception })
116
- expect { app.call(env) }.to raise_error(Exception)
117
- expect(lock.would_block).to eq(false)
118
- end
119
-
120
- it "unlock if the app throws" do
121
- env = Rack::MockRequest.env_for("/")
122
- lock, app = lock_app(lambda {|_| throw :bacon })
123
- expect { app.call(env) }.to raise_error(ArgumentError)
124
- expect(lock.would_block).to eq(false)
125
- end
126
126
  end