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
|
||||
-- RDB_Bootstrap's onServerStarted, rather than run at file top-level:
|
||||
-- PZ's require() is best-effort (warns and continues rather than forcing a
|
||||
-- synchronous load), so RDB_OpRegistry may not be defined yet if this file's
|
||||
-- top-level code ran immediately at auto-load time. onServerStarted fires
|
||||
-- only after every file has auto-loaded, so it's safe there.
|
||||
-- Registration is deferred to register(), called from RDB_Bootstrap's
|
||||
-- registerOps() (itself called from onServerStarted), rather than run at
|
||||
-- file top-level: PZ's require() is best-effort (warns and continues rather
|
||||
-- than forcing a synchronous load), so RDB_OpRegistry may not be defined yet
|
||||
-- if this file's top-level code ran immediately at auto-load time.
|
||||
-- onServerStarted fires only after every file has auto-loaded, so it's safe
|
||||
-- there.
|
||||
|
||||
RDB_OpWorldGetStats = {}
|
||||
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
-- Processed request-id -> cached response, so a repeated request id replays
|
||||
-- the original response instead of re-executing the op.
|
||||
-- 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")
|
||||
|
||||
RDB_Idempotency = {}
|
||||
@@ -18,20 +26,35 @@ local function ensureStore()
|
||||
return store
|
||||
end
|
||||
|
||||
local function isExpired(entry)
|
||||
return getTimestampMs() - (entry.ts or 0) >= RDB_Constants.IDEMPOTENCY_TTL_MS
|
||||
end
|
||||
|
||||
function RDB_Idempotency.has(id)
|
||||
return ensureStore()[id] ~= nil
|
||||
local entry = ensureStore()[id]
|
||||
return entry ~= nil and not isExpired(entry)
|
||||
end
|
||||
|
||||
function RDB_Idempotency.get(id)
|
||||
local entry = ensureStore()[id]
|
||||
if not entry then
|
||||
if not entry or isExpired(entry) then
|
||||
return nil
|
||||
end
|
||||
|
||||
return entry.response
|
||||
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)
|
||||
for id, entry in pairs(s) do
|
||||
if isExpired(entry) then
|
||||
s[id] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local n = 0
|
||||
for _ in pairs(s) do
|
||||
n = n + 1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- World telemetry.
|
||||
-- World telemetry, sourced from zombie.GameTime (getGameTime()).
|
||||
|
||||
require("RconDataBridge.RDB_Constants")
|
||||
require("RconDataBridge.RDB_Log")
|
||||
@@ -7,16 +7,30 @@ require("RconDataBridge.RDB_OptionsRegistry")
|
||||
|
||||
RDB_WorldStats = {}
|
||||
|
||||
local cached = { playerCount = 0, generatedAt = 0 }
|
||||
local cached = {}
|
||||
|
||||
function RDB_WorldStats.collect()
|
||||
return cached
|
||||
end
|
||||
|
||||
local function refresh()
|
||||
local gt = getGameTime()
|
||||
|
||||
cached = {
|
||||
playerCount = getNumActivePlayers(),
|
||||
generatedAt = getTimestampMs(),
|
||||
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(),
|
||||
}
|
||||
|
||||
RDB_OptionsRegistry.set(RDB_Constants.OPT.WORLD_STATS, RDB_Json.encode(cached))
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
-- path) and read via showoptions, neither of which goes through the
|
||||
-- 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 = {}
|
||||
|
||||
local ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
|
||||
@@ -24,6 +24,15 @@ RDB_Constants.MAX_PAYLOAD_BYTES = 4096
|
||||
RDB_Constants.OPTION_MAX_LENGTH = 8192
|
||||
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 = {
|
||||
SCHEMA_ERROR = "SCHEMA_ERROR",
|
||||
PAYLOAD_TOO_LARGE = "PAYLOAD_TOO_LARGE",
|
||||
|
||||
Reference in New Issue
Block a user