perf: throttle MQTT DB writes with 2s batched flush

Collect payloads in memory Map, flush to SQLite every 2 seconds
instead of on every message. Prevents DB lock and slowness with
high-volume brokers like Venus OS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Mueller 2026-04-06 21:03:36 +02:00
parent 9fbc5a1b45
commit 25d07a6bc4
2 changed files with 53 additions and 33 deletions

View File

@ -162,33 +162,40 @@ export function connectBroker(id: number): Promise<mqtt.MqttClient> {
resolve(client); resolve(client);
}); });
client.on('message', (topic: string, payloadBuf: Buffer) => { // Throttle DB writes: cache payloads in memory, flush periodically
const payloadCache = new Map<string, string>();
const flushPayloads = (): void => {
const db = getDb(); const db = getDb();
const payload = payloadBuf.toString(); for (const [t, p] of payloadCache.entries()) {
const topicRecord = db
.prepare('SELECT * FROM mqtt_topics WHERE broker_id = ? AND topic = ?')
.get(id, t) as MqttTopic | undefined;
if (!topicRecord) continue;
// Find the topic record db.run('UPDATE mqtt_topics SET last_payload = ? WHERE id = ?', [p, topicRecord.id]);
const topicRecord = db
.prepare(
'SELECT * FROM mqtt_topics WHERE broker_id = ? AND topic = ?'
)
.get(id, topic) as MqttTopic | undefined;
if (!topicRecord) return; const extracted = extractValue(p, topicRecord.json_path);
if (extracted === null) continue;
// Update last_payload const dp = getDatapointByTopicId(topicRecord.id);
db.run( if (dp) {
'UPDATE mqtt_topics SET last_payload = ? WHERE id = ?', updateDatapointValue(dp.id, extracted);
[payload, topicRecord.id] }
);
// Extract value and update datapoint
const extracted = extractValue(payload, topicRecord.json_path);
if (extracted === null) return;
const dp = getDatapointByTopicId(topicRecord.id);
if (dp) {
updateDatapointValue(dp.id, extracted);
} }
payloadCache.clear();
};
// Flush every 2 seconds
const flushTimer = setInterval(flushPayloads, 2000);
client.on('message', (topic: string, payloadBuf: Buffer) => {
payloadCache.set(topic, payloadBuf.toString());
});
// Clean up flush timer on disconnect
client.on('close', () => {
clearInterval(flushTimer);
flushPayloads();
}); });
client.on('error', (err: Error) => { client.on('error', (err: Error) => {

View File

@ -87,24 +87,37 @@ export function startDiscovery(brokerId: number, prefix?: string): Promise<MqttT
const discoveredTopics: Set<string> = new Set(); const discoveredTopics: Set<string> = new Set();
const finalize = (): void => { const finalize = (): void => {
clearInterval(flushInterval);
flushToDb(); // Final flush
if (tempClient) { if (tempClient) {
tempClient.end(true); tempClient.end(true);
} }
resolve(getTopicsForBroker(brokerId)); resolve(getTopicsForBroker(brokerId));
}; };
const onMessage = (topic: string, payload: Buffer): void => { // Collect payloads in memory, batch-write to DB periodically
const payloadStr = payload.toString(); const payloadCache: Map<string, string> = new Map();
if (!discoveredTopics.has(topic)) {
discoveredTopics.add(topic); const flushToDb = (): void => {
ensureTopicExists(brokerId, topic);
}
// Always update last_payload so the user sees the latest value
const db = getDb(); const db = getDb();
db.run( for (const [t, p] of payloadCache.entries()) {
'UPDATE mqtt_topics SET last_payload = ? WHERE broker_id = ? AND topic = ?', if (!discoveredTopics.has(t)) {
[payloadStr, brokerId, topic] discoveredTopics.add(t);
); ensureTopicExists(brokerId, t);
}
db.run(
'UPDATE mqtt_topics SET last_payload = ? WHERE broker_id = ? AND topic = ?',
[p, brokerId, t]
);
}
payloadCache.clear();
};
// Flush every 2 seconds instead of on every message
const flushInterval = setInterval(flushToDb, 2000);
const onMessage = (topic: string, payload: Buffer): void => {
payloadCache.set(topic, payload.toString());
}; };
if (existingClient && existingClient.connected) { if (existingClient && existingClient.connected) {