perf: buffer mod_vnc_lastactivity DB writes and flush periodically

The store :set() calls go through mod_storage_sql (LuaDBI), a blocking C
client. These hooks fire on the presence/auth hot path, so inline writes
stall the event loop under presence storms. Buffer writes in memory and
flush every 5 seconds (configurable via vnc_lastactivity_flush_interval).
The IQ handlers already read from in-memory map/remote_act_cache first,
so read consistency is preserved. Final flush on server-stopping and
host-deactivating prevents data loss on graceful shutdown.

Part-of: <http://gitlab.vnc.biz/uxf/vnctalk-prosody/-/merge_requests/3>
This commit is contained in:
2026-07-15 17:58:02 +02:00
parent efa6e73364
commit 83e578cb94
+60 -5
View File
@@ -26,6 +26,44 @@ local activity_store = module:open_store("activity", "map");
local remote_activity_store = module:open_store("remote_activity", "map");
local remote_avatar_update_store = module:open_store("remote_avatar", "map");
-- The store :set() calls go through mod_storage_sql (LuaDBI), which is a
-- blocking C client -- every write blocks the event loop for a DB round-trip.
-- These hooks fire on the presence/auth hot path, so doing the write inline
-- stalls the whole server under presence storms. Instead we buffer the
-- last-value-wins writes in memory (the IQ handlers already read from the
-- in-memory map/caches) and flush them periodically from a timer.
local FLUSH_INTERVAL = module:get_option_number("vnc_lastactivity_flush_interval", 5);
-- Pending writes, coalesced by user/contact. Only the most recent value per
-- key is kept, matching the "last value wins" semantics of the stores.
local pending_activity = {}; -- [username] = stamp (store key "last")
local pending_remote_activity = {}; -- [contact_bare] = stamp (store key "last")
local pending_remote_avatar = {}; -- [contact_bare] = stamp (store key "avatar")
local function flush_pending_writes()
for username, value in pairs(pending_activity) do
local ok, err = activity_store:set(username, "last", value);
if not ok then
module:log("warn", "flush activity_store:set(%s) failed: %s", username, tostring(err));
end
end
for contact_bare, value in pairs(pending_remote_activity) do
local ok, err = remote_activity_store:set(contact_bare, "last", value);
if not ok then
module:log("warn", "flush remote_activity_store:set(%s) failed: %s", contact_bare, tostring(err));
end
end
for contact_bare, value in pairs(pending_remote_avatar) do
local ok, err = remote_avatar_update_store:set(contact_bare, "avatar", value);
if not ok then
module:log("warn", "flush remote_avatar_update_store:set(%s) failed: %s", contact_bare, tostring(err));
end
end
for k in pairs(pending_activity) do pending_activity[k] = nil; end
for k in pairs(pending_remote_activity) do pending_remote_activity[k] = nil; end
for k in pairs(pending_remote_avatar) do pending_remote_avatar[k] = nil; end
end
local function getTableLng(tbl)
local getN = 0
for n in pairs(tbl) do
@@ -107,8 +145,8 @@ local map = {};
module:hook("authentication-success", function(event)
local session = event.session;
if session.username then
local pers = activity_store:set(session.username, "last", 0);
end
pending_activity[session.username] = 0;
end
end, 10);
@@ -139,7 +177,7 @@ module:hook("presence/bare", function(event)
remote_act_sessions[contact_bare] = nil;
remote_act_cache[contact_bare] = t;
-- module:log("info", "remote_act_cache for %s is now %s", contact_bare, remote_act_cache[contact_bare]);
remote_activity_store:set(contact_bare, "last", t);
pending_remote_activity[contact_bare] = t;
end
end
else
@@ -161,7 +199,7 @@ module:hook("presence/bare", function(event)
if (x_photo_update) then
-- module:log("info", "avatarupdate - need to store %s update for %s", x_photo_update, contact_bare);
local t = os.time();
remote_avatar_update_store:set(contact_bare, "avatar", t);
pending_remote_avatar[contact_bare] = t;
end
end
@@ -188,7 +226,7 @@ module:hook("pre-presence/bare", function(event)
end
if lastsession then
-- module:log("info", "last session gone - storing to activity_store for %s => %s", event.origin.username, s);
local pers = activity_store:set(event.origin.username, "last", s);
pending_activity[event.origin.username] = s;
end
end
end, 10);
@@ -413,3 +451,20 @@ end
module.restore = function(data)
map = data.map or {};
end
-- Flush buffered writes on a fixed cadence instead of on every presence/auth
-- event. Returning FLUSH_INTERVAL reschedules the timer. The DB I/O still
-- blocks the event loop while it runs, but once per interval rather than per
-- stanza.
module:add_timer(FLUSH_INTERVAL, function ()
flush_pending_writes();
return FLUSH_INTERVAL;
end);
-- Best-effort final flush so buffered writes are not lost on stop/reload.
module:hook_global("server-stopping", function ()
flush_pending_writes();
end);
module:hook("host-deactivating", function ()
flush_pending_writes();
end);