Skip to content

Commit e763de0

Browse files
committed
api: fix address already in use error
There was a bug when user wants to change server's `listen` address and reload config on the fly - error `address already in use` occures. This patch reworks server's address handling by applying roles' config. Closes #34
1 parent 9e8c17c commit e763de0

File tree

3 files changed

+109
-19
lines changed

3 files changed

+109
-19
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Fixed
1313

14+
- `Address already in use` error on change role's config on the fly (#34).
15+
1416
### Changed
1517

1618
## 0.3.0 - 2024-12-27

roles/metrics-export.lua

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,8 @@ local function apply_http(conf)
329329
local host, port, target
330330
if node.server ~= nil then
331331
target = {
332-
value = node.server,
333-
is_httpd_role = true,
332+
value = 'httpd_' .. node.server,
333+
httpd_name = node.server,
334334
}
335335
elseif node.listen ~= nil then
336336
local err
@@ -339,23 +339,33 @@ local function apply_http(conf)
339339
error("failed to parse URI: " .. err, 2)
340340
end
341341
target = {
342-
value = node.listen,
343-
is_httpd_role = false,
342+
value = 'listen_' .. host .. ':' .. tostring(port),
344343
}
345344
else
346345
target = {
347-
value = httpd_role.DEFAULT_SERVER_NAME,
348-
is_httpd_role = true,
346+
value = 'httpd_' .. httpd_role.DEFAULT_SERVER_NAME,
347+
httpd_name = httpd_role.DEFAULT_SERVER_NAME,
349348
}
350349
end
351350

352351
http_servers = http_servers or {}
352+
353353
-- Since the 'listen' and 'server' names of other servers in the config may be
354-
-- the same, we create a unique string concatenating the key name and information
355-
-- about whether it is an httpd key or not.
356-
enabled[tostring(target.value) .. tostring(target.is_httpd_role)] = true
354+
-- the same, we create a unique string concatenating the host and port of a server.
355+
enabled[target.value] = true
356+
357+
if http_servers[target.value] == nil then
358+
if target.httpd_name == nil then
359+
for k in pairs(http_servers) do
360+
-- We stop running server before creating a new one if port matches and hosts different
361+
-- only on 'listen' target.
362+
if k ~= target.value and string.find(k, ':' .. tostring(port)) ~= nil then
363+
http_servers[k].httpd:stop()
364+
enabled[k] = false
365+
end
366+
end
367+
end
357368

358-
if http_servers[tostring(target.value) .. tostring(target.is_httpd_role)] == nil then
359369
local httpd
360370
if node.listen ~= nil then
361371
httpd = http_server.new(host, port, {
@@ -368,17 +378,17 @@ local function apply_http(conf)
368378
})
369379
httpd:start()
370380
else
371-
httpd = httpd_role.get_server(target.value)
381+
httpd = httpd_role.get_server(target.httpd_name)
372382
end
373383

374-
http_servers[tostring(target.value) .. tostring(target.is_httpd_role)] = {
384+
http_servers[target.value] = {
375385
httpd = httpd,
376386
routes = {},
377-
is_httpd_role = target.is_httpd_role,
387+
httpd_name = target.httpd_name,
378388
}
379389
end
380390

381-
local server = http_servers[tostring(target.value) .. tostring(target.is_httpd_role)]
391+
local server = http_servers[target.value]
382392
local httpd = server.httpd
383393
local old_routes = server.routes
384394

@@ -418,14 +428,16 @@ local function apply_http(conf)
418428
end
419429
end
420430

421-
for target, server in pairs(http_servers) do
431+
for target, server in pairs(http_servers or {}) do
422432
if not enabled[target] then
423-
if server.is_httpd_role then
433+
if server.httpd_name ~= nil then
424434
for path, _ in pairs(server.routes) do
425435
server.httpd:delete(path)
426436
end
427437
else
428-
server.httpd:stop()
438+
if server.httpd.is_run == true then
439+
server.httpd:stop()
440+
end
429441
end
430442
http_servers[target] = nil
431443
end
@@ -434,12 +446,14 @@ end
434446

435447
local function stop_http()
436448
for _, server in pairs(http_servers or {}) do
437-
if server.is_httpd_role then
449+
if server.httpd_name ~= nil then
438450
for path, _ in pairs(server.routes) do
439451
server.httpd:delete(path)
440452
end
441453
else
442-
server.httpd:stop()
454+
if server.httpd.is_run == true then
455+
server.httpd:stop()
456+
end
443457
end
444458
end
445459
http_servers = nil
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
local fio = require('fio')
2+
local yaml = require('yaml')
3+
local socket = require('socket')
4+
local helpers = require('test.helpers')
5+
local server = require('test.helpers.server')
6+
7+
local t = require('luatest')
8+
local g = t.group()
9+
10+
g.before_all(function()
11+
helpers.skip_if_unsupported()
12+
end)
13+
14+
g.before_each(function(cg)
15+
cg.workdir = fio.tempdir()
16+
fio.mktree(cg.workdir)
17+
18+
fio.copytree(".rocks", fio.pathjoin(cg.workdir, ".rocks"))
19+
fio.copytree("roles", fio.pathjoin(cg.workdir, "roles"))
20+
fio.copytree(fio.pathjoin("test", "ssl_data"), fio.pathjoin(cg.workdir, "ssl_data"))
21+
fio.copyfile(fio.pathjoin('test', 'entrypoint', 'config.yaml'), cg.workdir)
22+
end)
23+
24+
g.after_each(function(cg)
25+
cg.server:stop()
26+
fio.rmtree(cg.workdir)
27+
end)
28+
29+
local function is_tcp_connect(host, port)
30+
local tcp = socket.tcp()
31+
tcp:settimeout(0.3)
32+
local ok, _ = tcp:connect(host, port)
33+
tcp:close()
34+
35+
return ok
36+
end
37+
38+
g.test_reload_config = function(cg)
39+
cg.server = server:new({
40+
config_file = fio.pathjoin(cg.workdir, 'config.yaml'),
41+
chdir = cg.workdir,
42+
alias = 'master',
43+
workdir = cg.workdir,
44+
})
45+
46+
cg.server:start({wait_until_ready = true})
47+
48+
local file = fio.open(fio.pathjoin(cg.workdir, 'config.yaml'), {'O_RDONLY'})
49+
t.assert(file ~= nil)
50+
51+
local cfg = file:read()
52+
file:close()
53+
54+
cfg = yaml.decode(cfg)
55+
local export_instances = cfg.groups['group-001'].replicasets['replicaset-001'].
56+
instances.master.roles_cfg['roles.metrics-export'].http
57+
58+
for i, v in pairs(export_instances) do
59+
if v.listen ~= nil and v.listen == '127.0.0.1:8082' then
60+
export_instances[i].listen = '127.0.0.2:8082'
61+
end
62+
end
63+
64+
file = fio.open(fio.pathjoin(cg.workdir, 'config.yaml'), {'O_CREAT', 'O_WRONLY', 'O_TRUNC'}, tonumber('644', 8))
65+
file:write(yaml.encode(cfg))
66+
file:close()
67+
68+
t.assert(is_tcp_connect('127.0.0.1', 8082))
69+
70+
cg.server:eval("require('config'):reload()")
71+
72+
t.assert_not(is_tcp_connect('127.0.0.1', 8082))
73+
t.assert(is_tcp_connect('127.0.0.2', 8082))
74+
end

0 commit comments

Comments
 (0)