tipi 0.33 → 0.37.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/Gemfile.lock +10 -4
- data/LICENSE +1 -1
- data/TODO.md +11 -47
- data/df/agent.rb +63 -0
- data/df/etc_benchmark.rb +15 -0
- data/df/multi_agent_supervisor.rb +87 -0
- data/df/multi_client.rb +84 -0
- data/df/routing_benchmark.rb +60 -0
- data/df/sample_agent.rb +89 -0
- data/df/server.rb +54 -0
- data/df/sse_page.html +29 -0
- data/df/stress.rb +24 -0
- data/df/ws_page.html +38 -0
- data/e +0 -0
- data/examples/http_request_ws_server.rb +35 -0
- data/examples/http_server.rb +6 -6
- data/examples/http_server_form.rb +23 -0
- data/examples/http_unix_socket_server.rb +17 -0
- data/examples/http_ws_server.rb +10 -12
- data/examples/routing_server.rb +34 -0
- data/examples/ws_page.html +1 -2
- data/lib/tipi.rb +5 -1
- data/lib/tipi/digital_fabric.rb +7 -0
- data/lib/tipi/digital_fabric/agent.rb +225 -0
- data/lib/tipi/digital_fabric/agent_proxy.rb +265 -0
- data/lib/tipi/digital_fabric/executive.rb +100 -0
- data/lib/tipi/digital_fabric/executive/index.html +69 -0
- data/lib/tipi/digital_fabric/protocol.rb +90 -0
- data/lib/tipi/digital_fabric/request_adapter.rb +48 -0
- data/lib/tipi/digital_fabric/service.rb +230 -0
- data/lib/tipi/http1_adapter.rb +50 -14
- data/lib/tipi/http2_adapter.rb +4 -2
- data/lib/tipi/http2_stream.rb +20 -8
- data/lib/tipi/rack_adapter.rb +1 -1
- data/lib/tipi/version.rb +1 -1
- data/lib/tipi/websocket.rb +33 -29
- data/test/helper.rb +1 -2
- data/test/test_http_server.rb +10 -12
- data/test/test_request.rb +108 -0
- data/tipi.gemspec +7 -3
- metadata +57 -6
- data/lib/tipi/request.rb +0 -118
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tipi/digital_fabric'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module DigitalFabric
|
7
|
+
# agent for managing DF service
|
8
|
+
class Executive
|
9
|
+
INDEX_HTML = IO.read(File.join(__dir__, 'executive/index.html'))
|
10
|
+
|
11
|
+
attr_reader :last_service_stats
|
12
|
+
|
13
|
+
def initialize(service, route = { path: '/executive' })
|
14
|
+
@service = service
|
15
|
+
route[:executive] = true
|
16
|
+
@service.mount(route, self)
|
17
|
+
@current_request_count = 0
|
18
|
+
@updater = spin_loop(interval: 10) { update_service_stats }
|
19
|
+
update_service_stats
|
20
|
+
end
|
21
|
+
|
22
|
+
def current_request_count
|
23
|
+
@current_request_count
|
24
|
+
end
|
25
|
+
|
26
|
+
def http_request(req)
|
27
|
+
@current_request_count += 1
|
28
|
+
case req.path
|
29
|
+
when '/'
|
30
|
+
req.respond(INDEX_HTML, 'Content-Type' => 'text/html')
|
31
|
+
when '/stats'
|
32
|
+
message = last_service_stats
|
33
|
+
req.respond(message.to_json, { 'Content-Type' => 'text.json' })
|
34
|
+
when '/stream/stats'
|
35
|
+
stream_stats(req)
|
36
|
+
else
|
37
|
+
req.respond('Invalid path', { ':status' => Qeweney::Status::NOT_FOUND })
|
38
|
+
end
|
39
|
+
ensure
|
40
|
+
@current_request_count -= 1
|
41
|
+
end
|
42
|
+
|
43
|
+
def stream_stats(req)
|
44
|
+
req.send_headers({ 'Content-Type' => 'text/event-stream' })
|
45
|
+
|
46
|
+
@service.timer.every(10) do
|
47
|
+
message = last_service_stats
|
48
|
+
req.send_chunk(format_sse_event(message.to_json))
|
49
|
+
end
|
50
|
+
rescue IOError, SystemCallError
|
51
|
+
# ignore
|
52
|
+
ensure
|
53
|
+
req.send_chunk("retry: 0\n\n", true) rescue nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def format_sse_event(data)
|
57
|
+
"data: #{data}\n\n"
|
58
|
+
end
|
59
|
+
|
60
|
+
def update_service_stats
|
61
|
+
@last_service_stats = {
|
62
|
+
service: @service.stats,
|
63
|
+
machine: machine_stats
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
TOP_CPU_REGEXP = /%Cpu(.+)/.freeze
|
68
|
+
TOP_CPU_IDLE_REGEXP = /([\d\.]+) id/.freeze
|
69
|
+
TOP_MEM_REGEXP = /MiB Mem(.+)/.freeze
|
70
|
+
TOP_MEM_FREE_REGEXP = /([\d\.]+) free/.freeze
|
71
|
+
LOADAVG_REGEXP = /^([\d\.]+)/.freeze
|
72
|
+
|
73
|
+
def machine_stats
|
74
|
+
top = `top -bn1 | head -n4`
|
75
|
+
unless top =~ TOP_CPU_REGEXP && Regexp.last_match(1) =~ TOP_CPU_IDLE_REGEXP
|
76
|
+
p top =~ TOP_CPU_REGEXP
|
77
|
+
p Regexp.last_match(1)
|
78
|
+
p Regexp.last_match(1) =~ TOP_CPU_IDLE_REGEXP
|
79
|
+
raise 'Invalid output from top (cpu)'
|
80
|
+
end
|
81
|
+
cpu_utilization = 100 - Regexp.last_match(1).to_i
|
82
|
+
|
83
|
+
unless top =~ TOP_MEM_REGEXP && Regexp.last_match(1) =~ TOP_MEM_FREE_REGEXP
|
84
|
+
raise 'Invalid output from top (mem)'
|
85
|
+
end
|
86
|
+
|
87
|
+
mem_free = Regexp.last_match(1).to_f
|
88
|
+
|
89
|
+
stats = `cat /proc/loadavg`
|
90
|
+
raise 'Invalid output from /proc/loadavg' unless stats =~ LOADAVG_REGEXP
|
91
|
+
load_avg = Regexp.last_match(1).to_f
|
92
|
+
|
93
|
+
{
|
94
|
+
mem_free: mem_free,
|
95
|
+
cpu_utilization: cpu_utilization,
|
96
|
+
load_avg: load_avg
|
97
|
+
}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<title>Digital Fabric Executive</title>
|
5
|
+
<style>
|
6
|
+
.hidden { display: none }
|
7
|
+
</style>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
<h1>Digital Fabric Executive</h1>
|
11
|
+
<script>
|
12
|
+
function updateStats(update) {
|
13
|
+
for (let k in update.service) {
|
14
|
+
let v = update.service[k];
|
15
|
+
let e = document.querySelector('#' + k);
|
16
|
+
if (e) e.innerText = v;
|
17
|
+
}
|
18
|
+
for (let k in update.machine) {
|
19
|
+
let v = update.machine[k];
|
20
|
+
let e = document.querySelector('#' + k);
|
21
|
+
if (e) e.innerText = v;
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
function connect() {
|
26
|
+
console.log("connecting...");
|
27
|
+
window.eventSource = new EventSource("/stream/stats");
|
28
|
+
|
29
|
+
window.eventSource.onopen = function(e) {
|
30
|
+
console.log("connected");
|
31
|
+
document.querySelector('#status').innerText = 'connected';
|
32
|
+
document.querySelector('#stats').className = '';
|
33
|
+
return false;
|
34
|
+
}
|
35
|
+
|
36
|
+
window.eventSource.onmessage = function(e) {
|
37
|
+
console.log("message", e.data);
|
38
|
+
updateStats(JSON.parse(e.data));
|
39
|
+
}
|
40
|
+
|
41
|
+
window.eventSource.onerror = function(e) {
|
42
|
+
console.log("error", e);
|
43
|
+
document.querySelector('#status').innerText = 'disconnected';
|
44
|
+
document.querySelector('#stats').className = 'hidden';
|
45
|
+
window.eventSource.close();
|
46
|
+
window.eventSource = null;
|
47
|
+
setTimeout(connect, 5000);
|
48
|
+
}
|
49
|
+
};
|
50
|
+
|
51
|
+
window.onload = connect;
|
52
|
+
</script>
|
53
|
+
<h2 id="status"></h2>
|
54
|
+
<div id="stats" class="hidden">
|
55
|
+
<h2>Service</h2>
|
56
|
+
<p>Request rate: <span id="http_request_rate"></span></p>
|
57
|
+
<p>Error rate: <span id="error_rate"></span></p>
|
58
|
+
<p>Average Latency: <span id="average_latency"></span>s</p>
|
59
|
+
<p>Connected agents: <span id="agent_count"></span></p>
|
60
|
+
<p>Connected clients: <span id="connection_count"></span></p>
|
61
|
+
<p>Concurrent requests: <span id="concurrent_requests"></span></p>
|
62
|
+
|
63
|
+
<h2>Machine</h2>
|
64
|
+
<p>CPU utilization: <span id="cpu_utilization"></span>%</p>
|
65
|
+
<p>Free memory: <span id="mem_free"></span>MB</p>
|
66
|
+
<p>Load average: <span id="load_avg"></span></p>
|
67
|
+
</div>
|
68
|
+
</body>
|
69
|
+
</html>
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DigitalFabric
|
4
|
+
module Protocol
|
5
|
+
PING = 'ping'
|
6
|
+
SHUTDOWN = 'shutdown'
|
7
|
+
|
8
|
+
HTTP_REQUEST = 'http_request'
|
9
|
+
HTTP_RESPONSE = 'http_response'
|
10
|
+
HTTP_UPGRADE = 'http_upgrade'
|
11
|
+
HTTP_GET_REQUEST_BODY = 'http_get_request_body'
|
12
|
+
HTTP_REQUEST_BODY = 'http_request_body'
|
13
|
+
|
14
|
+
CONN_DATA = 'conn_data'
|
15
|
+
CONN_CLOSE = 'conn_close'
|
16
|
+
|
17
|
+
WS_REQUEST = 'ws_request'
|
18
|
+
WS_RESPONSE = 'ws_response'
|
19
|
+
WS_DATA = 'ws_data'
|
20
|
+
WS_CLOSE = 'ws_close'
|
21
|
+
|
22
|
+
SEND_TIMEOUT = 15
|
23
|
+
RECV_TIMEOUT = SEND_TIMEOUT + 5
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def ping
|
27
|
+
{ kind: PING }
|
28
|
+
end
|
29
|
+
|
30
|
+
def shutdown
|
31
|
+
{ kind: SHUTDOWN }
|
32
|
+
end
|
33
|
+
|
34
|
+
DF_UPGRADE_RESPONSE = <<~HTTP.gsub("\n", "\r\n")
|
35
|
+
HTTP/1.1 101 Switching Protocols
|
36
|
+
Upgrade: df
|
37
|
+
Connection: Upgrade
|
38
|
+
|
39
|
+
HTTP
|
40
|
+
|
41
|
+
def df_upgrade_response
|
42
|
+
DF_UPGRADE_RESPONSE
|
43
|
+
end
|
44
|
+
|
45
|
+
def http_request(id, req)
|
46
|
+
{ kind: HTTP_REQUEST, id: id, headers: req.headers, body: req.next_chunk, complete: req.complete? }
|
47
|
+
end
|
48
|
+
|
49
|
+
def http_response(id, body, headers, complete)
|
50
|
+
{ kind: HTTP_RESPONSE, id: id, body: body, headers: headers, complete: complete }
|
51
|
+
end
|
52
|
+
|
53
|
+
def http_upgrade(id, headers)
|
54
|
+
{ kind: HTTP_UPGRADE, id: id }
|
55
|
+
end
|
56
|
+
|
57
|
+
def http_get_request_body(id, limit = nil)
|
58
|
+
{ kind: HTTP_GET_REQUEST_BODY, id: id, limit: limit }
|
59
|
+
end
|
60
|
+
|
61
|
+
def http_request_body(id, body, complete)
|
62
|
+
{ kind: HTTP_REQUEST_BODY, id: id, body: body, complete: complete }
|
63
|
+
end
|
64
|
+
|
65
|
+
def connection_data(id, data)
|
66
|
+
{ kind: CONN_DATA, id: id, data: data }
|
67
|
+
end
|
68
|
+
|
69
|
+
def connection_close(id)
|
70
|
+
{ kind: CONN_CLOSE, id: id }
|
71
|
+
end
|
72
|
+
|
73
|
+
def ws_request(id, headers)
|
74
|
+
{ kind: WS_REQUEST, id: id, headers: headers }
|
75
|
+
end
|
76
|
+
|
77
|
+
def ws_response(id, headers)
|
78
|
+
{ kind: WS_RESPONSE, id: id, headers: headers }
|
79
|
+
end
|
80
|
+
|
81
|
+
def ws_data(id, data)
|
82
|
+
{ id: id, kind: WS_DATA, data: data }
|
83
|
+
end
|
84
|
+
|
85
|
+
def ws_close(id)
|
86
|
+
{ id: id, kind: WS_CLOSE }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './protocol'
|
4
|
+
|
5
|
+
module DigitalFabric
|
6
|
+
class RequestAdapter
|
7
|
+
def initialize(agent, msg)
|
8
|
+
@agent = agent
|
9
|
+
@id = msg['id']
|
10
|
+
end
|
11
|
+
|
12
|
+
def protocol
|
13
|
+
'df'
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_body_chunk
|
17
|
+
@agent.get_http_request_body(@id, 1)
|
18
|
+
end
|
19
|
+
|
20
|
+
def consume_request
|
21
|
+
@agent.get_http_request_body(@id, nil)
|
22
|
+
end
|
23
|
+
|
24
|
+
def respond(body, headers)
|
25
|
+
@agent.send_df_message(
|
26
|
+
Protocol.http_response(@id, body, headers, true)
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def send_headers(headers, opts = {})
|
31
|
+
@agent.send_df_message(
|
32
|
+
Protocol.http_response(@id, nil, headers, false)
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def send_chunk(body, done: )
|
37
|
+
@agent.send_df_message(
|
38
|
+
Protocol.http_response(@id, body, nil, done)
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def finish
|
43
|
+
@agent.send_df_message(
|
44
|
+
Protocol.http_response(@id, nil, nil, true)
|
45
|
+
)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './protocol'
|
4
|
+
require_relative './agent_proxy'
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
module DigitalFabric
|
8
|
+
class Service
|
9
|
+
attr_reader :token
|
10
|
+
attr_reader :timer
|
11
|
+
|
12
|
+
def initialize(token: )
|
13
|
+
@token = token
|
14
|
+
@agents = {}
|
15
|
+
@routes = {}
|
16
|
+
@waiting_lists = {} # hash mapping routes to arrays of requests waiting for an agent to mount
|
17
|
+
@counters = {
|
18
|
+
connections: 0,
|
19
|
+
http_requests: 0,
|
20
|
+
errors: 0
|
21
|
+
}
|
22
|
+
@connection_count = 0
|
23
|
+
@http_latency_accumulator = 0
|
24
|
+
@http_latency_counter = 0
|
25
|
+
@last_counters = @counters.merge(stamp: Time.now.to_f - 1)
|
26
|
+
@fiber = Fiber.current
|
27
|
+
@timer = Polyphony::Timer.new(resolution: 1)
|
28
|
+
|
29
|
+
stats_updater = spin { @timer.every(10) { update_stats } }
|
30
|
+
@stats = {}
|
31
|
+
|
32
|
+
@current_request_count = 0
|
33
|
+
end
|
34
|
+
|
35
|
+
def update_stats
|
36
|
+
now = Time.now.to_f
|
37
|
+
elapsed = now - @last_counters[:stamp]
|
38
|
+
connections = @counters[:connections] - @last_counters[:connections]
|
39
|
+
http_requests = @counters[:http_requests] - @last_counters[:http_requests]
|
40
|
+
errors = @counters[:errors] - @last_counters[:errors]
|
41
|
+
@last_counters = @counters.merge(stamp: now)
|
42
|
+
|
43
|
+
average_latency = @http_latency_counter > 0 ?
|
44
|
+
@http_latency_accumulator / @http_latency_counter :
|
45
|
+
0
|
46
|
+
@http_latency_accumulator = 0
|
47
|
+
@http_latency_counter = 0
|
48
|
+
|
49
|
+
@stats = {
|
50
|
+
connection_rate: connections / elapsed,
|
51
|
+
http_request_rate: http_requests / elapsed,
|
52
|
+
error_rate: errors / elapsed,
|
53
|
+
average_latency: average_latency,
|
54
|
+
agent_count: @agents.size,
|
55
|
+
connection_count: @connection_count,
|
56
|
+
concurrent_requests: @current_request_count
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def incr_connection_count
|
61
|
+
@connection_count += 1
|
62
|
+
end
|
63
|
+
|
64
|
+
def decr_connection_count
|
65
|
+
@connection_count -= 1
|
66
|
+
end
|
67
|
+
|
68
|
+
attr_reader :stats
|
69
|
+
|
70
|
+
def total_request_count
|
71
|
+
count = 0
|
72
|
+
@agents.keys.each do |agent|
|
73
|
+
if agent.respond_to?(:current_request_count)
|
74
|
+
count += agent.current_request_count
|
75
|
+
end
|
76
|
+
end
|
77
|
+
count
|
78
|
+
end
|
79
|
+
|
80
|
+
def record_latency_measurement(latency)
|
81
|
+
@http_latency_accumulator += latency
|
82
|
+
@http_latency_counter += 1
|
83
|
+
end
|
84
|
+
|
85
|
+
def http_request(req)
|
86
|
+
@current_request_count += 1
|
87
|
+
@counters[:http_requests] += 1
|
88
|
+
@counters[:connections] += 1 if req.headers[':first']
|
89
|
+
|
90
|
+
return upgrade_request(req) if req.upgrade_protocol
|
91
|
+
|
92
|
+
inject_request_headers(req)
|
93
|
+
agent = find_agent(req)
|
94
|
+
unless agent
|
95
|
+
return req.respond('pong') if req.query[:q] == 'ping'
|
96
|
+
|
97
|
+
@counters[:errors] += 1
|
98
|
+
return req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)
|
99
|
+
end
|
100
|
+
|
101
|
+
agent.http_request(req)
|
102
|
+
rescue IOError, SystemCallError
|
103
|
+
@counters[:errors] += 1
|
104
|
+
rescue => e
|
105
|
+
@counters[:errors] += 1
|
106
|
+
p e
|
107
|
+
puts e.backtrace.join("\n")
|
108
|
+
req.respond(e.inspect, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
|
109
|
+
ensure
|
110
|
+
@current_request_count -= 1
|
111
|
+
req.adapter.conn.close if @shutdown
|
112
|
+
end
|
113
|
+
|
114
|
+
def inject_request_headers(req)
|
115
|
+
req.headers['x-request-id'] = SecureRandom.uuid
|
116
|
+
conn = req.adapter.conn
|
117
|
+
req.headers['x-forwarded-for'] = conn.peeraddr(false)[2]
|
118
|
+
req.headers['x-forwarded-proto'] = conn.is_a?(OpenSSL::SSL::SSLSocket) ? 'https' : 'http'
|
119
|
+
end
|
120
|
+
|
121
|
+
def upgrade_request(req)
|
122
|
+
case (protocol = req.upgrade_protocol)
|
123
|
+
when 'df'
|
124
|
+
df_upgrade(req)
|
125
|
+
else
|
126
|
+
agent = find_agent(req)
|
127
|
+
unless agent
|
128
|
+
@counters[:errors] += 1
|
129
|
+
return req.respond(nil, ':status' => Qeweney::Status::SERVICE_UNAVAILABLE)
|
130
|
+
end
|
131
|
+
|
132
|
+
agent.http_upgrade(req, protocol)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def df_upgrade(req)
|
137
|
+
if req.headers['df-token'] != @token
|
138
|
+
return req.respond(nil, ':status' => Qeweney::Status::FORBIDDEN)
|
139
|
+
end
|
140
|
+
|
141
|
+
req.adapter.conn << Protocol.df_upgrade_response
|
142
|
+
AgentProxy.new(self, req)
|
143
|
+
end
|
144
|
+
|
145
|
+
def mount(route, agent)
|
146
|
+
if route[:path]
|
147
|
+
route[:path_regexp] = path_regexp(route[:path])
|
148
|
+
end
|
149
|
+
@executive = agent if route[:executive]
|
150
|
+
@agents[agent] = route
|
151
|
+
@routing_changed = true
|
152
|
+
|
153
|
+
if (waiting = @waiting_lists[route])
|
154
|
+
waiting.each { |f| f.schedule(agent) }
|
155
|
+
@waiting_lists.delete(route)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def unmount(agent)
|
160
|
+
route = @agents[agent]
|
161
|
+
return unless route
|
162
|
+
|
163
|
+
@executive = nil if route[:executive]
|
164
|
+
@agents.delete(agent)
|
165
|
+
@routing_changed = true
|
166
|
+
|
167
|
+
@waiting_lists[route] ||= []
|
168
|
+
end
|
169
|
+
|
170
|
+
INVALID_HOST = 'INVALID_HOST'
|
171
|
+
|
172
|
+
def find_agent(req)
|
173
|
+
compile_agent_routes if @routing_changed
|
174
|
+
|
175
|
+
host = req.headers['host'] || INVALID_HOST
|
176
|
+
path = req.headers[':path']
|
177
|
+
|
178
|
+
route = @route_keys.find do |route|
|
179
|
+
(host == route[:host]) || (path =~ route[:path_regexp])
|
180
|
+
end
|
181
|
+
return @routes[route] if route
|
182
|
+
|
183
|
+
# search for a known route for an agent that recently unmounted
|
184
|
+
route, wait_list = @waiting_lists.find do |route, _|
|
185
|
+
(host == route[:host]) || (path =~ route[:path_regexp])
|
186
|
+
end
|
187
|
+
return wait_for_agent(wait_list) if route
|
188
|
+
|
189
|
+
nil
|
190
|
+
end
|
191
|
+
|
192
|
+
def compile_agent_routes
|
193
|
+
@routing_changed = false
|
194
|
+
|
195
|
+
@routes.clear
|
196
|
+
@agents.keys.reverse.each do |agent|
|
197
|
+
route = @agents[agent]
|
198
|
+
@routes[route] ||= agent
|
199
|
+
end
|
200
|
+
@route_keys = @routes.keys
|
201
|
+
end
|
202
|
+
|
203
|
+
def wait_for_agent(wait_list)
|
204
|
+
wait_list << Fiber.current
|
205
|
+
@timer.move_on_after(10) { suspend }
|
206
|
+
ensure
|
207
|
+
wait_list.delete(self)
|
208
|
+
end
|
209
|
+
|
210
|
+
def path_regexp(path)
|
211
|
+
/^#{path}/
|
212
|
+
end
|
213
|
+
|
214
|
+
def graceful_shutdown
|
215
|
+
@shutdown = true
|
216
|
+
@agents.keys.each do |agent|
|
217
|
+
if agent.respond_to?(:shutdown)
|
218
|
+
agent.shutdown
|
219
|
+
else
|
220
|
+
@agents.delete(agent)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
move_on_after(60) do
|
224
|
+
while !@agents.empty?
|
225
|
+
sleep 0.25
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|