Files
vnctalk-prosody/vnctalk/mod_vnc_lastactivity.lua
Stefan-Sangerandmarge e98f9cf4c9 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>
2026-07-16 04:26:15 +00:00

306 lines
10 KiB
Lua

-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local st = require "util.stanza";
local jid_bare = require "util.jid".bare;
local jid_split = require "util.jid".split;
local my_host = module:get_host();
local remote_act_cache = {};
local remote_act_sessions = {};
local activity_store = module:open_store("activity", "map");
local remote_activity_store = module:open_store("remote_activity", "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
for n in pairs(tbl) do
getN = getN + 1
end
return getN
end
module:add_feature("jabber:iq:last");
module:add_feature("jabber:iq:batch");
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);
cache_set(session.username.."@"..my_host, 0);
end
end, 10);
module:hook("presence/bare", function(event)
local origin, stanza = event.origin, event.stanza;
local typ = stanza.attr.type;
if origin.type == "s2sin" and ( typ == nil or typ == "unavailable" ) then
local contact_full = stanza.attr.from;
local contact_bare = jid_bare(contact_full);
local contact_user, contact_host, contact_res = jid_split(contact_full);
local contact_sessions = remote_act_sessions[contact_bare] or {};
if typ == "unavailable" then -- set current time in cache
local t = os.time();
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);
if ( contact_session_count == 0 ) then
remote_act_sessions[contact_bare] = nil;
remote_act_cache[contact_bare] = t;
remote_activity_store:set(contact_bare, "last", t);
cache_set(contact_bare, t);
end
end
else
-- set 0 (online) in cache
local contact_sessions = remote_act_sessions[contact_bare] or {};
contact_sessions[contact_full] = 1;
remote_act_sessions[contact_bare] = contact_sessions;
end
end
end, 10);
module:hook("pre-presence/bare", function(event)
local stanza = event.stanza;
if not(stanza.attr.to) and stanza.attr.type == "unavailable" then
local s = os.time();
local t = stanza:get_child_text("status");
map[event.origin.username] = {s = s, t = t};
local lastsession = true;
local luser, lhost = jid_split(stanza.attr.from);
if luser and lhost then
local user_sessions = hosts[lhost] and hosts[lhost].sessions[luser];
if user_sessions then
lastsession = false;
end
end
if lastsession then
local pers = activity_store:set(event.origin.username, "last", s);
cache_set(event.origin.username.."@"..my_host, s);
end
end
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
local querychild = stanza:get_child("query", "jabber:iq:batch");
local reply = st.reply(stanza):tag('query', {xmlns='jabber:iq:last'});
for ajid in querychild:childtags("jid") do
local abjid = ajid:get_text();
local username = jid_split(abjid);
local luser, lhost = jid_split(abjid);
local seconds, text = "-1", "";
if map[username] then
local mt = tonumber(map[username].s);
seconds = tostring(os.difftime(os.time(), mt));
text = map[username].s;
end
if luser and lhost then
local sessions_avail = false;
if (hosts[lhost]) then
if (hosts[lhost].sessions) then
sessions_avail = true;
end
end
if sessions_avail then
local user_sessions = hosts[lhost] and hosts[lhost].sessions[luser];
local latest_hibernated_stamp = 0;
local all_hibernated = true;
if user_sessions then
for x,s in pairs(user_sessions.sessions) do
if (s.hibernating) then
if (s.hibernating > latest_hibernated_stamp) then
latest_hibernated_stamp = s.hibernating;
end
else
all_hibernated = false;
end
end
if (latest_hibernated_stamp > 0 and all_hibernated) then
seconds = os.difftime(os.time(), latest_hibernated_stamp)
else
seconds = 0
end
end
end
if not (prosody.hosts[lhost]) then
local contact_bare = luser.."@"..lhost;
if remote_act_sessions[contact_bare] then
local contact_sessions = getTableLng(remote_act_sessions[contact_bare]);
if ( contact_sessions == 0 ) then
seconds = tostring(os.difftime(os.time(), remote_act_cache[contact_bare]));
else
seconds = 0;
end
else
if remote_act_cache[contact_bare] then
if ( remote_act_cache[contact_bare] > 0) then
seconds = tostring(os.difftime(os.time(), remote_act_cache[contact_bare]));
else
seconds = 0;
end
end
end
end
end
if (seconds == "-1") then
if (prosody.hosts[lhost]) then
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
map[event.origin.username] = {s = "0", t = text};
else
seconds = tostring(os.difftime(os.time(), nseconds));
map[event.origin.username] = {s = nseconds, t = text};
end
end
else
if (luser ~= nil) then
local contact_bare = luser.."@"..lhost;
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
map[event.origin.username] = {s = "0", t = text};
else
seconds = tostring(os.difftime(os.time(), nseconds));
map[event.origin.username] = {s = nseconds, t = text};
end
end
end
end
end
local rtag = st.stanza("result", { jid=abjid; seconds=tostring(seconds); });
reply:add_child(rtag);
end
origin.send(reply);
end
return true;
end);
module:hook("iq/bare/jabber:iq:last:query", function(event)
local origin, stanza = event.origin, event.stanza;
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 stanza.attr.to then
local seconds, text = "-1", "";
if map[username] then
mseconds = map[username].s or 0;
if (mseconds == 0) then
seconds=0;
map[username].s=os.time();
else
seconds = tostring(os.difftime(os.time(), map[username].s));
end
end
if luser and lhost then
local user_sessions = hosts[lhost] and hosts[lhost].sessions[luser];
if user_sessions then
local latest_hibernated_stamp = 0;
local all_hibernated = true;
for x,s in pairs(user_sessions.sessions) do
if (s.hibernating) then
if (s.hibernating > latest_hibernated_stamp) then
latest_hibernated_stamp = s.hibernating;
end
else
all_hibernated = false;
end
end
if (latest_hibernated_stamp > 0 and all_hibernated) then
seconds = os.difftime(os.time(), latest_hibernated_stamp)
else
seconds = 0
end
end
end
if (seconds == "-1") then
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
if (stored_seconds == -1) then
seconds = -1
else
seconds = tostring(os.difftime(os.time(), stored_seconds));
end;
if event.origin.username ~= nil then
map[event.origin.username] = {s = seconds, t = text};
end
end
end
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'));
end
return true;
end
end);
module.save = function()
return {map = map};
end
module.restore = function(data)
map = data.map or {};
end