perf: add global LRU cache to mod_vnc_lastactivity store reads

Add a module-scoped util.cache LRU (lastactivity_cache_size default 5000,
lastactivity_cache_ttl default 300s) in front of the activity_store /
remote_activity_store fallback. Write-through on all set sites
(authentication-success, local last-session unavailable, remote
unavailable) and read-through in both IQ handlers (batch and standard
jabber:iq:last), so repeated last-activity queries for the same contacts
no longer hit the synchronous SQL driver, reducing event-loop blocking.

Also removes dead code (avatar-hash helpers, dumpTable, unused imports,
commented-out debug logging) and the unused remote avatar-update tracking.

Part-of: <http://gitlab.vnc.biz/uxf/vnctalk-prosody/-/merge_requests/5>
This commit is contained in:
2026-07-16 04:26:15 +00:00
committed by marge
parent 658008606a
commit e98f9cf4c9
+51 -161
View File
@@ -7,24 +7,36 @@
--
local st = require "util.stanza";
local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
local jid_bare = require "util.jid".bare;
local jid_split = require "util.jid".split;
local base64 = require"util.encodings".base64;
local sha1 = require"util.hashes".sha1;
local my_host = module:get_host();
local my_host_type = module:get_host_type();
local avt_hash_table = {};
local remote_act_cache = {};
local remote_act_sessions = {};
module:depends"vcard";
local vcard_storage = module:open_store("vcard");
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");
-- Options:
-- lastactivity_cache_size max entries in the last-activity LRU cache (default 5000)
-- lastactivity_cache_ttl max age (seconds) of a cached entry before re-fetch (default 300)
local cache_lib = require "util.cache";
local last_activity_cache = cache_lib.new(
module:get_option_integer("lastactivity_cache_size", 5000, 1));
local cache_ttl = module:get_option_number("lastactivity_cache_ttl", 300);
-- cache stores the persisted "last" timestamp (0 == online) keyed by bare JID,
-- write-through on every store write and read-through on every store read.
local function cache_set(bare_jid, stamp)
last_activity_cache:set(bare_jid, { stamp = stamp, ts = os.time() });
end
local function cache_get(bare_jid)
local entry = last_activity_cache:get(bare_jid);
if not entry then return nil end
if os.difftime(os.time(), entry.ts) > cache_ttl then return nil end
return entry.stamp;
end
local function getTableLng(tbl)
local getN = 0
@@ -34,70 +46,6 @@ local function getTableLng(tbl)
return getN
end
local function dumpTable(t, depth)
local result = "";
if (not depth) then depth = 0 end;
local prefix = string.rep(" ", depth);
if (type(t) ~= "table") then
result = result.." "..tostring(t);
else
for key,value in pairs(t) do
result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
end
end
return result;
end
local function get_vcard(username)
local vcard, err = vcard_storage:get(username);
if vcard then
vcard = st.deserialize(vcard);
end
if not vcard then
vcard = st.stanza("vCard", { xmlns = "vcard-temp" });
end
return vcard, err;
end
local function calculate_avatar_hash(username)
local vcard = get_vcard(username)
local photo = vcard and vcard:get_child("PHOTO")
if photo then
local photo_type = photo:get_child_text("TYPE");
local photo_b64 = photo:get_child_text("BINVAL");
local photo_raw = photo_b64 and base64.decode(photo_b64);
if photo_raw and photo_type then
return sha1(photo_raw, true);
end
end
return ""
end
local function get_avatar_hash(username, host)
if (my_host ~= host) then
return nil
end
local session = nil
if (prosody.hosts[host].sessions ~= nil) then
session = prosody.hosts[host] and prosody.hosts[host].sessions[username]
end
local avt_hash = nil;
if not avt_hash_table[username] then
avt_hash = calculate_avatar_hash(username);
avt_hash_table[username] = avt_hash;
-- -- module:log("info", "DEBUG get_avatar_hash returning result after new calc");
else
-- -- module:log("info", "DEBUG get_avatar_hash returning result from cache");
avt_hash = avt_hash_table[username];
end;
return avt_hash
end
module:add_feature("jabber:iq:last");
module:add_feature("jabber:iq:batch");
@@ -108,6 +56,7 @@ module:hook("authentication-success", function(event)
local session = event.session;
if session.username then
local pers = activity_store:set(session.username, "last", 0);
cache_set(session.username.."@"..my_host, 0);
end
end, 10);
@@ -115,8 +64,6 @@ end, 10);
module:hook("presence/bare", function(event)
local origin, stanza = event.origin, event.stanza;
local typ = stanza.attr.type;
-- module:log("info", "Cache hook, got raw %s ", dumpTable(stanza));
-- module:log("info", "Cache hook, got %s from a %s - %s", stanza:top_tag(), origin.type, typ);
if origin.type == "s2sin" and ( typ == nil or typ == "unavailable" ) then
@@ -125,46 +72,24 @@ module:hook("presence/bare", function(event)
local contact_user, contact_host, contact_res = jid_split(contact_full);
local contact_sessions = remote_act_sessions[contact_bare] or {};
-- module:log("info", "contact_sessions from %s is %s", contact_bare, dumpTable(remote_act_sessions[contact_bare]));
if typ == "unavailable" then -- set current time in cache
local t = os.time();
local s = "";
if contact_sessions[contact_full] then
contact_sessions[contact_full] = nil;
remote_act_sessions[contact_bare] = contact_sessions;
local contact_session_count = getTableLng(contact_sessions);
-- module:log("info", "storing online state for %s in after removing contact_full, net length %s, %s", contact_full, contact_session_count, dumpTable(contact_sessions));
local contact_session_count = getTableLng(contact_sessions);
if ( contact_session_count == 0 ) then
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);
cache_set(contact_bare, t);
end
end
else
-- set 0 (online) in cache
-- module:log("info", "storing online state for %s in remote_act_cache %s", contact_bare, dumpTable(remote_act_sessions[contact_bare]));
-- remote_act_cache[contact_bare] = 0;
local contact_sessions = remote_act_sessions[contact_bare] or {};
-- module:log("info", "storing online state for %s in pre local contact_sessions %s", contact_bare, dumpTable(contact_sessions));
contact_sessions[contact_full] = 1;
-- module:log("info", "storing online state for %s in podt local contact_sessions %s", contact_bare, dumpTable(contact_sessions));
remote_act_sessions[contact_bare] = contact_sessions;
local contact_session_count = getTableLng(contact_sessions);
-- module:log("info", "contact_sessions afterstore from %s is new length %s, %s", contact_bare, contact_session_count, dumpTable(remote_act_sessions[contact_bare]));
local x_update = stanza:get_child("x", "vcard-temp:x:update") or nil;
if (x_update) then
local x_photo_update = x_update:get_child_text("photo") or nil;
-- module:log("info", "Cache hook1 debug: %s", x_photo_update);
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);
end
end
end
end
@@ -172,9 +97,7 @@ end, 10);
module:hook("pre-presence/bare", function(event)
local stanza = event.stanza;
-- module:log("info", "presence hook - event.origin.username: %s", event.origin.username);
if not(stanza.attr.to) and stanza.attr.type == "unavailable" then
-- module:log("info", "unavailable hook - event.origin.username: %s", event.origin.username);
local s = os.time();
local t = stanza:get_child_text("status");
map[event.origin.username] = {s = s, t = t};
@@ -187,8 +110,8 @@ module:hook("pre-presence/bare", function(event)
end
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);
cache_set(event.origin.username.."@"..my_host, s);
end
end
end, 10);
@@ -196,35 +119,24 @@ end, 10);
module:hook("iq/bare/jabber:iq:batch:query", function(event)
local origin, stanza = event.origin, event.stanza;
if stanza.attr.type == "get" then
-- -- module:log("info", "got stanza: %s", dumpTable(stanza));
local querychild = stanza:get_child("query", "jabber:iq:batch");
local reply = st.reply(stanza):tag('query', {xmlns='jabber:iq:last'});
-- :childtags("jid", "jabber:iq:batch");
-- -- module:log("info", "querychilds: %s", dumpTable(querychild));
for ajid in querychild:childtags("jid") do
local abjid = ajid:get_text();
-- module:log("info", "got jid: %s", abjid);
local username = jid_split(abjid);
local luser, lhost = jid_split(abjid);
local seconds, text = "-1", "";
local hash = "";
-- -- module:log("info", "map is now: %s", dumpTable(map));
if map[username] then
-- module:log("info", "found map[ %s ] => t: %s", username, map[username].t );
-- module:log("info", "found map[ %s ] => s: %s", username, map[username].s );
local mt = tonumber(map[username].s);
seconds = tostring(os.difftime(os.time(), mt));
text = map[username].s;
end
if luser and lhost then
-- -- module:log("info", "luser: %s ++ lhost %s", luser, lhost);
local sessions_avail = false;
if (hosts[lhost]) then
-- module:log("info", "lhost exists: %s", lhost);
if (hosts[lhost].sessions) then
-- module:log("info", "got session object for lhost %s", lhost);
sessions_avail = true;
end
end
@@ -235,17 +147,13 @@ module:hook("iq/bare/jabber:iq:batch:query", function(event)
if user_sessions then
for x,s in pairs(user_sessions.sessions) do
if (s.hibernating) then
-- module:log("info", "user_sessions s.hibernated %s is %s", s, s.hibernating);
if (s.hibernating > latest_hibernated_stamp) then
latest_hibernated_stamp = s.hibernating;
end
else
-- module:log("info", "user_sessions %s is active", s);
all_hibernated = false;
end
end
-- module:log("info","latest_hibernated_stamp is %s", latest_hibernated_stamp);
-- module:log("info","all_hibernated is %s", all_hibernated);
if (latest_hibernated_stamp > 0 and all_hibernated) then
seconds = os.difftime(os.time(), latest_hibernated_stamp)
else
@@ -255,15 +163,12 @@ module:hook("iq/bare/jabber:iq:batch:query", function(event)
end
if not (prosody.hosts[lhost]) then
local contact_bare = luser.."@"..lhost;
-- module:log("info","getting act for remote %s", contact_bare);
if remote_act_sessions[contact_bare] then
local contact_sessions = getTableLng(remote_act_sessions[contact_bare]);
if ( contact_sessions == 0 ) then
-- module:log("info","setting seconds to 0 for remote_act_cache %s -> ", contact_bare, remote_act_cache[contact_bare]);
seconds = tostring(os.difftime(os.time(), remote_act_cache[contact_bare]));
else
seconds = 0;
-- module:log("info","setting seconds to 0 for remote %s", contact_bare);
end
else
if remote_act_cache[contact_bare] then
@@ -279,32 +184,39 @@ module:hook("iq/bare/jabber:iq:batch:query", function(event)
if (seconds == "-1") then
if (prosody.hosts[lhost]) then
local stored_seconds = activity_store:get(luser, "last") or false;
if stored_seconds then
-- module:log("info", "read persistant lastactivity for %s as %s", luser, dumpTable(stored_seconds));
local nseconds = tonumber(stored_seconds);
local bare_jid = luser.."@"..lhost;
local nseconds = cache_get(bare_jid);
if nseconds == nil then
local stored_seconds = activity_store:get(luser, "last") or false;
if stored_seconds then
nseconds = tonumber(stored_seconds);
cache_set(bare_jid, nseconds);
end
end
if nseconds then
if (nseconds == 0) then
-- module:log("info","using restored for %s @ %s ", luser, lhost);
map[event.origin.username] = {s = "0", t = text};
else
seconds = tostring(os.difftime(os.time(), nseconds));
-- module:log("info","using restored for %s @ %s => %s", luser, lhost, nseconds);
map[event.origin.username] = {s = nseconds, t = text};
end
end
else
if (luser ~= nil) then
local contact_bare = luser.."@"..lhost;
local stored_seconds = remote_activity_store:get(contact_bare, "last") or false;
if stored_seconds then
-- module:log("info", "read persistant lastactivity for %s as %s", luser, dumpTable(stored_seconds));
local nseconds = tonumber(stored_seconds);
local nseconds = cache_get(contact_bare);
if nseconds == nil then
local stored_seconds = remote_activity_store:get(contact_bare, "last") or false;
if stored_seconds then
nseconds = tonumber(stored_seconds);
cache_set(contact_bare, nseconds);
end
end
if nseconds then
if (nseconds == 0) then
-- module:log("info","using restored for %s @ %s ", luser, lhost);
map[event.origin.username] = {s = "0", t = text};
else
seconds = tostring(os.difftime(os.time(), nseconds));
-- module:log("info","using restored for %s @ %s => %s", luser, lhost, nseconds);
map[event.origin.username] = {s = nseconds, t = text};
end
end
@@ -312,19 +224,9 @@ module:hook("iq/bare/jabber:iq:batch:query", function(event)
end
end
-- hash = get_avatar_hash(luser, lhost);
-- reply:add_child():tag("result", {jid=abjid, seconds=seconds, photo=hash}):up();
-- module:log("info","determined for %s => %s seconds", abjid, seconds);
-- module:log("info","determined for %s => %s seconds", abjid, tostring(seconds));
-- module:log("info","type jid: %s", type(abjid));
-- module:log("info","type seconds: %s", type(seconds));
-- module:log("info"," reply before add: %s", reply);
-- reply:add_child():tag("result", { jid=abjid; seconds=tostring(seconds); } ):up();
local rtag = st.stanza("result", { jid=abjid; seconds=tostring(seconds); });
reply:add_child(rtag);
end
-- module:log("info", "sending reply %s", reply);
origin.send(reply);
end
return true;
@@ -335,22 +237,15 @@ module:hook("iq/bare/jabber:iq:last:query", function(event)
if stanza.attr.type == "get" then
local username = jid_split(stanza.attr.to) or origin.username;
local luser, lhost = jid_split(stanza.attr.to);
-- if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
if stanza.attr.to then
local seconds, text = "-1", "";
local hash = "";
if map[username] then
mseconds = map[username].s or 0;
-- module:log("info", "mseconds: %s", mseconds);
if (mseconds == 0) then
seconds=0;
map[username].s=os.time();
else
-- module:log("info", "difftime from map for user %s is %s", username, dumpTable(map[username]));
-- module:log("info", "difftime will be called for %s == type %s", map[username].s, type(map[username].s));
seconds = tostring(os.difftime(os.time(), map[username].s));
-- module:log("info","using2 for %s @ %s => %s --- stored was %s", luser, lhost, seconds, map[username].s);
-- text = map[username].s;
end
end
if luser and lhost then
@@ -360,17 +255,13 @@ module:hook("iq/bare/jabber:iq:last:query", function(event)
local all_hibernated = true;
for x,s in pairs(user_sessions.sessions) do
if (s.hibernating) then
-- module:log("info", "user_sessions s.hibernated %s is %s", s, s.hibernating);
if (s.hibernating > latest_hibernated_stamp) then
latest_hibernated_stamp = s.hibernating;
end
else
-- module:log("info", "user_sessions %s is active", s);
all_hibernated = false;
end
end
-- module:log("info","latest_hibernated_stamp is %s", latest_hibernated_stamp);
-- module:log("info","all_hibernated is %s", all_hibernated);
if (latest_hibernated_stamp > 0 and all_hibernated) then
seconds = os.difftime(os.time(), latest_hibernated_stamp)
else
@@ -379,26 +270,25 @@ module:hook("iq/bare/jabber:iq:last:query", function(event)
end
end
if (seconds == "-1") then
local stored_seconds = activity_store:get(luser, "last") or -1;
local bare_jid = luser.."@"..lhost;
local stored_seconds = cache_get(bare_jid);
if stored_seconds == nil then
stored_seconds = activity_store:get(luser, "last") or -1;
if stored_seconds ~= -1 then
cache_set(bare_jid, tonumber(stored_seconds));
end
end
if (stored_seconds ~= 0) then
-- -- module:log("info", "read persistant lastactivity for %s as %s", luser, dumpTable(stored_seconds));
if (stored_seconds == -1) then
seconds = -1
else
seconds = tostring(os.difftime(os.time(), stored_seconds));
end;
-- module:log("info","using4 for %s @ %s => %s", luser, lhost, seconds);
if event.origin.username ~= nil then
map[event.origin.username] = {s = seconds, t = text};
end
else
-- local now = os.time();
-- module:log("info", "no stored data for %s @ %s", luser, lhost);
-- local upers = activity_store:set(luser, "last", now);
-- seconds = 0;
end
end
-- module:log("info","sending reply with: seconds=%s text=%s", seconds, text);
origin.send(st.reply(stanza):tag('query', {xmlns='jabber:iq:last', seconds=tostring(seconds)}):text(tostring(text)));
else
origin.send(st.error_reply(stanza, 'auth', 'forbidden'));