mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
a1a997bcda
Add the React Flow rule editor, ordered graph APIs and runtime DAG execution.\n\nPublish rules only on OpenResty reload and reconcile checksum-driven IP group snapshots in bounded shared memory.
357 lines
15 KiB
Lua
357 lines
15 KiB
Lua
local module_path = assert(WAF_IP_GROUPS_PATH, "WAF_IP_GROUPS_PATH is required")
|
|
|
|
local function assert_equal(actual, expected, message)
|
|
if actual ~= expected then
|
|
error((message or "values differ") .. ": expected " .. tostring(expected) .. ", got " .. tostring(actual), 2)
|
|
end
|
|
end
|
|
|
|
local shared_data = {}
|
|
local locks = {}
|
|
local shared = {}
|
|
function shared:get(key) return shared_data[key] end
|
|
function shared:set(key, value) shared_data[key] = value return true end
|
|
function shared:delete(key) shared_data[key] = nil return true end
|
|
function shared:safe_set(key, value) return shared:set(key, value) end
|
|
function shared:add(key, value, ttl)
|
|
assert_equal(ttl, 4, "coordination lock TTL")
|
|
if locks[key] then return false end
|
|
locks[key] = value
|
|
return true
|
|
end
|
|
function shared:safe_add(key, value, ttl) return shared:add(key, value, ttl) end
|
|
local function advance_time() locks = {} end
|
|
|
|
local disk_checksum = "v1"
|
|
local disk_json = "valid-v1"
|
|
local checksum_reads = 0
|
|
local json_reads = 0
|
|
local timer_callbacks = {}
|
|
|
|
local function decode(raw)
|
|
if raw == "valid-v1" then
|
|
return { groups = { ["1"] = { enabled = true, ip_list = { "192.0.2.1" } } } }
|
|
end
|
|
if raw == "valid-v2" then
|
|
return { groups = { ["2"] = { enabled = true, ip_list = { "198.51.100.2" } } } }
|
|
end
|
|
if raw == "valid-v3" then
|
|
return { groups = { ["3"] = { enabled = true, ip_list = { "203.0.113.3" } } } }
|
|
end
|
|
return nil, "invalid json"
|
|
end
|
|
|
|
local function load_worker()
|
|
local worker = assert(loadfile(module_path))()
|
|
worker.init({
|
|
shared = shared,
|
|
timer_every = function(interval, callback)
|
|
assert_equal(interval, 5, "refresh interval")
|
|
timer_callbacks[#timer_callbacks + 1] = callback
|
|
return true
|
|
end,
|
|
read_checksum = function()
|
|
checksum_reads = checksum_reads + 1
|
|
return disk_checksum
|
|
end,
|
|
read_json = function()
|
|
json_reads = json_reads + 1
|
|
return disk_json
|
|
end,
|
|
decode = decode,
|
|
max_snapshot_bytes = 20 * 1024 * 1024,
|
|
})
|
|
return worker
|
|
end
|
|
|
|
local first = load_worker()
|
|
local second = load_worker()
|
|
assert_equal(#timer_callbacks, 2, "each worker schedules a refresh timer")
|
|
assert_equal(checksum_reads, 1, "one worker coordinates initial checksum read")
|
|
assert_equal(json_reads, 1, "one worker reads initial JSON")
|
|
assert_equal(first.current().groups["1"].ip_list[1], "192.0.2.1", "first worker adopts initial snapshot")
|
|
assert_equal(second.current().groups["1"].ip_list[1], "192.0.2.1", "second worker adopts initial snapshot")
|
|
|
|
local function tick_all()
|
|
advance_time()
|
|
for _, callback in ipairs(timer_callbacks) do callback(false) end
|
|
end
|
|
|
|
checksum_reads = 0
|
|
json_reads = 0
|
|
for _ = 1, 3 do tick_all() end
|
|
assert_equal(checksum_reads, 3, "stable 15 seconds reads checksum once per interval")
|
|
assert_equal(json_reads, 0, "unchanged checksum never reads JSON")
|
|
|
|
disk_checksum = "v2"
|
|
disk_json = "valid-v2"
|
|
tick_all()
|
|
assert_equal(json_reads, 1, "changed snapshot JSON is read once across workers")
|
|
assert_equal(first.current().groups["2"].ip_list[1], "198.51.100.2", "first worker adopts v2")
|
|
assert_equal(second.current().groups["2"].ip_list[1], "198.51.100.2", "second worker adopts v2")
|
|
|
|
disk_checksum = "v3"
|
|
disk_json = "valid-v3"
|
|
tick_all()
|
|
assert_equal(shared_data.ip_groups_previous_version, "v2", "previous pointer follows committed version")
|
|
assert_equal(shared_data["ip_groups_raw:v1"], nil, "snapshot older than previous is cleaned")
|
|
assert_equal(shared_data["ip_groups_raw:v2"], "valid-v2", "previous committed raw is retained")
|
|
assert_equal(shared_data["ip_groups_raw:v3"], "valid-v3", "current committed raw is retained")
|
|
|
|
disk_checksum = "v2"
|
|
disk_json = "valid-v2"
|
|
tick_all()
|
|
assert_equal(shared_data.ip_groups_version, "v2", "rollback checksum becomes current commit")
|
|
assert_equal(shared_data.ip_groups_previous_version, "v3", "rollback retains former current as previous")
|
|
assert_equal(shared_data["ip_groups_raw:v2"], "valid-v2", "rollback must not clean its new current raw")
|
|
assert_equal(shared_data["ip_groups_raw:v3"], "valid-v3", "rollback retains previous raw")
|
|
|
|
disk_checksum = "v4"
|
|
disk_json = "invalid-v4"
|
|
tick_all()
|
|
assert_equal(shared_data.ip_groups_version, "v2", "invalid update preserves shared version")
|
|
assert_equal(first.current().groups["2"].ip_list[1], "198.51.100.2", "invalid update preserves first worker")
|
|
assert_equal(second.current().groups["2"].ip_list[1], "198.51.100.2", "invalid update preserves second worker")
|
|
|
|
local reads_before_requests = checksum_reads + json_reads
|
|
for _ = 1, 20 do
|
|
assert_equal(first.current().groups["2"].enabled, true, "request reads worker-local object")
|
|
end
|
|
assert_equal(checksum_reads + json_reads, reads_before_requests, "current() performs zero file I/O")
|
|
|
|
timer_callbacks[1](true)
|
|
assert_equal(checksum_reads + json_reads, reads_before_requests, "premature timer performs zero file I/O")
|
|
|
|
local function test_failed_commit_never_exposes_unpublished_raw_to_new_worker()
|
|
local data = {}
|
|
local held_locks = {}
|
|
local callbacks = {}
|
|
local checksum = "v1"
|
|
local raw = "valid-v1"
|
|
local reads = 0
|
|
local fail_commit = false
|
|
local interleaved_worker
|
|
local load_regression_worker
|
|
local regression_shared = {}
|
|
|
|
function regression_shared:get(key) return data[key] end
|
|
function regression_shared:add(key, value)
|
|
if held_locks[key] then return false end
|
|
held_locks[key] = value
|
|
return true
|
|
end
|
|
function regression_shared:delete(key) data[key] = nil return true end
|
|
local function set_regression_value(key, value)
|
|
if key == "ip_groups_version" and fail_commit then
|
|
return false, "shared dictionary full"
|
|
end
|
|
data[key] = value
|
|
if fail_commit and string.sub(key, 1, #"ip_groups_raw") == "ip_groups_raw" and not interleaved_worker then
|
|
interleaved_worker = load_regression_worker()
|
|
end
|
|
return true
|
|
end
|
|
function regression_shared:set(key, value) return set_regression_value(key, value) end
|
|
function regression_shared:safe_set(key, value) return set_regression_value(key, value) end
|
|
function regression_shared:safe_add(key, value) return regression_shared:add(key, value) end
|
|
|
|
load_regression_worker = function()
|
|
local worker = assert(loadfile(module_path))()
|
|
assert(worker.init({
|
|
shared = regression_shared,
|
|
timer_every = function(_, callback) callbacks[#callbacks + 1] = callback return true end,
|
|
read_checksum = function() return checksum end,
|
|
read_json = function() reads = reads + 1 return raw end,
|
|
decode = decode,
|
|
max_snapshot_bytes = 20 * 1024 * 1024,
|
|
}))
|
|
return worker
|
|
end
|
|
|
|
local established_worker = load_regression_worker()
|
|
assert_equal(established_worker.current().groups["1"].ip_list[1], "192.0.2.1", "v1 is committed before failure")
|
|
|
|
held_locks = {}
|
|
reads = 0
|
|
checksum = "v2"
|
|
raw = "valid-v2"
|
|
fail_commit = true
|
|
callbacks[1](false)
|
|
|
|
assert_equal(reads, 1, "failed commit still reads changed JSON only once")
|
|
assert_equal(data.ip_groups_version, "v1", "failed pointer write preserves committed version")
|
|
assert_equal(data["ip_groups_raw:v2"], nil, "failed commit cleans only unpublished v2 raw")
|
|
assert_equal(established_worker.current().groups["1"].ip_list[1], "192.0.2.1", "existing worker preserves committed v1")
|
|
assert(interleaved_worker, "raw publication must interleave a newly initialized worker")
|
|
assert_equal(interleaved_worker.current().groups["2"], nil, "new worker must not expose unpublished v2")
|
|
assert_equal(interleaved_worker.current().groups["1"].ip_list[1], "192.0.2.1", "new worker must never adopt unpublished v2 raw")
|
|
end
|
|
|
|
test_failed_commit_never_exposes_unpublished_raw_to_new_worker()
|
|
|
|
local function test_capacity_failure_never_evicts_committed_snapshot()
|
|
local data = {
|
|
ip_groups_version = "v1",
|
|
ip_groups_previous_version = "v0",
|
|
["ip_groups_raw:v1"] = "valid-v1",
|
|
["ip_groups_raw:v0"] = "valid-v0",
|
|
}
|
|
local locks = {}
|
|
local callbacks = {}
|
|
local disk_checksum = "v1"
|
|
local disk_raw = "valid-v1"
|
|
local json_reads = 0
|
|
local ordinary_writes = 0
|
|
local warnings = {}
|
|
local dict = {}
|
|
function dict:get(key) return data[key] end
|
|
function dict:delete(key) data[key] = nil return true end
|
|
function dict:add(key, value)
|
|
if locks[key] then return false end
|
|
locks[key] = value
|
|
return true
|
|
end
|
|
function dict:safe_add(key, value) return dict:add(key, value) end
|
|
function dict:set(key, value)
|
|
ordinary_writes = ordinary_writes + 1
|
|
if key == "ip_groups_raw:v2" then
|
|
data = { [key] = value }
|
|
return true, nil, true
|
|
end
|
|
data[key] = value
|
|
return true, nil, false
|
|
end
|
|
function dict:safe_set(key, value)
|
|
if key == "ip_groups_raw:v2" then return nil, "no memory", false end
|
|
data[key] = value
|
|
return true, nil, false
|
|
end
|
|
|
|
local worker = assert(loadfile(module_path))()
|
|
assert(worker.init({
|
|
shared = dict,
|
|
timer_every = function(_, callback) callbacks[1] = callback return true end,
|
|
read_checksum = function() return disk_checksum end,
|
|
read_json = function() json_reads = json_reads + 1 return disk_raw end,
|
|
decode = decode,
|
|
max_snapshot_bytes = 20 * 1024 * 1024,
|
|
log_warning = function(message) warnings[#warnings + 1] = message end,
|
|
}))
|
|
assert_equal(worker.current().groups["1"].ip_list[1], "192.0.2.1", "worker starts from committed v1")
|
|
|
|
locks = {}
|
|
disk_checksum = "v2"
|
|
disk_raw = "valid-v2"
|
|
callbacks[1](false)
|
|
|
|
assert_equal(ordinary_writes, 0, "snapshot publication must never use evicting set")
|
|
assert_equal(json_reads, 1, "capacity failure reads changed JSON once")
|
|
assert_equal(data.ip_groups_version, "v1", "capacity failure preserves commit pointer")
|
|
assert_equal(data.ip_groups_previous_version, "v0", "capacity failure preserves previous metadata")
|
|
assert_equal(data["ip_groups_raw:v1"], "valid-v1", "capacity failure preserves current raw")
|
|
assert_equal(data["ip_groups_raw:v0"], "valid-v0", "capacity failure preserves previous raw")
|
|
assert_equal(data["ip_groups_raw:v2"], nil, "capacity failure does not publish new raw")
|
|
assert_equal(worker.current().groups["1"].ip_list[1], "192.0.2.1", "capacity failure preserves worker-local snapshot")
|
|
assert_equal(#warnings, 1, "capacity failure is logged")
|
|
end
|
|
|
|
local function test_previous_metadata_failure_keeps_committed_snapshot_without_cleanup()
|
|
local data = {
|
|
ip_groups_version = "v1",
|
|
ip_groups_previous_version = "v0",
|
|
["ip_groups_raw:v1"] = "valid-v1",
|
|
["ip_groups_raw:v0"] = "valid-v0",
|
|
}
|
|
local locks = {}
|
|
local callback
|
|
local checksum = "v1"
|
|
local raw = "valid-v1"
|
|
local deletes = 0
|
|
local warnings = {}
|
|
local dict = {}
|
|
function dict:get(key) return data[key] end
|
|
function dict:delete(key) deletes = deletes + 1 data[key] = nil return true end
|
|
function dict:add(key, value)
|
|
if locks[key] then return false end
|
|
locks[key] = value
|
|
return true
|
|
end
|
|
function dict:safe_add(key, value) return dict:add(key, value) end
|
|
function dict:set(key, value) data[key] = value return true end
|
|
function dict:safe_set(key, value)
|
|
if key == "ip_groups_previous_version" then return nil, "no memory", false end
|
|
data[key] = value
|
|
return true, nil, false
|
|
end
|
|
|
|
local worker = assert(loadfile(module_path))()
|
|
assert(worker.init({
|
|
shared = dict,
|
|
timer_every = function(_, value) callback = value return true end,
|
|
read_checksum = function() return checksum end,
|
|
read_json = function() return raw end,
|
|
decode = decode,
|
|
max_snapshot_bytes = 20 * 1024 * 1024,
|
|
log_warning = function(message) warnings[#warnings + 1] = message end,
|
|
}))
|
|
|
|
locks = {}
|
|
checksum = "v2"
|
|
raw = "valid-v2"
|
|
callback(false)
|
|
|
|
assert_equal(data.ip_groups_version, "v2", "successful commit pointer remains authoritative")
|
|
assert_equal(data.ip_groups_previous_version, "v0", "failed previous metadata write is not forced")
|
|
assert_equal(data["ip_groups_raw:v2"], "valid-v2", "new committed raw remains")
|
|
assert_equal(data["ip_groups_raw:v1"], "valid-v1", "old current raw remains when cleanup is skipped")
|
|
assert_equal(data["ip_groups_raw:v0"], "valid-v0", "old previous raw remains when cleanup is skipped")
|
|
assert_equal(deletes, 0, "previous metadata failure skips all cleanup")
|
|
assert_equal(worker.current().groups["2"].ip_list[1], "198.51.100.2", "worker adopts valid committed v2")
|
|
assert_equal(#warnings, 1, "previous metadata failure is logged")
|
|
end
|
|
|
|
local function test_oversized_raw_is_rejected_before_shared_publication()
|
|
local data = { ip_groups_version = "v1", ["ip_groups_raw:v1"] = "valid-v1" }
|
|
local locks = {}
|
|
local callback
|
|
local checksum = "v1"
|
|
local raw = "valid-v1"
|
|
local shared_writes = 0
|
|
local warnings = {}
|
|
local dict = {}
|
|
function dict:get(key) return data[key] end
|
|
function dict:delete(key) data[key] = nil return true end
|
|
function dict:add(key, value) if locks[key] then return false end locks[key] = value return true end
|
|
function dict:safe_add(key, value) return dict:add(key, value) end
|
|
function dict:set(key, value) shared_writes = shared_writes + 1 data[key] = value return true end
|
|
function dict:safe_set(key, value) shared_writes = shared_writes + 1 data[key] = value return true, nil, false end
|
|
|
|
local worker = assert(loadfile(module_path))()
|
|
assert(worker.init({
|
|
shared = dict,
|
|
timer_every = function(_, value) callback = value return true end,
|
|
read_checksum = function() return checksum end,
|
|
read_json = function() return raw end,
|
|
decode = decode,
|
|
max_snapshot_bytes = 4,
|
|
log_warning = function(message) warnings[#warnings + 1] = message end,
|
|
}))
|
|
|
|
locks = {}
|
|
checksum = "v2"
|
|
raw = "valid-v2"
|
|
callback(false)
|
|
|
|
assert_equal(shared_writes, 0, "oversized raw is rejected before shared writes")
|
|
assert_equal(data.ip_groups_version, "v1", "oversized raw preserves commit pointer")
|
|
assert_equal(data["ip_groups_raw:v1"], "valid-v1", "oversized raw preserves committed data")
|
|
assert_equal(worker.current().groups["1"].ip_list[1], "192.0.2.1", "oversized raw preserves worker-local snapshot")
|
|
assert_equal(#warnings, 1, "oversized raw rejection is logged")
|
|
end
|
|
|
|
test_capacity_failure_never_evicts_committed_snapshot()
|
|
test_previous_metadata_failure_keeps_committed_snapshot_without_cleanup()
|
|
test_oversized_raw_is_rejected_before_shared_publication()
|
|
|
|
return true
|