mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-10-15 14:49:58 +00:00
* smp server: limit by time the queues to export journal messages for
* pass queue/msg thresholds separately
* reset db connection on errors
* Revert "smp server: limit by time the queues to export journal messages for"
This reverts commit d3bc0cba4b.
* fix test compilation
* flag to expire messages
* improve test
* expire messages newer than quota
459 lines
13 KiB
Haskell
459 lines
13 KiB
Haskell
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE QuasiQuotes #-}
|
|
|
|
module Simplex.Messaging.Server.QueueStore.Postgres.Migrations where
|
|
|
|
import Data.List (sortOn)
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as T
|
|
import Simplex.Messaging.Agent.Store.Shared
|
|
import Text.RawString.QQ (r)
|
|
|
|
serverSchemaMigrations :: [(String, Text, Maybe Text)]
|
|
serverSchemaMigrations =
|
|
[ ("20250207_initial", m20250207_initial, Nothing),
|
|
("20250319_updated_index", m20250319_updated_index, Just down_m20250319_updated_index),
|
|
("20250320_short_links", m20250320_short_links, Just down_m20250320_short_links),
|
|
("20250514_service_certs", m20250514_service_certs, Just down_m20250514_service_certs),
|
|
("20250903_store_messages", m20250903_store_messages, Just down_m20250903_store_messages)
|
|
]
|
|
|
|
-- | The list of migrations in ascending order by date
|
|
serverMigrations :: [Migration]
|
|
serverMigrations = sortOn name $ map migration serverSchemaMigrations
|
|
where
|
|
migration (name, up, down) = Migration {name, up, down = down}
|
|
|
|
m20250207_initial :: Text
|
|
m20250207_initial =
|
|
T.pack
|
|
[r|
|
|
CREATE TABLE msg_queues(
|
|
recipient_id BYTEA NOT NULL,
|
|
recipient_key BYTEA NOT NULL,
|
|
rcv_dh_secret BYTEA NOT NULL,
|
|
sender_id BYTEA NOT NULL,
|
|
sender_key BYTEA,
|
|
snd_secure BOOLEAN NOT NULL,
|
|
notifier_id BYTEA,
|
|
notifier_key BYTEA,
|
|
rcv_ntf_dh_secret BYTEA,
|
|
status TEXT NOT NULL,
|
|
updated_at BIGINT,
|
|
deleted_at BIGINT,
|
|
PRIMARY KEY (recipient_id)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX idx_msg_queues_sender_id ON msg_queues(sender_id);
|
|
CREATE UNIQUE INDEX idx_msg_queues_notifier_id ON msg_queues(notifier_id);
|
|
CREATE INDEX idx_msg_queues_deleted_at ON msg_queues (deleted_at);
|
|
|]
|
|
|
|
m20250319_updated_index :: Text
|
|
m20250319_updated_index =
|
|
T.pack
|
|
[r|
|
|
DROP INDEX idx_msg_queues_deleted_at;
|
|
CREATE INDEX idx_msg_queues_updated_at ON msg_queues (deleted_at, updated_at);
|
|
|]
|
|
|
|
down_m20250319_updated_index :: Text
|
|
down_m20250319_updated_index =
|
|
T.pack
|
|
[r|
|
|
DROP INDEX idx_msg_queues_updated_at;
|
|
CREATE INDEX idx_msg_queues_deleted_at ON msg_queues (deleted_at);
|
|
|]
|
|
|
|
m20250320_short_links :: Text
|
|
m20250320_short_links =
|
|
T.pack
|
|
[r|
|
|
ALTER TABLE msg_queues
|
|
ADD COLUMN queue_mode TEXT,
|
|
ADD COLUMN link_id BYTEA,
|
|
ADD COLUMN fixed_data BYTEA,
|
|
ADD COLUMN user_data BYTEA;
|
|
|
|
UPDATE msg_queues SET queue_mode = 'M' WHERE snd_secure IS TRUE;
|
|
|
|
ALTER TABLE msg_queues DROP COLUMN snd_secure;
|
|
|
|
UPDATE msg_queues SET recipient_key = ('\x01'::BYTEA || chr(length(recipient_key))::BYTEA || recipient_key);
|
|
|
|
ALTER TABLE msg_queues RENAME COLUMN recipient_key TO recipient_keys;
|
|
|
|
CREATE UNIQUE INDEX idx_msg_queues_link_id ON msg_queues(link_id);
|
|
|]
|
|
|
|
down_m20250320_short_links :: Text
|
|
down_m20250320_short_links =
|
|
T.pack
|
|
[r|
|
|
ALTER TABLE msg_queues ADD COLUMN snd_secure BOOLEAN NOT NULL DEFAULT FALSE;
|
|
|
|
UPDATE msg_queues SET snd_secure = TRUE WHERE queue_mode = 'M';
|
|
|
|
DROP INDEX idx_msg_queues_link_id;
|
|
|
|
ALTER TABLE msg_queues
|
|
DROP COLUMN queue_mode,
|
|
DROP COLUMN link_id,
|
|
DROP COLUMN fixed_data,
|
|
DROP COLUMN user_data;
|
|
|
|
DO $$
|
|
DECLARE bad_id BYTEA;
|
|
BEGIN
|
|
SELECT recipient_id INTO bad_id
|
|
FROM msg_queues
|
|
WHERE get_byte(recipient_keys, 0) != 1
|
|
OR get_byte(recipient_keys, 1) != length(recipient_keys) - 2
|
|
LIMIT 1;
|
|
|
|
IF bad_id IS NOT NULL
|
|
THEN RAISE EXCEPTION 'Cannot downgrade: many keys or incorrect length in recipient_keys for %', encode(bad_id, 'base64');
|
|
END IF;
|
|
END;
|
|
$$;
|
|
|
|
UPDATE msg_queues SET recipient_keys = substring(recipient_keys from 3);
|
|
|
|
ALTER TABLE msg_queues RENAME COLUMN recipient_keys TO recipient_key;
|
|
|]
|
|
|
|
m20250514_service_certs :: Text
|
|
m20250514_service_certs =
|
|
T.pack
|
|
[r|
|
|
CREATE TABLE services(
|
|
service_id BYTEA NOT NULL,
|
|
service_role TEXT NOT NULL,
|
|
service_cert BYTEA NOT NULL,
|
|
service_cert_hash BYTEA NOT NULL UNIQUE,
|
|
created_at BIGINT NOT NULL,
|
|
PRIMARY KEY (service_id)
|
|
);
|
|
|
|
CREATE INDEX idx_services_service_role ON services(service_role);
|
|
|
|
ALTER TABLE msg_queues
|
|
ADD COLUMN rcv_service_id BYTEA REFERENCES services(service_id) ON DELETE SET NULL ON UPDATE RESTRICT,
|
|
ADD COLUMN ntf_service_id BYTEA REFERENCES services(service_id) ON DELETE SET NULL ON UPDATE RESTRICT;
|
|
|
|
CREATE INDEX idx_msg_queues_rcv_service_id ON msg_queues(rcv_service_id, deleted_at);
|
|
CREATE INDEX idx_msg_queues_ntf_service_id ON msg_queues(ntf_service_id, deleted_at);
|
|
|]
|
|
|
|
down_m20250514_service_certs :: Text
|
|
down_m20250514_service_certs =
|
|
T.pack
|
|
[r|
|
|
DROP INDEX idx_msg_queues_rcv_service_id;
|
|
DROP INDEX idx_msg_queues_ntf_service_id;
|
|
|
|
ALTER TABLE msg_queues
|
|
DROP COLUMN rcv_service_id,
|
|
DROP COLUMN ntf_service_id;
|
|
|
|
DROP INDEX idx_services_service_role;
|
|
|
|
DROP TABLE services;
|
|
|]
|
|
|
|
m20250903_store_messages :: Text
|
|
m20250903_store_messages =
|
|
T.pack
|
|
[r|
|
|
CREATE TABLE messages(
|
|
message_id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
|
recipient_id BYTEA NOT NULL REFERENCES msg_queues ON DELETE CASCADE ON UPDATE RESTRICT,
|
|
msg_id BYTEA NOT NULL,
|
|
msg_ts BIGINT NOT NULL,
|
|
msg_quota BOOLEAN NOT NULL,
|
|
msg_ntf_flag BOOLEAN NOT NULL,
|
|
msg_body BYTEA NOT NULL
|
|
);
|
|
|
|
ALTER TABLE msg_queues
|
|
ADD COLUMN msg_can_write BOOLEAN NOT NULL DEFAULT TRUE,
|
|
ADD COLUMN msg_queue_expire BOOLEAN NOT NULL DEFAULT FALSE,
|
|
ADD COLUMN msg_queue_size BIGINT NOT NULL DEFAULT 0;
|
|
|
|
CREATE INDEX idx_messages_recipient_id_message_id ON messages (recipient_id, message_id);
|
|
CREATE INDEX idx_messages_recipient_id_msg_ts on messages(recipient_id, msg_ts);
|
|
CREATE INDEX idx_messages_recipient_id_msg_quota on messages(recipient_id, msg_quota);
|
|
|
|
DROP INDEX idx_msg_queues_updated_at;
|
|
CREATE INDEX idx_msg_queues_updated_at_recipient_id ON msg_queues (deleted_at, updated_at, msg_queue_expire, recipient_id);
|
|
|
|
CREATE FUNCTION write_message(
|
|
p_recipient_id BYTEA,
|
|
p_msg_id BYTEA,
|
|
p_msg_ts BIGINT,
|
|
p_msg_quota BOOLEAN,
|
|
p_msg_ntf_flag BOOLEAN,
|
|
p_msg_body BYTEA,
|
|
p_quota INT
|
|
)
|
|
RETURNS TABLE (quota_written BOOLEAN, was_empty BOOLEAN)
|
|
LANGUAGE plpgsql AS $$
|
|
DECLARE
|
|
q_can_write BOOLEAN;
|
|
q_size BIGINT;
|
|
BEGIN
|
|
SELECT msg_can_write, msg_queue_size INTO q_can_write, q_size
|
|
FROM msg_queues
|
|
WHERE recipient_id = p_recipient_id AND deleted_at IS NULL
|
|
FOR UPDATE;
|
|
|
|
IF q_can_write OR q_size = 0 THEN
|
|
quota_written := p_msg_quota OR q_size >= p_quota;
|
|
was_empty := q_size = 0;
|
|
|
|
INSERT INTO messages(recipient_id, msg_id, msg_ts, msg_quota, msg_ntf_flag, msg_body)
|
|
VALUES (p_recipient_id, p_msg_id, p_msg_ts, quota_written, p_msg_ntf_flag AND NOT quota_written, CASE WHEN quota_written THEN '' :: BYTEA ELSE p_msg_body END);
|
|
|
|
UPDATE msg_queues
|
|
SET msg_can_write = NOT quota_written,
|
|
msg_queue_expire = TRUE,
|
|
msg_queue_size = msg_queue_size + 1
|
|
WHERE recipient_id = p_recipient_id;
|
|
|
|
RETURN QUERY VALUES (quota_written, was_empty);
|
|
END IF;
|
|
END;
|
|
$$;
|
|
|
|
CREATE FUNCTION try_del_msg(p_recipient_id BYTEA, p_msg_id BYTEA)
|
|
RETURNS TABLE (r_msg_id BYTEA, r_msg_ts BIGINT, r_msg_quota BOOLEAN, r_msg_ntf_flag BOOLEAN, r_msg_body BYTEA)
|
|
LANGUAGE plpgsql AS $$
|
|
DECLARE
|
|
q_size BIGINT;
|
|
msg RECORD;
|
|
BEGIN
|
|
SELECT msg_queue_size INTO q_size
|
|
FROM msg_queues
|
|
WHERE recipient_id = p_recipient_id AND deleted_at IS NULL
|
|
FOR UPDATE;
|
|
|
|
IF NOT FOUND THEN
|
|
RETURN;
|
|
END IF;
|
|
|
|
SELECT message_id, msg_id, msg_ts, msg_quota, msg_ntf_flag, msg_body
|
|
INTO msg
|
|
FROM messages
|
|
WHERE recipient_id = p_recipient_id
|
|
ORDER BY message_id ASC LIMIT 1;
|
|
|
|
IF NOT FOUND THEN
|
|
IF q_size != 0 THEN
|
|
UPDATE msg_queues
|
|
SET msg_can_write = TRUE,
|
|
msg_queue_expire = FALSE,
|
|
msg_queue_size = 0
|
|
WHERE recipient_id = p_recipient_id;
|
|
END IF;
|
|
RETURN;
|
|
END IF;
|
|
|
|
IF msg.msg_id = p_msg_id THEN
|
|
DELETE FROM messages WHERE message_id = msg.message_id;
|
|
IF FOUND THEN
|
|
UPDATE msg_queues
|
|
SET msg_can_write = msg_can_write OR msg_queue_size <= 1,
|
|
msg_queue_expire = msg_queue_size > 1,
|
|
msg_queue_size = GREATEST(msg_queue_size - 1, 0)
|
|
WHERE recipient_id = p_recipient_id;
|
|
RETURN QUERY VALUES (msg.msg_id, msg.msg_ts, msg.msg_quota, msg.msg_ntf_flag, msg.msg_body);
|
|
END IF;
|
|
END IF;
|
|
END;
|
|
$$;
|
|
|
|
CREATE FUNCTION try_del_peek_msg(p_recipient_id BYTEA, p_msg_id BYTEA)
|
|
RETURNS TABLE (r_msg_id BYTEA, r_msg_ts BIGINT, r_msg_quota BOOLEAN, r_msg_ntf_flag BOOLEAN, r_msg_body BYTEA)
|
|
LANGUAGE plpgsql AS $$
|
|
DECLARE
|
|
q_size BIGINT;
|
|
msg RECORD;
|
|
msg_deleted BOOLEAN;
|
|
BEGIN
|
|
SELECT msg_queue_size INTO q_size
|
|
FROM msg_queues
|
|
WHERE recipient_id = p_recipient_id AND deleted_at IS NULL
|
|
FOR UPDATE;
|
|
|
|
IF NOT FOUND THEN
|
|
RETURN;
|
|
END IF;
|
|
|
|
SELECT message_id, msg_id, msg_ts, msg_quota, msg_ntf_flag, msg_body
|
|
INTO msg
|
|
FROM messages
|
|
WHERE recipient_id = p_recipient_id
|
|
ORDER BY message_id ASC LIMIT 1;
|
|
|
|
IF NOT FOUND THEN
|
|
IF q_size != 0 THEN
|
|
UPDATE msg_queues
|
|
SET msg_can_write = TRUE,
|
|
msg_queue_expire = FALSE,
|
|
msg_queue_size = 0
|
|
WHERE recipient_id = p_recipient_id;
|
|
END IF;
|
|
RETURN;
|
|
END IF;
|
|
|
|
IF msg.msg_id = p_msg_id THEN
|
|
DELETE FROM messages WHERE message_id = msg.message_id;
|
|
|
|
msg_deleted := FOUND;
|
|
IF msg_deleted THEN
|
|
RETURN QUERY VALUES (msg.msg_id, msg.msg_ts, msg.msg_quota, msg.msg_ntf_flag, msg.msg_body);
|
|
END IF;
|
|
|
|
SELECT msg_id, msg_ts, msg_quota, msg_ntf_flag, msg_body
|
|
INTO msg
|
|
FROM messages
|
|
WHERE recipient_id = p_recipient_id
|
|
ORDER BY message_id ASC LIMIT 1;
|
|
|
|
IF FOUND THEN
|
|
RETURN QUERY VALUES (msg.msg_id, msg.msg_ts, msg.msg_quota, msg.msg_ntf_flag, msg.msg_body);
|
|
IF msg_deleted THEN
|
|
UPDATE msg_queues
|
|
SET msg_can_write = msg_can_write OR msg_queue_size <= 1,
|
|
msg_queue_expire = msg_queue_size > 1,
|
|
msg_queue_size = GREATEST(msg_queue_size - 1, 0)
|
|
WHERE recipient_id = p_recipient_id;
|
|
END IF;
|
|
ELSIF msg_deleted OR q_size != 0 THEN
|
|
UPDATE msg_queues
|
|
SET msg_can_write = TRUE,
|
|
msg_queue_expire = FALSE,
|
|
msg_queue_size = 0
|
|
WHERE recipient_id = p_recipient_id;
|
|
END IF;
|
|
ELSE
|
|
RETURN QUERY VALUES (msg.msg_id, msg.msg_ts, msg.msg_quota, msg.msg_ntf_flag, msg.msg_body);
|
|
END IF;
|
|
END;
|
|
$$;
|
|
|
|
CREATE FUNCTION delete_expired_msgs(p_recipient_id BYTEA, p_old_ts BIGINT) RETURNS BIGINT
|
|
LANGUAGE plpgsql AS $$
|
|
DECLARE
|
|
q_size BIGINT;
|
|
keep_min_id BIGINT;
|
|
del_count BIGINT;
|
|
BEGIN
|
|
SELECT msg_queue_size INTO q_size
|
|
FROM msg_queues
|
|
WHERE recipient_id = p_recipient_id AND deleted_at IS NULL
|
|
FOR UPDATE SKIP LOCKED;
|
|
|
|
IF NOT FOUND OR q_size = 0 THEN
|
|
RETURN 0;
|
|
END IF;
|
|
|
|
SELECT MIN(message_id) INTO keep_min_id
|
|
FROM messages WHERE recipient_id = p_recipient_id AND msg_ts >= p_old_ts AND msg_quota = FALSE;
|
|
|
|
IF keep_min_id IS NULL THEN
|
|
DELETE FROM messages WHERE recipient_id = p_recipient_id AND msg_quota = FALSE;
|
|
ELSE
|
|
DELETE FROM messages WHERE recipient_id = p_recipient_id AND message_id < keep_min_id AND msg_quota = FALSE;
|
|
END IF;
|
|
|
|
GET DIAGNOSTICS del_count = ROW_COUNT;
|
|
IF del_count > 0 THEN
|
|
UPDATE msg_queues
|
|
SET msg_can_write = msg_can_write OR msg_queue_size <= del_count,
|
|
msg_queue_expire = msg_queue_size > del_count AND keep_min_id IS NOT NULL,
|
|
msg_queue_size = GREATEST(msg_queue_size - del_count, 0)
|
|
WHERE recipient_id = p_recipient_id;
|
|
END IF;
|
|
RETURN del_count;
|
|
END;
|
|
$$;
|
|
|
|
CREATE PROCEDURE expire_old_messages(
|
|
p_old_queue BIGINT,
|
|
p_old_ts BIGINT,
|
|
batch_size INT,
|
|
OUT r_expired_msgs_count BIGINT,
|
|
OUT r_stored_msgs_count BIGINT,
|
|
OUT r_stored_queues BIGINT
|
|
)
|
|
LANGUAGE plpgsql AS $$
|
|
DECLARE
|
|
rids BYTEA[];
|
|
rid BYTEA;
|
|
last_rid BYTEA := '\x';
|
|
del_count BIGINT;
|
|
total_deleted BIGINT := 0;
|
|
BEGIN
|
|
LOOP
|
|
SELECT array_agg(recipient_id)
|
|
INTO rids
|
|
FROM (
|
|
SELECT recipient_id
|
|
FROM msg_queues
|
|
WHERE deleted_at IS NULL
|
|
AND updated_at > p_old_queue
|
|
AND msg_queue_expire = TRUE
|
|
AND recipient_id > last_rid
|
|
ORDER BY recipient_id ASC
|
|
LIMIT batch_size
|
|
) qs;
|
|
|
|
EXIT WHEN rids IS NULL OR cardinality(rids) = 0;
|
|
|
|
FOREACH rid IN ARRAY rids
|
|
LOOP
|
|
BEGIN
|
|
del_count := delete_expired_msgs(rid, p_old_ts);
|
|
total_deleted := total_deleted + del_count;
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RAISE WARNING 'STORE, expire_old_messages, error expiring queue %: %', encode(rid, 'base64'), SQLERRM;
|
|
CONTINUE;
|
|
END;
|
|
COMMIT;
|
|
END LOOP;
|
|
last_rid := rids[cardinality(rids)];
|
|
END LOOP;
|
|
|
|
r_expired_msgs_count := total_deleted;
|
|
r_stored_msgs_count := (SELECT COUNT(1) FROM messages);
|
|
r_stored_queues := (SELECT COUNT(1) FROM msg_queues WHERE deleted_at IS NULL);
|
|
END;
|
|
$$;
|
|
|]
|
|
|
|
down_m20250903_store_messages :: Text
|
|
down_m20250903_store_messages =
|
|
T.pack
|
|
[r|
|
|
DROP FUNCTION write_message;
|
|
DROP FUNCTION try_del_msg;
|
|
DROP FUNCTION try_del_peek_msg;
|
|
DROP FUNCTION delete_expired_msgs;
|
|
DROP PROCEDURE expire_old_messages;
|
|
|
|
DROP INDEX idx_msg_queues_updated_at_recipient_id;
|
|
CREATE INDEX idx_msg_queues_updated_at ON msg_queues (deleted_at, updated_at);
|
|
|
|
DROP INDEX idx_messages_recipient_id_message_id;
|
|
DROP INDEX idx_messages_recipient_id_msg_ts;
|
|
DROP INDEX idx_messages_recipient_id_msg_quota;
|
|
|
|
ALTER TABLE msg_queues
|
|
DROP COLUMN msg_can_write,
|
|
DROP COLUMN msg_queue_expire,
|
|
DROP COLUMN msg_queue_size;
|
|
|
|
DROP TABLE messages;
|
|
|]
|