Implement idempotency TTL and enhance world stats collection
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
-- Registration is deferred to RDB_OpRegisterAll(), called from
|
-- Registration is deferred to register(), called from RDB_Bootstrap's
|
||||||
-- RDB_Bootstrap's onServerStarted, rather than run at file top-level:
|
-- registerOps() (itself called from onServerStarted), rather than run at
|
||||||
-- PZ's require() is best-effort (warns and continues rather than forcing a
|
-- file top-level: PZ's require() is best-effort (warns and continues rather
|
||||||
-- synchronous load), so RDB_OpRegistry may not be defined yet if this file's
|
-- than forcing a synchronous load), so RDB_OpRegistry may not be defined yet
|
||||||
-- top-level code ran immediately at auto-load time. onServerStarted fires
|
-- if this file's top-level code ran immediately at auto-load time.
|
||||||
-- only after every file has auto-loaded, so it's safe there.
|
-- onServerStarted fires only after every file has auto-loaded, so it's safe
|
||||||
|
-- there.
|
||||||
|
|
||||||
RDB_OpWorldGetStats = {}
|
RDB_OpWorldGetStats = {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
-- Processed request-id -> cached response, so a repeated request id replays
|
-- Processed request-id -> cached response, so a repeated request id replays
|
||||||
-- the original response instead of re-executing the op.
|
-- the original response instead of re-executing the op.
|
||||||
-- Persisted via ModData so it survives restarts.
|
-- Persisted via ModData so it survives restarts.
|
||||||
|
--
|
||||||
|
-- Entries expire after RDB_Constants.IDEMPOTENCY_TTL_MS (60s by default):
|
||||||
|
-- a request id is only idempotent for retries within that window, not
|
||||||
|
-- forever. Without a TTL, a client that reuses a fixed id for routine
|
||||||
|
-- polling would get the first-ever response back on every call, since a
|
||||||
|
-- low-traffic server would rarely reach MAX_ENTRIES and trigger the
|
||||||
|
-- count-based eviction below.
|
||||||
|
|
||||||
|
require("RconDataBridge.RDB_Constants")
|
||||||
require("RconDataBridge.RDB_Log")
|
require("RconDataBridge.RDB_Log")
|
||||||
|
|
||||||
RDB_Idempotency = {}
|
RDB_Idempotency = {}
|
||||||
@@ -18,20 +26,35 @@ local function ensureStore()
|
|||||||
return store
|
return store
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function isExpired(entry)
|
||||||
|
return getTimestampMs() - (entry.ts or 0) >= RDB_Constants.IDEMPOTENCY_TTL_MS
|
||||||
|
end
|
||||||
|
|
||||||
function RDB_Idempotency.has(id)
|
function RDB_Idempotency.has(id)
|
||||||
return ensureStore()[id] ~= nil
|
local entry = ensureStore()[id]
|
||||||
|
return entry ~= nil and not isExpired(entry)
|
||||||
end
|
end
|
||||||
|
|
||||||
function RDB_Idempotency.get(id)
|
function RDB_Idempotency.get(id)
|
||||||
local entry = ensureStore()[id]
|
local entry = ensureStore()[id]
|
||||||
if not entry then
|
if not entry or isExpired(entry) then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
return entry.response
|
return entry.response
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Sweeps expired entries first (the common case that keeps the store small
|
||||||
|
-- under normal traffic), then falls back to trimming the oldest entries if
|
||||||
|
-- still over MAX_ENTRIES (a backstop against a burst of distinct ids within
|
||||||
|
-- a single TTL window).
|
||||||
local function prune(s)
|
local function prune(s)
|
||||||
|
for id, entry in pairs(s) do
|
||||||
|
if isExpired(entry) then
|
||||||
|
s[id] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local n = 0
|
local n = 0
|
||||||
for _ in pairs(s) do
|
for _ in pairs(s) do
|
||||||
n = n + 1
|
n = n + 1
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
-- World telemetry.
|
-- World telemetry, sourced from zombie.GameTime (getGameTime()).
|
||||||
|
|
||||||
require("RconDataBridge.RDB_Constants")
|
require("RconDataBridge.RDB_Constants")
|
||||||
require("RconDataBridge.RDB_Log")
|
require("RconDataBridge.RDB_Log")
|
||||||
@@ -7,15 +7,29 @@ require("RconDataBridge.RDB_OptionsRegistry")
|
|||||||
|
|
||||||
RDB_WorldStats = {}
|
RDB_WorldStats = {}
|
||||||
|
|
||||||
local cached = { playerCount = 0, generatedAt = 0 }
|
local cached = {}
|
||||||
|
|
||||||
function RDB_WorldStats.collect()
|
function RDB_WorldStats.collect()
|
||||||
return cached
|
return cached
|
||||||
end
|
end
|
||||||
|
|
||||||
local function refresh()
|
local function refresh()
|
||||||
|
local gt = getGameTime()
|
||||||
|
|
||||||
cached = {
|
cached = {
|
||||||
playerCount = getNumActivePlayers(),
|
worldAgeHours = gt:getWorldAgeHours(),
|
||||||
|
worldAgeDays = gt:getWorldAgeDaysSinceBegin(),
|
||||||
|
nightsSurvived = gt:getNightsSurvived(),
|
||||||
|
date = {
|
||||||
|
year = gt:getYear(),
|
||||||
|
month = gt:getMonth(),
|
||||||
|
day = gt:getDay(),
|
||||||
|
hour = gt:getHour(),
|
||||||
|
minute = gt:getMinutes(),
|
||||||
|
},
|
||||||
|
isNight = gt:isNight(),
|
||||||
|
isRaining = gt:isRainingToday(),
|
||||||
|
timeMultiplier = gt:getMultiplier(),
|
||||||
generatedAt = getTimestampMs(),
|
generatedAt = getTimestampMs(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,6 @@
|
|||||||
-- path) and read via showoptions, neither of which goes through the
|
-- path) and read via showoptions, neither of which goes through the
|
||||||
-- tokenizer, so they carry raw JSON unmolested.
|
-- tokenizer, so they carry raw JSON unmolested.
|
||||||
|
|
||||||
-- TODO: does output still base64 encode? otherwise the API is weird (base64 json in, json out)
|
|
||||||
|
|
||||||
RDB_Base64 = {}
|
RDB_Base64 = {}
|
||||||
|
|
||||||
local ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
local ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ RDB_Constants.MAX_PAYLOAD_BYTES = 4096
|
|||||||
RDB_Constants.OPTION_MAX_LENGTH = 8192
|
RDB_Constants.OPTION_MAX_LENGTH = 8192
|
||||||
RDB_Constants.POLL_EVERY_TICKS = 30
|
RDB_Constants.POLL_EVERY_TICKS = 30
|
||||||
|
|
||||||
|
-- How long a request id's cached response is replayed for on retry before
|
||||||
|
-- it's treated as expired and the op runs again. Deliberately short: a
|
||||||
|
-- client that reuses a fixed id for routine polling (e.g. "ws-poll" every
|
||||||
|
-- few minutes) should get a fresh answer each time, not the first-ever
|
||||||
|
-- response forever. A count-based cap alone (MAX_ENTRIES in
|
||||||
|
-- RDB_Idempotency) doesn't help here, it only evicts under high request
|
||||||
|
-- volume, never under low-and-slow polling, which is exactly this case.
|
||||||
|
RDB_Constants.IDEMPOTENCY_TTL_MS = 60 * 1000
|
||||||
|
|
||||||
RDB_Constants.ERROR_CODES = {
|
RDB_Constants.ERROR_CODES = {
|
||||||
SCHEMA_ERROR = "SCHEMA_ERROR",
|
SCHEMA_ERROR = "SCHEMA_ERROR",
|
||||||
PAYLOAD_TOO_LARGE = "PAYLOAD_TOO_LARGE",
|
PAYLOAD_TOO_LARGE = "PAYLOAD_TOO_LARGE",
|
||||||
|
|||||||
Reference in New Issue
Block a user