tipi 0.31 → 0.36

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 (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