Convert telex to store mesages in a more scalable way.

I was severely worried about `mbox` and `spool` becoming way
too large way too quick - We just can't store the entire array
of full messages in there all the time.

Instead, both those arrays become simple string arrays of `msgid`
objects, which are nothing more but simple numeric strings. Each
message can now live in mod_storage which should scale a lot better
and make player mboxes a lot smaller - it can now easily hold a few
hundred messages without growing much.

This means all the messages are in mod_storage. We can at a later
point perhaps consider compression.

There's compat code to read old mbox formats, but not spool. Thus
the server spool must be empty when this code is deployed.
This commit is contained in:
Auke Kok 2019-08-21 22:19:32 -07:00
parent 4f6f1ce9c3
commit 7a69c6c733
2 changed files with 139 additions and 46 deletions

View File

@ -42,8 +42,7 @@ telex = {}
- reply to a message - reply to a message
- delete a message - delete a message
message table format: msg = {
{
"from" = <string>, "from" = <string>,
"to" = <string>, "to" = <string>,
"subject" = <string>, "subject" = <string>,
@ -52,6 +51,18 @@ telex = {}
"age" = <int> "age" = <int>
} }
mbox = {
[1] = msgid,
[2] = msgid,
...
}
spool = {
[1] = msgid,
[2] = msgid,
...
}
Spool/mbox format: array of messages Spool/mbox format: array of messages
]]-- ]]--
@ -59,12 +70,45 @@ telex = {}
local S = minetest.get_mod_storage() local S = minetest.get_mod_storage()
assert(S) assert(S)
-- helper functions for handling msgid
-- allocate a new msgid str
function telex.msgid()
local msgid = S:get_int("msgid") + 1
S:set_int("msgid", msgid)
return "m" .. tostring(msgid)
end
-- get
function telex.get_msg(msgid)
if not msgid then
return nil
end
local msg = S:get_string(msgid)
if msg then
return telex.decode(msg)
else
return nil
end
end
-- set
function telex.save_msg(msgid, msg)
S:set_string(msgid, telex.encode(msg))
end
-- remove
function telex.remove_msg(msgid)
S:set_string(msgid, "")
end
-- returns an array of strings -- returns an array of strings
function telex.list(player) function telex.list(player)
local pmeta = player:get_meta() local pmeta = player:get_meta()
local mbox = telex.decode(pmeta:get_string("telex_mbox")) local mbox = telex.decode(pmeta:get_string("telex_mbox"))
local list = {} local list = {}
for n, msg in pairs(mbox) do for n, msgid in pairs(mbox) do
local msg = telex.get_msg(msgid)
local unread = "!" local unread = "!"
if msg.read == 1 then if msg.read == 1 then
unread = " " unread = " "
@ -81,7 +125,10 @@ function telex.get(player, no)
if not mbox then if not mbox then
return { "You have no messages." } return { "You have no messages." }
end end
local msg = mbox[no]
local msgid = mbox[no]
local msg = telex.get_msg(msgid)
if not msg then if not msg then
return { "No such message exists." } return { "No such message exists." }
end end
@ -96,7 +143,8 @@ function telex.read(player, no)
if not mbox then if not mbox then
return { "You have no messages." } return { "You have no messages." }
end end
local msg = mbox[no] local msgid = mbox[no]
local msg = telex.get_msg(msgid)
if not msg then if not msg then
return { "No such message exists." } return { "No such message exists." }
end end
@ -112,7 +160,8 @@ function telex.read(player, no)
-- mark as read and store -- mark as read and store
msg.read = 1 msg.read = 1
mbox[no] = msg telex.save_msg(msgid, msg)
mbox[no] = msgid
pmeta:set_string("telex_mbox", telex.encode(mbox)) pmeta:set_string("telex_mbox", telex.encode(mbox))
return list return list
@ -125,68 +174,94 @@ function telex.delete(player, no)
if not mbox then if not mbox then
return { "You have no messages." } return { "You have no messages." }
end end
local msg = mbox[no] local msgid = mbox[no]
local msg = telex.get_msg(msgid)
if not msg then if not msg then
return { "No such message exists." } return { "No such message exists." }
end end
table.remove(mbox, no) table.remove(mbox, no)
telex.remove_msg(msgid)
pmeta:set_string("telex_mbox", telex.encode(mbox)) pmeta:set_string("telex_mbox", telex.encode(mbox))
return { "Message deleted." } return { "Message deleted." }
end end
-- returns nothing -- external should use `send`, internal uses `deliver`
function telex.deliver(message) function telex.send(msg)
assert(message.from) assert(msg.from)
assert(message.to) assert(msg.to)
assert(message.subject) assert(msg.subject)
assert(message.content) assert(msg.content)
local player = minetest.get_player_by_name(message.to) local msgid = telex.msgid()
telex.save_msg(msgid, msg)
telex.deliver(msgid, msg)
end
-- returns nothing
function telex.deliver(msgid, msg)
-- msg is optional
if not msg then
msg = telex.get_msg(msgid)
end
local player = minetest.get_player_by_name(msg.to)
if player then if player then
-- remove age, no longer needed
msg.age = nil
telex.save_msg(msgid, msg)
-- retrieve inbox -- retrieve inbox
local pmeta = player:get_meta() local pmeta = player:get_meta()
local mbox = telex.decode(pmeta:get_string("telex_mbox")) local mbox = telex.decode(pmeta:get_string("telex_mbox"))
table.insert(mbox, message) table.insert(mbox, msgid)
pmeta:set_string("telex_mbox", telex.encode(mbox)) pmeta:set_string("telex_mbox", telex.encode(mbox))
minetest.chat_send_player(message.to, "You have a new message from <" .. minetest.chat_send_player(msg.to, "You have a new message from <" ..
message.from .. ">, use a terminal to read your messages"); msg.from .. ">, use a terminal to read your messages");
minetest.log("action", "delivered a message from <" .. message.from .. "> to <" .. message.to .. ">") minetest.log("action", "delivered a message from <" .. msg.from .. "> to <" .. msg.to .. ">")
else else
-- append to spool -- append to spool
message.age = 7 * 86400 -- 7 days max in spool msg.age = 7 * 86400 -- 7 days max in spool
telex.save_msg(msgid, msg)
local spool = telex.decode(S:get_string("telex_spool")) local spool = telex.decode(S:get_string("telex_spool"))
table.insert(spool, message) table.insert(spool, msgid)
S:set_string("telex_spool", telex.encode(spool)) S:set_string("telex_spool", telex.encode(spool))
minetest.log("action", "spooled a message from <" .. message.from .. "> to <" .. message.to .. ">") minetest.log("action", "spooled a message from <" .. msg.from .. "> to <" .. msg.to .. ">")
announce.admins("spooled a message from <" .. message.from .. "> to <" .. message.to .. ">") announce.admins("spooled a message from <" .. msg.from .. "> to <" .. msg.to .. ">")
end end
end end
-- returns nothing -- returns nothing
function telex.retour(message) function telex.retour(msgid)
if message.from == "MAILER-DEAMON" then local msg = telex.get_msg(msgid)
if msg.from == "MAILER-DEAMON" then
-- discard, we tried hard enough! -- discard, we tried hard enough!
minetest.log("action", "discarded an expired spool message from <" .. message.from .. "> to <" .. message.to .. ">") minetest.log("action", "discarded an expired spool message from <" .. msg.from .. "> to <" .. msg.to .. ">")
announce.admins("discarded an expired spool message from <" .. message.from .. "> to <" .. message.to .. ">") announce.admins("discarded an expired spool message from <" .. msg.from .. "> to <" .. msg.to .. ">")
return return
end end
local to = message.to local to = msg.to
local from = message.from local from = msg.from
message.to = from msg.to = from
message.from = "MAILER-DAEMON" msg.from = "MAILER-DAEMON"
message.subject = "UNDELIVERABLE: " .. message.subject msg.subject = "UNDELIVERABLE: " .. msg.subject
message.age = 86400 * 30 -- return mail for 30 days max, then discard. msg.age = 86400 * 30 -- return mail for 30 days max, then discard.
table.insert(message.content, 1, "Your message to <" .. to .. "> was unable to be delivered.") table.insert(msg.content, 1, "Your message to <" .. to .. "> was unable to be delivered.")
table.insert(message.content, 2, "================== ORIGINAL MESSAGE BELOW ================") table.insert(msg.content, 2, "================== ORIGINAL MESSAGE BELOW ================")
telex.deliver(message)
-- remove the old msg, it's no longer in spool
telex.remove_msg(msgid)
-- allocate a new ID for the return msg
local msgid2 = telex.msgid()
telex.save_msg(msgid2, msg)
telex.deliver(msgid2, msg)
minetest.log("action", "returned a message from <" .. from .. "> back to <" .. to .. ">") minetest.log("action", "returned a message from <" .. from .. "> back to <" .. to .. ">")
announce.admins("returned a message from <" .. from .. "> back to <" .. to .. ">") announce.admins("returned a message from <" .. from .. "> back to <" .. to .. ">")
end end
-- returns message/mbox -- returns message/mbox --FIXME compress/decompress
function telex.decode(digest) function telex.decode(digest)
if not digest or digest == "" then if not digest or digest == "" then
return {} return {}
@ -205,6 +280,22 @@ function telex.encode(message_or_mbox)
end end
minetest.register_on_joinplayer(function(player) minetest.register_on_joinplayer(function(player)
-- convert old mbox
local pmeta = player:get_meta()
local mbox = telex.decode(pmeta:get_string("telex_mbox"))
local save = false
for k, v in pairs(mbox) do
if type(v) == "table" then
local msgid = telex.msgid()
telex.save_msg(msgid, v)
mbox[k] = msgid
save = true
end
end
if save then
pmeta:set_string("telex_mbox", telex.encode(mbox))
end
-- check the spool for messages for `player` -- check the spool for messages for `player`
local spool = telex.decode(S:get_string("telex_spool")) local spool = telex.decode(S:get_string("telex_spool"))
local name = player:get_player_name() local name = player:get_player_name()
@ -212,10 +303,11 @@ minetest.register_on_joinplayer(function(player)
-- deliver items for this player -- deliver items for this player
local nos = {} local nos = {}
local del = {} local del = {}
for k, msg in pairs(spool) do for k, msgid in pairs(spool) do
local msg = telex.get_msg(msgid)
if msg.to == name then if msg.to == name then
table.insert(nos, k) table.insert(nos, k)
table.insert(del, msg) table.insert(del, msgid)
end end
end end
@ -226,14 +318,13 @@ minetest.register_on_joinplayer(function(player)
S:set_string("telex_spool", telex.encode(spool)) S:set_string("telex_spool", telex.encode(spool))
-- now deliver them to player -- now deliver them to player
for _, msg in pairs(del) do for _, msgid in pairs(del) do
telex.deliver(msg) telex.deliver(msgid)
end end
if #nos > 0 then if #nos > 0 then
--FIXME minetest.after() --FIXME minetest.after()
minetest.chat_send_player(name, "You have " .. #nos .. " new message(s), use a terminal to read your messages"); minetest.chat_send_player(name, "You have " .. #nos .. " new message(s), use a terminal to read your messages");
end end
end) end)
@ -241,12 +332,14 @@ function telex.process_spool()
local spool = telex.decode(S:get_string("telex_spool")) local spool = telex.decode(S:get_string("telex_spool"))
local old = {} local old = {}
local del = {} local del = {}
for k, msg in pairs(spool) do --FIXME BAD for k, msgid in pairs(spool) do
local msg = telex.get_msg(msgid)
msg.age = msg.age - 3600 msg.age = msg.age - 3600
if msg.age < 0 then if msg.age < 0 then
old[#old + 1] = k old[#old + 1] = k
table.insert(del, msg) table.insert(del, msgid)
end end
telex.save_msg(msgid, msg)
end end
-- remove old msgs from spool -- remove old msgs from spool
@ -256,8 +349,8 @@ function telex.process_spool()
S:set_string("telex_spool", telex.encode(spool)) S:set_string("telex_spool", telex.encode(spool))
-- return the actual messages -- return the actual messages
for _, msg in pairs(del) do for _, msgid in pairs(del) do
telex.retour(msg) telex.retour(msgid)
end end
minetest.after(3600, telex.process_spool) minetest.after(3600, telex.process_spool)

View File

@ -292,7 +292,7 @@ term.commands = {
from = c.name, from = c.name,
content = string.split(text, "\n") content = string.split(text, "\n")
} }
telex.deliver(msg) telex.send(msg)
return output .. "\n" .. "Mail sent to <" .. p .. ">." return output .. "\n" .. "Mail sent to <" .. p .. ">."
elseif h == "read" then elseif h == "read" then