Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/install/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Mount the directory containing the `.mmdb` files to `/etc/geoip2` in the Fairvis
docker run -v /path/to/geoip-dbs:/etc/geoip2 ... ghcr.io/fairvisor/fairvisor-edge
```

The runtime will automatically detect the databases and reload them every 24 hours.
The runtime will automatically detect the databases on startup and reload them every 24 hours.

### 3. Verification
If the databases are missing, Nginx will fail to start. Ensure the worker process has read permissions for these files.
Fairvisor uses a Lua-based reader for MMDB files. If the databases are missing or unreadable, a warning will be logged in OpenResty's error log, and GeoIP/ASN lookups will gracefully fall back to headers or return `nil`.
43 changes: 43 additions & 0 deletions spec/unit/decision_api_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ runner:given("^the decision api dependencies are initialized$", function(ctx)
ngx.log_calls[#ngx.log_calls + 1] = { level = level, args = { ... } }
end

ngx.timer_calls = {}
ngx.timer = {
every = function(interval, callback)
ngx.timer_calls[#ngx.timer_calls + 1] = { interval = interval, callback = callback }
return true
end
}

ctx.bundle = { id = "bundle-1" }

ctx.bundle_loader = {
Expand Down Expand Up @@ -146,6 +154,17 @@ runner:given("^the decision api dependencies are initialized$", function(ctx)

ctx.env_overrides = {}
ctx.original_getenv = os.getenv

-- Mock GeoIP databases for reload test
ctx.test_cleanup_files = ctx.test_cleanup_files or {}
os.execute("mkdir -p data/geoip2")
local country_db = "data/geoip2/GeoLite2-Country.mmdb"
local f = io.open(country_db, "wb")
if f then
f:write("dummy")
f:close()
ctx.test_cleanup_files[#ctx.test_cleanup_files + 1] = country_db
end
end)

runner:given('^the mode is "([^"]+)"$', function(ctx, mode)
Expand All @@ -164,6 +183,7 @@ runner:given('^the mode is "([^"]+)"$', function(ctx, mode)

assert.is_true(ok)
assert.is_nil(err)
ctx.decision_api_initted = true
end)

runner:given('^the mode is "([^"]+)" and retry jitter is "([^"]+)"$', function(ctx, mode, _jitter)
Expand All @@ -179,6 +199,7 @@ runner:given('^the mode is "([^"]+)" and retry jitter is "([^"]+)"$', function(c
})
assert.is_true(ok)
assert.is_nil(err)
ctx.decision_api_initted = true
end)

runner:given('^headers include "([^"]+)" as "([^"]+)"$', function(_, name, value)
Expand Down Expand Up @@ -377,7 +398,29 @@ runner:then_('^request context body_hash is nil$', function(ctx)
assert.is_nil(ctx.request_context.body_hash)
end)

runner:then_("^geoip hot%-reload timer is scheduled for 24 hours$", function(_)
local found = false
local intervals = {}
for _, t in ipairs(ngx.timer_calls or {}) do
intervals[#intervals + 1] = t.interval
if t.interval == 86400 then
found = true
break
end
end
assert.is_true(found, "GeoIP reload timer (86400) not found in: " .. table.concat(intervals, ", "))
end)

runner:when("^I build request context$", function(ctx)
-- Ensure init is called with default mocks if not already done via "the mode is..."
if not ctx.decision_api_initted then
decision_api.init({
bundle_loader = ctx.bundle_loader,
rule_engine = ctx.rule_engine,
health = ctx.health,
})
ctx.decision_api_initted = true
end
ctx.request_context = decision_api.build_request_context()
end)

Expand Down
6 changes: 6 additions & 0 deletions spec/unit/features/decision_api.feature
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ Feature: Decision API unit behavior
And request context body_hash is nil
And the test cleanup restores globals

Scenario: BUG-15 geoip hot-reload timer is scheduled
Given the decision api dependencies are initialized
When I build request context
Then geoip hot-reload timer is scheduled for 24 hours
And the test cleanup restores globals

Rule: Access phase decision mapping
Scenario: Returns 503 when no bundle exists
Given the decision api dependencies are initialized
Expand Down
63 changes: 44 additions & 19 deletions src/fairvisor/decision_api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,41 @@ local function _inject_debug_headers(headers, decision)

return headers
end
local function _reload_geoip(geoip)
if not geoip then return end

local country_paths = { "/etc/geoip2/GeoLite2-Country.mmdb", "data/geoip2/GeoLite2-Country.mmdb" }
local asn_paths = { "/etc/geoip2/GeoLite2-ASN.mmdb", "data/geoip2/GeoLite2-ASN.mmdb" }

local dbs = {}
for _, path in ipairs(country_paths) do
local f = io.open(path, "rb")
if f then
f:close()
dbs.country = path
break
end
end

for _, path in ipairs(asn_paths) do
local f = io.open(path, "rb")
if f then
f:close()
dbs.asn = path
break
end
end

if dbs.country or dbs.asn then
_log_info("geoip_init_attempting databases_present")
local ok, err = pcall(geoip.init, dbs)
if not ok then
_log_err("geoip_init_failed err=", err)
else
_log_info("geoip_databases_loaded country=", tostring(dbs.country ~= nil), " asn=", tostring(dbs.asn ~= nil))
end
end
end

function _M.init(deps)
if type(deps) ~= "table" then
Expand All @@ -811,26 +846,16 @@ function _M.init(deps)
local geoip_ok, geoip = pcall(require, "resty.maxminddb")
if geoip_ok then
_deps.geoip = geoip
local country_db = "/etc/geoip2/GeoLite2-Country.mmdb"
local asn_db = "/etc/geoip2/GeoLite2-ASN.mmdb"

local dbs = {}
local f_country = io.open(country_db, "rb")
if f_country then
f_country:close()
dbs.country = country_db
end

local f_asn = io.open(asn_db, "rb")
if f_asn then
f_asn:close()
dbs.asn = asn_db
end

if dbs.country or dbs.asn then
local ok, err = pcall(geoip.init, dbs)
_reload_geoip(geoip)

-- Hot-reload GeoIP databases every 24 hours as required by issue #15
if ngx.timer and ngx.timer.every then
local ok, err = ngx.timer.every(86400, function(premature)
if premature then return end
_reload_geoip(geoip)
end)
if not ok then
_log_err("geoip_init_failed err=", err)
_log_err("geoip_reload_timer_failed err=", err)
end
end
end
Expand Down
Loading