MQTTnet icon indicating copy to clipboard operation
MQTTnet copied to clipboard

[Server] Retained messages store optimized for database

Open Int32Overflow opened this issue 2 years ago • 5 comments

Describe the feature request

Currently, all retained messages are kept in the working memory, which can quickly become a problem with larger quantities. It would be a good idea to store these in a SQL database or in Redis, for example. However, the "interfaces" are currently very limited in order to implement such a concept.

Which project is your feature request related to?

  • Server

Describe the solution you'd like

In the method "FilterRetainedApplicationMessages" the retained messages are no longer handled in a for loop. The topic is now passed to a method in the new interface "IMqttClientSubscriptionsManager", which can return several retained messages as a result. This interface can of course be overwritten.

https://github.com/dotnet/MQTTnet/blob/2aa4219838a67e336ae4f5902d956c455bc48c11/Source/MQTTnet/Server/Internal/MqttClientSubscriptionsManager.cs#L401-L453

Int32Overflow avatar Jan 27 '23 12:01 Int32Overflow

I am wondering how to get the matching retained messages from the database. If the topic contains no wildcards ist might be working (or some usages of #). But how to get the proper set of messages from the database if the "+" wildcard is used?

chkr1011 avatar Jan 28 '23 11:01 chkr1011

For example cache only topics in ram and load payload and other stuff on match from database.

Int32Overflow avatar Jan 28 '23 12:01 Int32Overflow

Perhaps it is also possible to split the topics into individual segments and then use them like a tree path. PostgreSQL can map trees for example...

Int32Overflow avatar Jan 28 '23 15:01 Int32Overflow

How the retained message is looked up should probably remain an extension because different storage providers will have different capabilities. For SQL, the LIKE condition would probably work, with wildcard topic characters becoming % in the query. I.e. for topic house/1/room/+/temperature the query would be something like select * from retainedmessages where topic like 'house/1/room/%/temperature'. Case sensitivity and special characters (underscore) would need to be taken care of.

logicaloud avatar Jan 31 '23 02:01 logicaloud

@chkr1011 Can you give me an update?

I was able to successfully save and search the Mqtt topics in a Postgresql table. Postgresql supports a very fast way to search for topics that match a certain topic pattern with the help of index.

-- https://www.postgresql.org/docs/current/ltree.html

CREATE EXTENSION  ltree;

CREATE TABLE IF NOT EXISTS test (path ltree not null unique, message bytea);
CREATE INDEX path_gist_idx ON test USING GIST (path gist_ltree_ops(siglen=1000));
CREATE INDEX path_idx ON test USING BTREE (path);

INSERT INTO test VALUES ('Root1');
INSERT INTO test VALUES ('Root1.Child1.Child1.Child1');
INSERT INTO test VALUES ('Root1.Child1.Child1.Child1.A');
INSERT INTO test VALUES ('Root1.Child1.Child1.Child1.B');
INSERT INTO test VALUES ('Root1.Child1.Child1.Child2');
INSERT INTO test VALUES ('Root1.Child1.Child1.Child3');
INSERT INTO test VALUES ('Root1.Child1.Child1.Child3.A');
INSERT INTO test VALUES ('Root1.Child1.Child1.Child4');

INSERT INTO test VALUES ('Root1.Child1.Child2.Child1');
INSERT INTO test VALUES ('Root1.Child1.Child2.Child2');
INSERT INTO test VALUES ('Root1.Child1.Child2.Child3');
INSERT INTO test VALUES ('Root1.Child1.Child2.Child4');

INSERT INTO test VALUES ('Root1.Child1.Child3.Child1');
INSERT INTO test VALUES ('Root1.Child1.Child3.Child2');
INSERT INTO test VALUES ('Root1.Child1.Child3.Child3');
INSERT INTO test VALUES ('Root1.Child1.Child3.Child4');

INSERT INTO test VALUES ('Root1.Child1.Child4.Child1');
INSERT INTO test VALUES ('Root1.Child1.Child4.Child2');
INSERT INTO test VALUES ('Root1.Child1.Child4.Child3');
INSERT INTO test VALUES ('Root1.Child1.Child4.Child4');

INSERT INTO test VALUES ('Root1.Child1.Child5.Child1');
INSERT INTO test VALUES ('Root1.Child1.Child5.Child2');
INSERT INTO test VALUES ('Root1.Child1.Child5.Child3');
INSERT INTO test VALUES ('Root1.Child1.Child5.Child4');

INSERT INTO test VALUES ('Root1.Child1.Child6.Child1');
INSERT INTO test VALUES ('Root1.Child1.Child6.Child2');
INSERT INTO test VALUES ('Root1.Child1.Child6.Child3');
INSERT INTO test VALUES ('Root1.Child1.Child6.Child4');
-- Root1.Child2
INSERT INTO test VALUES ('Root1.Child2.Child1.Child1');
INSERT INTO test VALUES ('Root1.Child2.Child1.Child2');
INSERT INTO test VALUES ('Root1.Child2.Child1.Child3');
INSERT INTO test VALUES ('Root1.Child2.Child1.Child4');

INSERT INTO test VALUES ('Root1.Child2.Child2.Child1');
INSERT INTO test VALUES ('Root1.Child2.Child2.Child2');
INSERT INTO test VALUES ('Root1.Child2.Child2.Child3');
INSERT INTO test VALUES ('Root1.Child2.Child2.Child4');

INSERT INTO test VALUES ('Root1.Child2.Child3.Child1');
INSERT INTO test VALUES ('Root1.Child2.Child3.Child2');
INSERT INTO test VALUES ('Root1.Child2.Child3.Child3');
INSERT INTO test VALUES ('Root1.Child2.Child3.Child4');

INSERT INTO test VALUES ('Root1.Child2.Child4.Child1');
INSERT INTO test VALUES ('Root1.Child2.Child4.Child2');
INSERT INTO test VALUES ('Root1.Child2.Child4.Child3');
INSERT INTO test VALUES ('Root1.Child2.Child4.Child4');

INSERT INTO test VALUES ('Root1.Child2.Child5.Child1');
INSERT INTO test VALUES ('Root1.Child2.Child5.Child2');
INSERT INTO test VALUES ('Root1.Child2.Child5.Child3');
INSERT INTO test VALUES ('Root1.Child2.Child5.Child4');

INSERT INTO test VALUES ('Root1.Child2.Child6.Child1');
INSERT INTO test VALUES ('Root1.Child2.Child6.Child2');
INSERT INTO test VALUES ('Root1.Child2.Child6.Child3');
INSERT INTO test VALUES ('Root1.Child2.Child6.Child4');
-- Root1.Child3
INSERT INTO test VALUES ('Root1.Child3.Child1.Child1');
INSERT INTO test VALUES ('Root1.Child3.Child1.Child2');
INSERT INTO test VALUES ('Root1.Child3.Child1.Child3');
INSERT INTO test VALUES ('Root1.Child3.Child1.Child4');

INSERT INTO test VALUES ('Root1.Child3.Child2.Child1');
INSERT INTO test VALUES ('Root1.Child3.Child2.Child2');
INSERT INTO test VALUES ('Root1.Child3.Child2.Child3');
INSERT INTO test VALUES ('Root1.Child3.Child2.Child4');

INSERT INTO test VALUES ('Root1.Child3.Child3.Child1');
INSERT INTO test VALUES ('Root1.Child3.Child3.Child2');
INSERT INTO test VALUES ('Root1.Child3.Child3.Child3');
INSERT INTO test VALUES ('Root1.Child3.Child3.Child4');

INSERT INTO test VALUES ('Root1.Child3.Child4.Child1');
INSERT INTO test VALUES ('Root1.Child3.Child4.Child2');
INSERT INTO test VALUES ('Root1.Child3.Child4.Child3');
INSERT INTO test VALUES ('Root1.Child3.Child4.Child4');

INSERT INTO test VALUES ('Root1.Child3.Child5.Child1');
INSERT INTO test VALUES ('Root1.Child3.Child5.Child2');
INSERT INTO test VALUES ('Root1.Child3.Child5.Child3');
INSERT INTO test VALUES ('Root1.Child3.Child5.Child4');

INSERT INTO test VALUES ('Root1.Child3.Child6.Child1');
INSERT INTO test VALUES ('Root1.Child3.Child6.Child2');
INSERT INTO test VALUES ('Root1.Child3.Child6.Child3');
INSERT INTO test VALUES ('Root1.Child3.Child6.Child4');

Example 1

MQTT-Wildcard: Root1/Child1/+/Child1 corresponds to the following SQL command:

SELECT path FROM test WHERE path ~ 'Root1.Child1.*{1}.Child1'

Results:

  • Root1.Child1.Child1.Child1
  • Root1.Child1.Child2.Child1
  • Root1.Child1.Child3.Child1
  • Root1.Child1.Child4.Child1
  • Root1.Child1.Child5.Child1
  • Root1.Child1.Child6.Child1

Example 2

MQTT-Wildcard: Root1/Child1/Child1/Child1/# corresponds to the following SQL command:

SELECT path FROM test WHERE path ~ 'Root1.Child1.Child1.Child1.*'

Results:

  • Root1.Child1.Child1.Child1
  • Root1.Child1.Child1.Child1.A
  • Root1.Child1.Child1.Child1.B

Int32Overflow avatar Nov 25 '23 12:11 Int32Overflow