Skip to content

Commit

Permalink
Add option idle_timeout
Browse files Browse the repository at this point in the history
Option idle_timeout allows to set a maximum amount of time an idle
(keep-alive) connection will remain idle before closing. When the idle
timeout is exceeded, HTTP server closes the keepalive connection.

Implementation of wait_cond() helper taken in expirationd tests, see
[1].

1. https://github.com/tarantool/expirationd/blob/c3b86c8bea7b7f6ddbf5b079836c60f3ee3976c1/test.lua#L20-L57

Fixes #137
  • Loading branch information
ligurio committed Jun 27, 2022
1 parent a57c9bc commit a0961dd
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Added

- Add option to control keepalive connection state (#137).
- Add option to control idle connections (#137).

## [1.2.0] - 2021-11-10

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ httpd = require('http.server').new(host, port[, { options } ])
})
```
* `idle_timeout` - maximum amount of time an idle (keep-alive) connection will
remain idle before closing. When the idle timeout is exceeded, HTTP server
closes the keepalive connection. Default value: 0 seconds (disabled).
## Using routes
Expand Down
8 changes: 7 additions & 1 deletion http/server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,8 @@ local function process_client(self, s, peer)
local is_eof = false
while true do
local chunk = s:read{
delimiter = { "\n\n", "\r\n\r\n" }
delimiter = { "\n\n", "\r\n\r\n" },
timeout = self.idle_timeout,
}

if chunk == '' then
Expand Down Expand Up @@ -1306,6 +1307,10 @@ local exports = {
if type(options.disable_keepalive) ~= 'table' then
error('Option disable_keepalive must be a table.')
end
if options.idle_timeout ~= nil and
type(options.idle_timeout) ~= 'number' then
error('Option idle_timeout must be a number.')
end

local default = {
max_header_size = 4096,
Expand All @@ -1320,6 +1325,7 @@ local exports = {
log_errors = true,
display_errors = false,
disable_keepalive = {},
idle_timeout = 0, -- no timeout, option is disabled
}

local self = {
Expand Down
45 changes: 45 additions & 0 deletions test/helpers.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
local clock = require('clock')
local fio = require('fio')
local fiber = require('fiber')
local http_server = require('http.server')
local http_client = require('http.client')

Expand Down Expand Up @@ -94,4 +96,47 @@ helpers.teardown = function(httpd)
end)
end

-- Strip pcall()'s true or re-raise catched error.
local function wait_cond_finish(status, ...)
if not status then
error((...), 2)
end

return ...
end

-- Block until the condition function returns a positive value
-- (anything except `nil` and `false`) or until the timeout
-- exceeds. Return the result of the last invocation of the
-- condition function (it is `false` or `nil` in case of exiting
-- by the timeout).
--
-- If the condition function raises a Lua error, wait_cond()
-- continues retrying. If the latest attempt raises an error (and
-- we hit a timeout), the error will be re-raised.
function helpers.wait_cond(cond, timeout, delay)
assert(type(cond) == 'function')

local timeout = timeout or 60
local delay = delay or 0.001

local start_time = clock.monotonic()
local res = {
pcall(cond),
}

while not res[1] or not res[2] do
local work_time = clock.monotonic() - start_time
if work_time > timeout then
return wait_cond_finish(res[1], res[2])
end
fiber.sleep(delay)
res = {
pcall(cond),
}
end

return wait_cond_finish(res[1], res[2])
end

return helpers
37 changes: 37 additions & 0 deletions test/integration/http_server_options_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,40 @@ g.test_disable_keepalive_default = function(g)
local httpd = g.httpd
t.assert_equals(table.getn(httpd.options.disable_keepalive), 0)
end

g.before_test('test_idle_timeout_default', function()
g.httpd = helpers.cfgserv()
g.httpd:start()
end)

g.test_idle_timeout_default = function(g)
local httpd = g.httpd
t.assert_equals(httpd.options.idle_timeout, 0)
end

g.before_test('test_idle_timeout', function()
g.idle_timeout = 0.5
g.httpd = helpers.cfgserv({
idle_timeout = g.idle_timeout,
})
g.httpd:start()
end)

g.test_idle_timeout = function(g)
local conn_is_opened = false
local conn_is_closed = false
local httpd = g.httpd
g.httpd.internal.pre_handler = function() conn_is_opened = true end
g.httpd.internal.post_handler = function() conn_is_closed = true end

t.assert_equals(httpd.options.idle_timeout, g.idle_timeout)

local r = http_client.request('GET', helpers.base_uri .. '/test', nil, {})
t.assert_equals(r.status, 200)
t.assert_equals(conn_is_opened, true)

helpers.wait_cond(function()
return conn_is_closed ~= true
end, g.idle_timeout)
t.assert_equals(conn_is_closed, true)
end

0 comments on commit a0961dd

Please sign in to comment.