tipi 0.31 → 0.36

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/Gemfile.lock +10 -4
  4. data/LICENSE +1 -1
  5. data/TODO.md +13 -47
  6. data/bin/tipi +13 -0
  7. data/df/agent.rb +63 -0
  8. data/df/etc_benchmark.rb +15 -0
  9. data/df/multi_agent_supervisor.rb +87 -0
  10. data/df/multi_client.rb +84 -0
  11. data/df/routing_benchmark.rb +60 -0
  12. data/df/sample_agent.rb +89 -0
  13. data/df/server.rb +54 -0
  14. data/df/sse_page.html +29 -0
  15. data/df/stress.rb +24 -0
  16. data/df/ws_page.html +38 -0
  17. data/examples/http_request_ws_server.rb +34 -0
  18. data/examples/http_server.rb +6 -6
  19. data/examples/http_server_forked.rb +4 -5
  20. data/examples/http_server_form.rb +23 -0
  21. data/examples/http_server_throttled_accept.rb +23 -0
  22. data/examples/http_unix_socket_server.rb +17 -0
  23. data/examples/http_ws_server.rb +10 -12
  24. data/examples/routing_server.rb +34 -0
  25. data/examples/websocket_client.rb +1 -2
  26. data/examples/websocket_demo.rb +4 -2
  27. data/examples/ws_page.html +1 -2
  28. data/lib/tipi.rb +7 -5
  29. data/lib/tipi/config_dsl.rb +153 -0
  30. data/lib/tipi/configuration.rb +1 -1
  31. data/lib/tipi/digital_fabric.rb +7 -0
  32. data/lib/tipi/digital_fabric/agent.rb +225 -0
  33. data/lib/tipi/digital_fabric/agent_proxy.rb +265 -0
  34. data/lib/tipi/digital_fabric/executive.rb +100 -0
  35. data/lib/tipi/digital_fabric/executive/index.html +69 -0
  36. data/lib/tipi/digital_fabric/protocol.rb +90 -0
  37. data/lib/tipi/digital_fabric/request_adapter.rb +48 -0
  38. data/lib/tipi/digital_fabric/service.rb +230 -0
  39. data/lib/tipi/http1_adapter.rb +59 -37
  40. data/lib/tipi/http2_adapter.rb +5 -3
  41. data/lib/tipi/http2_stream.rb +19 -7
  42. data/lib/tipi/rack_adapter.rb +11 -3
  43. data/lib/tipi/version.rb +1 -1
  44. data/lib/tipi/websocket.rb +33 -13
  45. data/test/helper.rb +1 -2
  46. data/test/test_http_server.rb +3 -2
  47. data/test/test_request.rb +108 -0
  48. data/tipi.gemspec +7 -3
  49. metadata +59 -7
  50. data/lib/tipi/request.rb +0 -118
@@ -5,14 +5,14 @@ require 'tipi'
5
5
  require 'tipi/websocket'
6
6
 
7
7
  def ws_handler(conn)
8
- timer = spin do
9
- throttled_loop(1) do
10
- conn << Time.now.to_s
11
- end
8
+ timer = spin_loop(interval: 1) do
9
+ conn << Time.now.to_s
12
10
  end
13
11
  while (msg = conn.recv)
14
12
  conn << "you said: #{msg}"
15
13
  end
14
+ rescue Exception => e
15
+ p e
16
16
  ensure
17
17
  timer.stop
18
18
  end
@@ -21,17 +21,15 @@ opts = {
21
21
  reuse_addr: true,
22
22
  dont_linger: true,
23
23
  upgrade: {
24
- websocket: Polyphony::Websocket.handler(&method(:ws_handler))
24
+ websocket: Tipi::Websocket.handler(&method(:ws_handler))
25
25
  }
26
26
  }
27
27
 
28
28
  HTML = IO.read(File.join(__dir__, 'ws_page.html'))
29
29
 
30
- spin do
31
- Tipi.serve('0.0.0.0', 1234, opts) do |req|
32
- req.respond(HTML, 'Content-Type' => 'text/html')
33
- end
34
- end
35
-
36
30
  puts "pid: #{Process.pid}"
37
- puts 'Listening on port 1234...'
31
+ puts 'Listening on port 4411...'
32
+
33
+ Tipi.serve('0.0.0.0', 4411, opts) do |req|
34
+ req.respond(HTML, 'Content-Type' => 'text/html')
35
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'tipi'
5
+
6
+ opts = {
7
+ reuse_addr: true,
8
+ dont_linger: true
9
+ }
10
+
11
+ puts "pid: #{Process.pid}"
12
+ puts 'Listening on port 4411...'
13
+
14
+ app = Tipi.route do |r|
15
+ r.root do
16
+ r.redirect '/hello'
17
+ end
18
+ r.on 'hello' do
19
+ r.get 'world' do
20
+ r.respond 'Hello world'
21
+ end
22
+ r.get do
23
+ r.respond 'Hello'
24
+ end
25
+ r.post do
26
+ puts 'Someone said Hello'
27
+ r.redirect '/'
28
+ end
29
+ end
30
+ end
31
+
32
+ spin do
33
+ Tipi.serve('0.0.0.0', 4411, opts, &app)
34
+ end.await
@@ -26,8 +26,7 @@ class WebsocketClient
26
26
  end
27
27
 
28
28
  def receive
29
- loop do
30
- data = @socket.readpartial(8192)
29
+ @socket.read_loop do |data|
31
30
  @reader << data
32
31
  parsed = @reader.next
33
32
  return parsed if parsed
@@ -30,8 +30,10 @@ class WebsocketClient
30
30
  end
31
31
 
32
32
  def receive
33
- loop do
34
- data = @socket.readpartial(8192)
33
+ parsed = @reader.next
34
+ return parsed if parsed
35
+
36
+ @socket.read_loop do |data|
35
37
  @reader << data
36
38
  parsed = @reader.next
37
39
  return parsed if parsed
@@ -6,14 +6,13 @@
6
6
  <body>
7
7
  <script>
8
8
  var connect = function () {
9
- var exampleSocket = new WebSocket("ws://localhost:1234");
9
+ var exampleSocket = new WebSocket("wss://dev.realiteq.net/");
10
10
 
11
11
  exampleSocket.onopen = function (event) {
12
12
  document.querySelector('#status').innerText = 'connected';
13
13
  exampleSocket.send("Can you hear me?");
14
14
  };
15
15
  exampleSocket.onclose = function (event) {
16
- console.log('onclose');
17
16
  document.querySelector('#status').innerText = 'disconnected';
18
17
  setTimeout(function () {
19
18
  // exampleSocket.removeAllListeners();
data/lib/tipi.rb CHANGED
@@ -28,10 +28,8 @@ module Tipi
28
28
  end
29
29
 
30
30
  def accept_loop(server, opts, &handler)
31
- loop do
32
- client = server.accept
31
+ server.accept_loop do |client|
33
32
  spin { client_loop(client, opts, &handler) }
34
- snooze
35
33
  rescue OpenSSL::SSL::SSLError
36
34
  # disregard
37
35
  end
@@ -42,14 +40,18 @@ module Tipi
42
40
  adapter = protocol_adapter(client, opts)
43
41
  adapter.each(&handler)
44
42
  ensure
45
- client.close
43
+ client.close rescue nil
46
44
  end
47
45
 
48
46
  def protocol_adapter(socket, opts)
49
47
  use_http2 = socket.respond_to?(:alpn_protocol) &&
50
- socket.alpn_protocol == H2_PROTOCOL
48
+ socket.alpn_protocol == H2_PROTOCOL
51
49
  klass = use_http2 ? HTTP2Adapter : HTTP1Adapter
52
50
  klass.new(socket, opts)
53
51
  end
52
+
53
+ def route(&block)
54
+ proc { |req| req.route(&block) }
55
+ end
54
56
  end
55
57
  end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tipi
4
+ module Configuration
5
+ class Interpreter
6
+ # make_blank_slate
7
+
8
+ def initialize(assembler)
9
+ @assembler = assembler
10
+ end
11
+
12
+ def gzip_response
13
+ @assembler.emit 'req = Tipi::GZip.wrap(req)'
14
+ end
15
+
16
+ def log(out)
17
+ @assembler.wrap_current_frame 'logger.log_request(req) do |req|'
18
+ end
19
+
20
+ def error(&block)
21
+ assembler.emit_exception_handler &block
22
+ end
23
+
24
+ def match(pattern, &block)
25
+ @assembler.emit_conditional "if req.path =~ #{pattern.inspect}", &block
26
+ end
27
+ end
28
+
29
+ class Assembler
30
+ def self.from_source(code)
31
+ new.from_source code
32
+ end
33
+
34
+ def from_source(code)
35
+ @stack = [new_frame]
36
+ @app_procs = {}
37
+ @interpreter = Interpreter.new self
38
+ @interpreter.instance_eval code
39
+
40
+ loop do
41
+ frame = @stack.pop
42
+ return assemble_app_proc(frame).join("\n") if @stack.empty?
43
+
44
+ @stack.last[:body] << assemble_frame(frame)
45
+ end
46
+ end
47
+
48
+ def new_frame
49
+ {
50
+ prelude: [],
51
+ body: []
52
+ }
53
+ end
54
+
55
+ def add_frame(&block)
56
+ @stack.push new_frame
57
+ yield
58
+ ensure
59
+ frame = @stack.pop
60
+ emit assemble(frame)
61
+ end
62
+
63
+ def wrap_current_frame(head)
64
+ frame = @stack.pop
65
+ wrapper = new_frame
66
+ wrapper[:body] << head
67
+ @stack.push wrapper
68
+ @stack.push frame
69
+ end
70
+
71
+ def emit(code)
72
+ @stack.last[:body] << code
73
+ end
74
+
75
+ def emit_prelude(code)
76
+ @stack.last[:prelude] << code
77
+ end
78
+
79
+ def emit_exception_handler(&block)
80
+ proc_id = add_app_proc block
81
+ @stack.last[:rescue_proc_id] = proc_id
82
+ end
83
+
84
+ def emit_block(conditional, &block)
85
+ proc_id = add_app_proc block
86
+ @stack.last[:branched] = true
87
+ emit conditional
88
+ add_frame &block
89
+ end
90
+
91
+ def add_app_proc(proc)
92
+ id = :"proc#{@app_procs.size}"
93
+ @app_procs[id] = proc
94
+ id
95
+ end
96
+
97
+ def assemble_frame(frame)
98
+ indent = 0
99
+ lines = []
100
+ emit_code lines, frame[:prelude], indent
101
+ if frame[:rescue_proc_id]
102
+ emit_code lines, 'begin', indent
103
+ indent += 1
104
+ end
105
+ emit_code lines, frame[:body], indent
106
+ if frame[:rescue_proc_id]
107
+ emit_code lines, 'rescue => e', indent
108
+ emit_code lines, " app_procs[#{frame[:rescue_proc_id].inspect}].call(req, e)", indent
109
+ emit_code lines, 'end', indent
110
+ indent -= 1
111
+ end
112
+ lines
113
+ end
114
+
115
+ def assemble_app_proc(frame)
116
+ indent = 0
117
+ lines = []
118
+ emit_code lines, frame[:prelude], indent
119
+ emit_code lines, 'proc do |req|', indent
120
+ emit_code lines, frame[:body], indent + 1
121
+ emit_code lines, 'end', indent
122
+
123
+ lines
124
+ end
125
+
126
+ def emit_code(lines, code, indent)
127
+ if code.is_a? Array
128
+ code.each { |l| emit_code lines, l, indent + 1 }
129
+ else
130
+ lines << (indent_line code, indent)
131
+ end
132
+ end
133
+
134
+ @@indents = Hash.new { |h, k| h[k] = ' ' * k }
135
+
136
+ def indent_line(code, indent)
137
+ indent == 0 ? code : "#{ @@indents[indent] }#{code}"
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+
144
+ def assemble(code)
145
+ Tipi::Configuration::Assembler.from_source(code)
146
+ end
147
+
148
+ code = assemble <<~RUBY
149
+ gzip_response
150
+ log STDOUT
151
+ RUBY
152
+
153
+ puts code
@@ -19,7 +19,7 @@ module Tipi
19
19
  end
20
20
 
21
21
  def start_listeners(config)
22
- puts "listening on port 1234"
22
+ puts "Listening on port 1234"
23
23
  @server = Polyphony::Net.tcp_listen('0.0.0.0', 1234, { reuse_addr: true, dont_linger: true })
24
24
  end
25
25
 
@@ -0,0 +1,7 @@
1
+ module DigitalFabric
2
+ end
3
+
4
+ ::DF = DigitalFabric
5
+
6
+ require_relative 'digital_fabric/service'
7
+ require_relative 'digital_fabric/agent_proxy'
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './protocol'
4
+ require_relative './request_adapter'
5
+
6
+ require 'msgpack'
7
+ require 'tipi/websocket'
8
+ require 'tipi/request'
9
+
10
+ module DigitalFabric
11
+ class Agent
12
+ def initialize(server_url, route, token)
13
+ @server_url = server_url
14
+ @route = route
15
+ @token = token
16
+ @requests = {}
17
+ @long_running_requests = {}
18
+ @name = '<unknown>'
19
+ end
20
+
21
+ class TimeoutError < RuntimeError
22
+ end
23
+
24
+ class GracefulShutdown < RuntimeError
25
+ end
26
+
27
+ def run
28
+ @fiber = Fiber.current
29
+ @keep_alive_timer = spin_loop(interval: 5) { keep_alive }
30
+ while true
31
+ connect_and_process_incoming_requests
32
+ return if @shutdown
33
+ sleep 5
34
+ end
35
+ ensure
36
+ @keep_alive_timer.stop
37
+ end
38
+
39
+ def connect_and_process_incoming_requests
40
+ # log 'Connecting...'
41
+ @socket = connect_to_server
42
+ @last_recv = @last_send = Time.now
43
+
44
+ df_upgrade
45
+ @connected = true
46
+ @msgpack_reader = MessagePack::Unpacker.new
47
+
48
+ process_incoming_requests
49
+ rescue IOError, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EPIPE, TimeoutError
50
+ log 'Disconnected' if @connected
51
+ @connected = nil
52
+ end
53
+
54
+ def connect_to_server
55
+ if @server_url =~ /^([^\:]+)\:(\d+)$/
56
+ host = Regexp.last_match(1)
57
+ port = Regexp.last_match(2)
58
+ Polyphony::Net.tcp_connect(host, port)
59
+ else
60
+ UNIXSocket.new(@server_url)
61
+ end
62
+ end
63
+
64
+ UPGRADE_REQUEST = <<~HTTP
65
+ GET / HTTP/1.1
66
+ Host: localhost
67
+ Connection: upgrade
68
+ Upgrade: df
69
+ DF-Token: %s
70
+ DF-Mount: %s
71
+
72
+ HTTP
73
+
74
+ def df_upgrade
75
+ @socket << format(UPGRADE_REQUEST, @token, mount_point)
76
+ while (line = @socket.gets)
77
+ break if line.chomp.empty?
78
+ end
79
+ # log 'Connection upgraded'
80
+ end
81
+
82
+ def mount_point
83
+ if @route[:host]
84
+ "host=#{@route[:host]}"
85
+ elsif @route[:path]
86
+ "path=#{@route[:path]}"
87
+ else
88
+ nil
89
+ end
90
+ end
91
+
92
+ def log(msg)
93
+ puts "#{Time.now} (#{@name}) #{msg}"
94
+ end
95
+
96
+ def process_incoming_requests
97
+ @socket.feed_loop(@msgpack_reader, :feed_each) do |msg|
98
+ recv_df_message(msg)
99
+ return if @shutdown && @requests.empty?
100
+ end
101
+ rescue IOError, SystemCallError, TimeoutError
102
+ # ignore
103
+ end
104
+
105
+ def keep_alive
106
+ return unless @connected
107
+
108
+ now = Time.now
109
+ if now - @last_send >= Protocol::SEND_TIMEOUT
110
+ send_df_message(Protocol.ping)
111
+ end
112
+ # if now - @last_recv >= Protocol::RECV_TIMEOUT
113
+ # raise TimeoutError
114
+ # end
115
+ rescue IOError, SystemCallError => e
116
+ # transmit exception to fiber running the agent
117
+ @fiber.raise(e)
118
+ end
119
+
120
+ def recv_df_message(msg)
121
+ @last_recv = Time.now
122
+ case msg['kind']
123
+ when Protocol::SHUTDOWN
124
+ recv_shutdown
125
+ when Protocol::HTTP_REQUEST
126
+ recv_http_request(msg)
127
+ when Protocol::HTTP_REQUEST_BODY
128
+ recv_http_request_body(msg)
129
+ when Protocol::WS_REQUEST
130
+ recv_ws_request(msg)
131
+ when Protocol::CONN_DATA, Protocol::CONN_CLOSE,
132
+ Protocol::WS_DATA, Protocol::WS_CLOSE
133
+ fiber = @requests[msg['id']]
134
+ fiber << msg if fiber
135
+ end
136
+ end
137
+
138
+ def send_df_message(msg)
139
+ # we mark long-running requests by applying simple heuristics to sent DF
140
+ # messages. This is so we can correctly stop long-running requests
141
+ # upon graceful shutdown
142
+ if is_long_running_request_response?(msg)
143
+ id = msg[:id]
144
+ @long_running_requests[id] = @requests[id]
145
+ end
146
+ @last_send = Time.now
147
+ @socket << msg.to_msgpack
148
+ end
149
+
150
+ def is_long_running_request_response?(msg)
151
+ case msg[:kind]
152
+ when Protocol::HTTP_UPGRADE
153
+ true
154
+ when Protocol::HTTP_RESPONSE
155
+ msg[:body] && !msg[:complete]
156
+ end
157
+ end
158
+
159
+ def recv_shutdown
160
+ # puts "Received shutdown message (#{@requests.size} pending requests)"
161
+ # puts " (Long running requests: #{@long_running_requests.size})"
162
+ @shutdown = true
163
+ @long_running_requests.values.each { |f| f.terminate(true) }
164
+ end
165
+
166
+ def recv_http_request(msg)
167
+ req = prepare_http_request(msg)
168
+ id = msg['id']
169
+ @requests[id] = spin do
170
+ http_request(req)
171
+ rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE
172
+ # ignore
173
+ rescue Polyphony::Terminate
174
+ req.respond(nil, { ':status' => Qeweney::Status::SERVICE_UNAVAILABLE }) if Fiber.current.graceful_shutdown?
175
+ ensure
176
+ @requests.delete(id)
177
+ @long_running_requests.delete(id)
178
+ @fiber.terminate if @shutdown && @requests.empty?
179
+ end
180
+ end
181
+
182
+ def prepare_http_request(msg)
183
+ req = Qeweney::Request.new(msg['headers'], RequestAdapter.new(self, msg))
184
+ req.buffer_body_chunk(msg['body']) if msg['body']
185
+ req.complete! if msg['complete']
186
+ req
187
+ end
188
+
189
+ def recv_http_request_body(msg)
190
+ fiber = @requests[msg['id']]
191
+ return unless fiber
192
+
193
+ fiber << msg['body']
194
+ end
195
+
196
+ def get_http_request_body(id, limit)
197
+ send_df_message(Protocol.http_get_request_body(id, limit))
198
+ receive
199
+ end
200
+
201
+ def recv_ws_request(msg)
202
+ req = Qeweney::Request.new(msg['headers'], RequestAdapter.new(self, msg))
203
+ id = msg['id']
204
+ @requests[id] = @long_running_requests[id] = spin do
205
+ ws_request(req)
206
+ rescue IOError, Errno::ECONNREFUSED, Errno::EPIPE
207
+ # ignore
208
+ ensure
209
+ @requests.delete(id)
210
+ @long_running_requests.delete(id)
211
+ @fiber.terminate if @shutdown && @requests.empty?
212
+ end
213
+ end
214
+
215
+ # default handler for HTTP request
216
+ def http_request(req)
217
+ req.respond(nil, { ':status': Qeweney::Status::SERVICE_UNAVAILABLE })
218
+ end
219
+
220
+ # default handler for WS request
221
+ def ws_request(req)
222
+ req.respond(nil, { ':status': Qeweney::Status::SERVICE_UNAVAILABLE })
223
+ end
224
+ end
225
+ end