tipi 0.38 → 0.42
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +5 -1
- data/.gitignore +5 -0
- data/CHANGELOG.md +34 -0
- data/Gemfile +5 -1
- data/Gemfile.lock +58 -16
- data/Rakefile +7 -3
- data/TODO.md +77 -1
- data/benchmarks/bm_http1_parser.rb +61 -0
- data/bin/benchmark +37 -0
- data/bin/h1pd +6 -0
- data/bin/tipi +3 -21
- data/df/sample_agent.rb +1 -1
- data/df/server.rb +16 -47
- data/df/server_utils.rb +178 -0
- data/examples/full_service.rb +13 -0
- data/examples/http1_parser.rb +55 -0
- data/examples/http_server.rb +15 -3
- data/examples/http_server_forked.rb +5 -1
- data/examples/http_server_routes.rb +29 -0
- data/examples/http_server_static.rb +26 -0
- data/examples/http_server_throttled.rb +3 -2
- data/examples/https_server.rb +6 -4
- data/examples/https_wss_server.rb +2 -1
- data/examples/rack_server.rb +5 -0
- data/examples/rack_server_https.rb +1 -1
- data/examples/rack_server_https_forked.rb +4 -3
- data/examples/routing_server.rb +5 -4
- data/examples/servername_cb.rb +37 -0
- data/examples/websocket_demo.rb +2 -8
- data/examples/ws_page.html +2 -2
- data/ext/tipi/extconf.rb +13 -0
- data/ext/tipi/http1_parser.c +823 -0
- data/ext/tipi/http1_parser.h +18 -0
- data/ext/tipi/tipi_ext.c +5 -0
- data/lib/tipi.rb +89 -1
- data/lib/tipi/acme.rb +308 -0
- data/lib/tipi/cli.rb +30 -0
- data/lib/tipi/digital_fabric/agent.rb +22 -17
- data/lib/tipi/digital_fabric/agent_proxy.rb +95 -40
- data/lib/tipi/digital_fabric/executive.rb +6 -2
- data/lib/tipi/digital_fabric/protocol.rb +87 -15
- data/lib/tipi/digital_fabric/request_adapter.rb +6 -10
- data/lib/tipi/digital_fabric/service.rb +77 -51
- data/lib/tipi/http1_adapter.rb +116 -117
- data/lib/tipi/http2_adapter.rb +56 -10
- data/lib/tipi/http2_stream.rb +106 -53
- data/lib/tipi/rack_adapter.rb +2 -53
- data/lib/tipi/response_extensions.rb +17 -0
- data/lib/tipi/version.rb +1 -1
- data/security/http1.rb +12 -0
- data/test/helper.rb +60 -11
- data/test/test_http1_parser.rb +586 -0
- data/test/test_http_server.rb +0 -27
- data/test/test_request.rb +1 -28
- data/tipi.gemspec +11 -5
- metadata +96 -22
- data/e +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 82155f4b86223d3fb32dc8c314abfeb75fda951747e63832ed5c15bc3b1f43cc
|
4
|
+
data.tar.gz: 721b98fb8d330d1bc38f8f236f82ddd909235d31f5875c1b4f89895e4e3926da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 10389779975234d90c968626b4f9d6b168491f9a32ecf1be71683c37cf11f58f1bac52a36478691e72328236a16d309a8462104bda556772e25cebe52b2b0d53
|
7
|
+
data.tar.gz: 99db2c15278cdfc286ffad05f266f5d62eec6a3bb1363f8dfa4c302549a308a691496ee60ec085ede59e3b594533e8e196bbca144878e5a6dccf2ecfc962151c
|
data/.github/workflows/test.yml
CHANGED
@@ -22,6 +22,10 @@ jobs:
|
|
22
22
|
- name: Install dependencies
|
23
23
|
run: |
|
24
24
|
gem install bundler
|
25
|
-
bundle install
|
25
|
+
POLYPHONY_USE_LIBEV=1 bundle install
|
26
|
+
- name: Show Linux kernel version
|
27
|
+
run: uname -r
|
28
|
+
- name: Compile C-extension
|
29
|
+
run: bundle exec rake compile
|
26
30
|
- name: Run tests
|
27
31
|
run: bundle exec rake test
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,37 @@
|
|
1
|
+
## 0.42 2021-08-16
|
2
|
+
|
3
|
+
- HTTP/1 parser: disable UTF-8 parsing for all but header values
|
4
|
+
- Add support for parsing HTTP/1 from callable source
|
5
|
+
- Introduce full_service API for automatic HTTPS
|
6
|
+
- Introduce automatic SSL certificate provisioning
|
7
|
+
- Improve handling of exceptions
|
8
|
+
- Various fixes to DF service and agent pxoy
|
9
|
+
- Fix upgrading to HTTP2 with a request body
|
10
|
+
- Switch to new HTTP/1 parser
|
11
|
+
|
12
|
+
## 0.41 2021-07-26
|
13
|
+
|
14
|
+
- Fix Rack adapter (#11)
|
15
|
+
- Introduce experimental HTTP/1 parser
|
16
|
+
- More work on DF server
|
17
|
+
- Allow setting chunk size in `#respond_from_io`
|
18
|
+
|
19
|
+
## 0.40 2021-06-24
|
20
|
+
|
21
|
+
- Implement serving static files using splice_chunks (nice performance boost for
|
22
|
+
files bigger than 1M)
|
23
|
+
- Call shutdown before closing socket
|
24
|
+
- Fix examples (thanks @timhatch!)
|
25
|
+
|
26
|
+
## 0.39 2021-06-20
|
27
|
+
|
28
|
+
- More work on DF server
|
29
|
+
- Fix HTTP2StreamHandler#send_headers
|
30
|
+
- Various fixes to HTTP/2 adapter
|
31
|
+
- Fix host detection for HTTP/2 connections
|
32
|
+
- Fix HTTP/1 adapter #respond with nil body
|
33
|
+
- Fix HTTP1Adapter#send_headers
|
34
|
+
|
1
35
|
## 0.38 2021-03-09
|
2
36
|
|
3
37
|
- Don't use chunked transfer encoding for non-streaming responses
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,39 +1,77 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ../polyphony
|
3
|
+
specs:
|
4
|
+
polyphony (0.69)
|
5
|
+
|
6
|
+
PATH
|
7
|
+
remote: ../qeweney
|
8
|
+
specs:
|
9
|
+
qeweney (0.14)
|
10
|
+
escape_utils (~> 1.2.1)
|
11
|
+
|
1
12
|
PATH
|
2
13
|
remote: .
|
3
14
|
specs:
|
4
|
-
tipi (0.
|
5
|
-
|
6
|
-
|
15
|
+
tipi (0.42)
|
16
|
+
acme-client (~> 2.0.8)
|
17
|
+
extralite (~> 1.2)
|
18
|
+
http-2 (~> 0.11)
|
19
|
+
localhost (~> 1.1.4)
|
7
20
|
msgpack (~> 1.4.2)
|
8
|
-
polyphony (~> 0.
|
9
|
-
qeweney (~> 0.
|
21
|
+
polyphony (~> 0.69)
|
22
|
+
qeweney (~> 0.14)
|
10
23
|
rack (>= 2.0.8, < 2.3.0)
|
11
24
|
websocket (~> 1.2.8)
|
12
25
|
|
13
26
|
GEM
|
14
27
|
remote: https://rubygems.org/
|
15
28
|
specs:
|
29
|
+
acme-client (2.0.8)
|
30
|
+
faraday (>= 0.17, < 2.0.0)
|
16
31
|
ansi (1.5.0)
|
17
32
|
builder (3.2.4)
|
18
|
-
|
33
|
+
cuba (3.9.3)
|
34
|
+
rack (>= 1.6.0)
|
35
|
+
docile (1.4.0)
|
19
36
|
escape_utils (1.2.1)
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
37
|
+
extralite (1.2)
|
38
|
+
faraday (1.7.0)
|
39
|
+
faraday-em_http (~> 1.0)
|
40
|
+
faraday-em_synchrony (~> 1.0)
|
41
|
+
faraday-excon (~> 1.1)
|
42
|
+
faraday-httpclient (~> 1.0.1)
|
43
|
+
faraday-net_http (~> 1.0)
|
44
|
+
faraday-net_http_persistent (~> 1.1)
|
45
|
+
faraday-patron (~> 1.0)
|
46
|
+
faraday-rack (~> 1.0)
|
47
|
+
multipart-post (>= 1.2, < 3)
|
48
|
+
ruby2_keywords (>= 0.0.4)
|
49
|
+
faraday-em_http (1.0.0)
|
50
|
+
faraday-em_synchrony (1.0.0)
|
51
|
+
faraday-excon (1.1.0)
|
52
|
+
faraday-httpclient (1.0.1)
|
53
|
+
faraday-net_http (1.0.1)
|
54
|
+
faraday-net_http_persistent (1.2.0)
|
55
|
+
faraday-patron (1.0.0)
|
56
|
+
faraday-rack (1.0.0)
|
57
|
+
http-2 (0.11.0)
|
58
|
+
http_parser.rb (0.7.0)
|
59
|
+
json (2.5.1)
|
60
|
+
localhost (1.1.8)
|
24
61
|
minitest (5.11.3)
|
25
|
-
minitest-reporters (1.4.
|
62
|
+
minitest-reporters (1.4.3)
|
26
63
|
ansi
|
27
64
|
builder
|
28
65
|
minitest (>= 5.0)
|
29
66
|
ruby-progressbar
|
30
67
|
msgpack (1.4.2)
|
31
|
-
|
32
|
-
qeweney (0.7.5)
|
33
|
-
escape_utils (~> 1.2.1)
|
68
|
+
multipart-post (2.1.1)
|
34
69
|
rack (2.2.3)
|
35
70
|
rake (12.3.3)
|
36
|
-
|
71
|
+
rake-compiler (1.1.1)
|
72
|
+
rake
|
73
|
+
ruby-progressbar (1.11.0)
|
74
|
+
ruby2_keywords (0.0.5)
|
37
75
|
simplecov (0.17.1)
|
38
76
|
docile (~> 1.1)
|
39
77
|
json (>= 1.8, < 3)
|
@@ -45,10 +83,14 @@ PLATFORMS
|
|
45
83
|
ruby
|
46
84
|
|
47
85
|
DEPENDENCIES
|
48
|
-
|
86
|
+
cuba (~> 3.9.3)
|
87
|
+
http_parser.rb (= 0.7.0)
|
49
88
|
minitest (~> 5.11.3)
|
50
89
|
minitest-reporters (~> 1.4.2)
|
90
|
+
polyphony!
|
91
|
+
qeweney!
|
51
92
|
rake (~> 12.3.3)
|
93
|
+
rake-compiler (= 1.1.1)
|
52
94
|
simplecov (~> 0.17.1)
|
53
95
|
tipi!
|
54
96
|
|
data/Rakefile
CHANGED
@@ -3,10 +3,14 @@
|
|
3
3
|
require "bundler/gem_tasks"
|
4
4
|
require "rake/clean"
|
5
5
|
|
6
|
-
|
6
|
+
require "rake/extensiontask"
|
7
|
+
Rake::ExtensionTask.new("tipi_ext") do |ext|
|
8
|
+
ext.ext_dir = "ext/tipi"
|
9
|
+
end
|
10
|
+
|
11
|
+
task :recompile => [:clean, :compile]
|
12
|
+
task :default => [:compile, :test]
|
7
13
|
|
8
|
-
task :default => [:test]
|
9
14
|
task :test do
|
10
15
|
exec 'ruby test/run.rb'
|
11
16
|
end
|
12
|
-
|
data/TODO.md
CHANGED
@@ -1,4 +1,80 @@
|
|
1
|
-
|
1
|
+
## Add an API for reading a request body chunk into an IO (pipe)
|
2
|
+
|
3
|
+
```ruby
|
4
|
+
# currently
|
5
|
+
chunk = req.next_chunk
|
6
|
+
# or
|
7
|
+
req.each_chunk { |c| do_something(c) }
|
8
|
+
|
9
|
+
# what we'd like to do
|
10
|
+
r, w = IO.pipe
|
11
|
+
len = req.splice_chunk(w)
|
12
|
+
sock << "Here comes a chunk of #{len} bytes\n"
|
13
|
+
sock.splice(r, len)
|
14
|
+
|
15
|
+
# or:
|
16
|
+
r, w = IO.pipe
|
17
|
+
req.splice_each_chunk(w) do |len|
|
18
|
+
sock << "Here comes a chunk of #{len} bytes\n"
|
19
|
+
sock.splice(r, len)
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
# HTTP/1.1 parser
|
24
|
+
|
25
|
+
- httparser.rb is not actively updated
|
26
|
+
- the httparser.rb C parser code comes originally from https://github.com/nodejs/llhttp
|
27
|
+
- there's a Ruby gem https://github.com/metabahn/llhttp, but its API is too low-level
|
28
|
+
(lots of callbacks, headers need to be retained across callbacks)
|
29
|
+
- the basic idea is to import the C-code, then build a parser object with the following
|
30
|
+
callbacks:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
on_headers_complete(headers)
|
34
|
+
on_body_chunk(chunk)
|
35
|
+
on_message_complete
|
36
|
+
```
|
37
|
+
|
38
|
+
- The llhttp gem's C-code is here: https://github.com/metabahn/llhttp/tree/main/mri
|
39
|
+
|
40
|
+
- Actually, if you do a C extension, instead of a callback-based API, we can
|
41
|
+
design a blocking API:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
parser = Tipi::HTTP1::Parser.new
|
45
|
+
parser.each_request(socket) do |headers|
|
46
|
+
request = Request.new(normalize_headers(headers))
|
47
|
+
handle_request(request)
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
# What about HTTP/2?
|
52
|
+
|
53
|
+
It would be a nice exercise in converting a callback-based API to a blocking
|
54
|
+
one:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
parser = Tipi::HTTP2::Parser.new(socket)
|
58
|
+
parser.each_stream(socket) do |stream|
|
59
|
+
spin { handle_stream(stream) }
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
|
64
|
+
|
65
|
+
# DF
|
66
|
+
|
67
|
+
- Add attack protection for IP-address HTTP host:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
IPV4_REGEXP = /^\d+\.\d+\.\d+\.\d+$/.freeze
|
71
|
+
|
72
|
+
def is_attack_request?(req)
|
73
|
+
return true if req.host =~ IPV4_REGEXP && req.query[:q] != 'ping'
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
- Add attack route to Qeweney routing API
|
2
78
|
|
3
79
|
|
4
80
|
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
|
5
|
+
HTTP_REQUEST = "GET /foo HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n"
|
6
|
+
|
7
|
+
def benchmark_other_http1_parser(iterations)
|
8
|
+
STDOUT << "http_parser.rb: "
|
9
|
+
require 'http_parser.rb'
|
10
|
+
|
11
|
+
i, o = IO.pipe
|
12
|
+
parser = Http::Parser.new
|
13
|
+
done = false
|
14
|
+
headers = nil
|
15
|
+
parser.on_headers_complete = proc do |h|
|
16
|
+
headers = h
|
17
|
+
headers[':method'] = parser.http_method
|
18
|
+
headers[':path'] = parser.request_url
|
19
|
+
headers[':protocol'] = parser.http_version
|
20
|
+
end
|
21
|
+
parser.on_message_complete = proc { done = true }
|
22
|
+
|
23
|
+
t0 = Time.now
|
24
|
+
iterations.times do
|
25
|
+
o << HTTP_REQUEST
|
26
|
+
done = false
|
27
|
+
while !done
|
28
|
+
msg = i.readpartial(4096)
|
29
|
+
parser << msg
|
30
|
+
end
|
31
|
+
end
|
32
|
+
t1 = Time.now
|
33
|
+
puts "#{iterations / (t1 - t0)} ips"
|
34
|
+
end
|
35
|
+
|
36
|
+
def benchmark_tipi_http1_parser(iterations)
|
37
|
+
STDOUT << "tipi parser: "
|
38
|
+
require_relative '../lib/tipi_ext'
|
39
|
+
i, o = IO.pipe
|
40
|
+
reader = proc { |len| i.readpartial(len) }
|
41
|
+
parser = Tipi::HTTP1Parser.new(reader)
|
42
|
+
|
43
|
+
t0 = Time.now
|
44
|
+
iterations.times do
|
45
|
+
o << HTTP_REQUEST
|
46
|
+
headers = parser.parse_headers
|
47
|
+
end
|
48
|
+
t1 = Time.now
|
49
|
+
puts "#{iterations / (t1 - t0)} ips"
|
50
|
+
end
|
51
|
+
|
52
|
+
def fork_benchmark(method, iterations)
|
53
|
+
pid = fork { send(method, iterations) }
|
54
|
+
Process.wait(pid)
|
55
|
+
end
|
56
|
+
|
57
|
+
x = 500000
|
58
|
+
fork_benchmark(:benchmark_other_http1_parser, x)
|
59
|
+
fork_benchmark(:benchmark_tipi_http1_parser, x)
|
60
|
+
|
61
|
+
# benchmark_tipi_http1_parser(x)
|
data/bin/benchmark
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony'
|
5
|
+
|
6
|
+
def parse_latency(latency)
|
7
|
+
m = latency.match(/^([\d\.]+)(us|ms|s)$/)
|
8
|
+
return nil unless m
|
9
|
+
|
10
|
+
value = m[1].to_f
|
11
|
+
case m[2]
|
12
|
+
when 's' then value
|
13
|
+
when 'ms' then value / 1000
|
14
|
+
when 'us' then value / 1000000
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse_wrk_results(results)
|
19
|
+
lines = results.lines
|
20
|
+
latencies = lines[3].strip.split(/\s+/)
|
21
|
+
throughput = lines[6].strip.split(/\s+/)
|
22
|
+
|
23
|
+
{
|
24
|
+
latency_avg: parse_latency(latencies[1]),
|
25
|
+
latency_max: parse_latency(latencies[3]),
|
26
|
+
rate: throughput[1].to_f
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def run_wrk(duration: 10, threads: 2, connections: 10, url: )
|
31
|
+
`wrk -d#{duration} -t#{threads} -c#{connections} #{url}`
|
32
|
+
end
|
33
|
+
|
34
|
+
[8, 64, 256, 512].each do |c|
|
35
|
+
puts "connections: #{c}"
|
36
|
+
p parse_wrk_results(run_wrk(duration: 10, threads: 4, connections: c, url: "http://localhost:10080/"))
|
37
|
+
end
|
data/bin/h1pd
ADDED
data/bin/tipi
CHANGED
@@ -1,26 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'bundler/setup'
|
4
|
-
require '
|
5
|
-
require File.expand_path('../lib/tipi/configuration', __dir__)
|
4
|
+
require 'tipi/cli'
|
6
5
|
|
7
|
-
|
8
|
-
#config[:forked] = 4
|
6
|
+
trap('SIGINT') { exit }
|
9
7
|
|
10
|
-
|
11
|
-
puts
|
12
|
-
|
13
|
-
configuration_manager = spin { Tipi::Configuration.supervise_config }
|
14
|
-
|
15
|
-
configuration_manager << config
|
16
|
-
configuration_manager.await
|
17
|
-
|
18
|
-
__END__
|
19
|
-
|
20
|
-
ooo
|
21
|
-
oo
|
22
|
-
o
|
23
|
-
\|/
|
24
|
-
/ \ Tipi - A better web server for a better world
|
25
|
-
/___\
|
26
|
-
|
8
|
+
Tipi::CLI.start
|
data/df/sample_agent.rb
CHANGED
@@ -13,7 +13,7 @@ class SampleAgent < DigitalFabric::Agent
|
|
13
13
|
HTML_SSE = IO.read(File.join(__dir__, 'sse_page.html'))
|
14
14
|
|
15
15
|
def http_request(req)
|
16
|
-
path = req
|
16
|
+
path = req.headers[':path']
|
17
17
|
case path
|
18
18
|
when '/agent'
|
19
19
|
send_df_message(Protocol.http_response(
|
data/df/server.rb
CHANGED
@@ -1,54 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require 'tipi'
|
5
|
-
require 'tipi/digital_fabric'
|
6
|
-
require 'tipi/digital_fabric/executive'
|
7
|
-
require 'json'
|
8
|
-
require 'fileutils'
|
9
|
-
FileUtils.cd(__dir__)
|
3
|
+
require_relative 'server_utils'
|
10
4
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
class Polyphony::BaseException
|
17
|
-
attr_reader :caller_backtrace
|
18
|
-
end
|
19
|
-
|
20
|
-
puts "pid: #{Process.pid}"
|
21
|
-
|
22
|
-
tcp_listener = spin do
|
23
|
-
opts = {
|
24
|
-
reuse_addr: true,
|
25
|
-
dont_linger: true,
|
26
|
-
}
|
27
|
-
puts 'Listening on localhost:4411'
|
28
|
-
server = Polyphony::Net.tcp_listen('0.0.0.0', 4411, opts)
|
29
|
-
server.accept_loop do |client|
|
30
|
-
spin do
|
31
|
-
service.incr_connection_count
|
32
|
-
Tipi.client_loop(client, opts) { |req| service.http_request(req) }
|
33
|
-
ensure
|
34
|
-
service.decr_connection_count
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
UNIX_SOCKET_PATH = '/tmp/df.sock'
|
40
|
-
|
41
|
-
unix_listener = spin do
|
42
|
-
puts "Listening on #{UNIX_SOCKET_PATH}"
|
43
|
-
FileUtils.rm(UNIX_SOCKET_PATH) if File.exists?(UNIX_SOCKET_PATH)
|
44
|
-
socket = UNIXServer.new(UNIX_SOCKET_PATH)
|
45
|
-
Tipi.accept_loop(socket, {}) { |req| service.http_request(req) }
|
46
|
-
end
|
5
|
+
listeners = [
|
6
|
+
listen_http,
|
7
|
+
listen_https,
|
8
|
+
listen_unix
|
9
|
+
]
|
47
10
|
|
48
11
|
begin
|
49
|
-
|
12
|
+
log('Starting DF server')
|
13
|
+
Fiber.await(*listeners)
|
50
14
|
rescue Interrupt
|
51
|
-
|
52
|
-
service.graceful_shutdown
|
53
|
-
|
15
|
+
log('Got SIGINT, shutting down gracefully')
|
16
|
+
@service.graceful_shutdown
|
17
|
+
rescue SystemExit
|
18
|
+
# ignore
|
19
|
+
rescue Exception => e
|
20
|
+
log("Uncaught exception", error: e, source: e.source_fiber, raising: e.raising_fiber, backtrace: e.backtrace)
|
21
|
+
ensure
|
22
|
+
log('DF server stopped')
|
54
23
|
end
|