Skip to content

Commit

Permalink
Add an 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.

Fixes #137

Reviewed-by: Alexander Turenko <alexander.turenko@tarantool.org>
  • Loading branch information
ligurio committed Jul 9, 2022
1 parent cb3b99a commit 27a0dda
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
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
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
12 changes: 9 additions & 3 deletions http/server.lua
Expand Up @@ -758,9 +758,9 @@ 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" }
}
local chunk = s:read({
delimiter = { "\n\n", "\r\n\r\n" },
}, self.idle_timeout)

if chunk == '' then
is_eof = true
Expand Down Expand Up @@ -1306,6 +1306,10 @@ local exports = {
if type(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 +1324,7 @@ local exports = {
log_errors = true,
display_errors = false,
disable_keepalive = {},
idle_timeout = 0, -- no timeout, option is disabled
}

local self = {
Expand Down Expand Up @@ -1355,6 +1360,7 @@ local exports = {
},

disable_keepalive = tomap(disable_keepalive),
idle_timeout = options.idle_timeout,

internal = {
preprocess_client_handler = function() end,
Expand Down
78 changes: 78 additions & 0 deletions test/integration/http_server_options_test.lua
Expand Up @@ -87,3 +87,81 @@ 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_is_set', function()
g.idle_timeout = 0.5
g.httpd = helpers.cfgserv({
idle_timeout = g.idle_timeout,
})
g.httpd:start()
end)

g.test_idle_timeout_is_set = function(g)
local conn_is_opened = false
local conn_is_closed = false
local httpd = g.httpd
g.httpd.internal.preprocess_client_handler = function() conn_is_opened = true end
g.httpd.internal.postprocess_client_handler = function() conn_is_closed = true end

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

-- Set HTTP keepalive headers: Connection:Keep-Alive and
-- Keep-Alive:timeout=<keepalive_idle>. Otherwise HTTP client will send
-- "Connection:close".
local opts = {
keepalive_idle = 3600,
keepalive_interval = 3600,
}
local r = http_client.new():request('GET', helpers.base_uri .. '/test', nil, opts)
t.assert_equals(r.status, 200)
t.assert_equals(r.headers.connection, 'keep-alive')
t.assert_equals(conn_is_opened, true)

helpers.retrying({
timeout = g.idle_timeout * 2,
}, function()
assert(conn_is_closed, 'connection is closed')
end)
t.assert_equals(conn_is_closed, true) -- Connection is closed.
end

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

g.test_idle_timeout_is_unset = function(g)
local conn_is_opened = false
local conn_is_closed = false
g.httpd.internal.preprocess_client_handler = function() conn_is_opened = true end
g.httpd.internal.postprocess_client_handler = function() conn_is_closed = true end

-- Set HTTP keepalive headers: Connection:Keep-Alive and
-- Keep-Alive:timeout=<keepalive_idle>. Otherwise HTTP client will send
-- "Connection:close".
local opts = {
keepalive_idle = 3600,
keepalive_interval = 3600,
}
local r = http_client.new():request('GET', helpers.base_uri .. '/test', nil, opts)

-- The server should not close the connection during the keepalive_idle
-- seconds. We could check that server will close connection after
-- keepalive_idle seconds, but HTTP server under test does not support
-- "Keep-Alive: timeout=NUM" header.

t.assert_equals(r.status, 200)
t.assert_equals(r.headers.connection, 'keep-alive')
t.assert_equals(conn_is_opened, true)
t.assert_equals(conn_is_closed, false) -- Connection is alive.
end

0 comments on commit 27a0dda

Please sign in to comment.