Add in-memory caching strategy for aggregated queries with TTL and automatic invalidation on data changes

This commit is contained in:
nmemmert committed 2025-12-11 09:54:19 -05:00
1 parent a4206b9a0f
commit a4295d8081
4 files changed
+110 -5

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
// Simple in-memory cache with TTL support
class CacheManager {
constructor() {
this.cache = new Map();
}
set(key, value, ttlSeconds = 300) {
const expiresAt = Date.now() + (ttlSeconds * 1000);
this.cache.set(key, { value, expiresAt });
}
get(key) {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() > item.expiresAt) {
this.cache.delete(key);
return null;
}
return item.value;
}
has(key) {
return this.get(key) !== null;
}
delete(key) {
this.cache.delete(key);
}
clear() {
this.cache.clear();
}
// Invalidate all cache keys matching a pattern
invalidatePattern(pattern) {
for (const key of this.cache.keys()) {
if (key.includes(pattern)) {
this.cache.delete(key);
}
}
}
// Get cache statistics
getStats() {
return {
size: this.cache.size,
keys: Array.from(this.cache.keys()),
};
}
}
module.exports = new CacheManager();
+4
View File
@@ -100,6 +100,10 @@ try {
db.prepare("ALTER TABLE resources ADD COLUMN sla_target REAL DEFAULT 99.9").run();
} catch (err) {}
try {
db.prepare("ALTER TABLE resources ADD COLUMN email_to TEXT").run();
} catch (err) {}
try {
db.prepare("ALTER TABLE incidents ADD COLUMN acknowledged INTEGER DEFAULT 0").run();
} catch (err) {}
+45 -2
View File
@@ -7,6 +7,7 @@ const db = require('./database');
const scheduler = require('./scheduler');
const monitorService = require('./monitorService');
const notificationService = require('./notificationService');
const cache = require('./cache');
const app = express();
const PORT = process.env.PORT || 3001;
@@ -99,6 +100,10 @@ app.post('/api/resources', (req, res) => {
email_to || null
);
// Invalidate related cache entries
cache.invalidatePattern('history:');
cache.invalidatePattern('sla:');
res.json({ id: result.lastInsertRowid, message: 'Resource created' });
});
@@ -129,12 +134,22 @@ app.put('/api/resources/:id', (req, res) => {
email_to || null,
req.params.id
);
// Invalidate related cache entries
cache.invalidatePattern('history:');
cache.invalidatePattern('sla:');
res.json({ message: 'Resource updated' });
});
// Delete resource
app.delete('/api/resources/:id', (req, res) => {
db.prepare('DELETE FROM resources WHERE id = ?').run(req.params.id);
// Invalidate related cache entries
cache.invalidatePattern('history:');
cache.invalidatePattern('sla:');
res.json({ message: 'Resource deleted' });
});
@@ -331,6 +346,15 @@ app.get('/api/history/overview', (req, res) => {
const currentPage = Math.max(1, parseInt(pageParam || page || 1));
const offset = (currentPage - 1) * pageLimit;
// Create cache key based on query parameters
const cacheKey = `history:days=${days}:page=${currentPage}:limit=${pageLimit}`;
// Check cache first
const cachedResult = cache.get(cacheKey);
if (cachedResult) {
return res.json(cachedResult);
}
// Get total count of enabled resources
const totalCount = db.prepare('SELECT COUNT(*) as count FROM resources WHERE enabled = 1').get();
const total = totalCount.count;
@@ -377,7 +401,12 @@ app.get('/api/history/overview', (req, res) => {
};
});
res.json({ resources: overview, total, page: currentPage, limit: pageLimit });
const result = { resources: overview, total, page: currentPage, limit: pageLimit };
// Cache result for 2 minutes (120 seconds)
cache.set(cacheKey, result, 120);
res.json(result);
});
// Acknowledge incident
@@ -407,6 +436,15 @@ app.get('/api/sla', (req, res) => {
const currentPage = Math.max(1, parseInt(page));
const offset = (currentPage - 1) * pageLimit;
// Create cache key
const cacheKey = `sla:days=${days}:page=${currentPage}:limit=${pageLimit}`;
// Check cache first
const cachedResult = cache.get(cacheKey);
if (cachedResult) {
return res.json(cachedResult);
}
// Get total count
const totalCount = db.prepare('SELECT COUNT(*) as count FROM resources WHERE enabled = 1').get();
const total = totalCount.count;
@@ -448,7 +486,12 @@ app.get('/api/sla', (req, res) => {
};
});
res.json({ resources: slaData, total, page: currentPage, limit: pageLimit });
const result = { resources: slaData, total, page: currentPage, limit: pageLimit };
// Cache result for 3 minutes (180 seconds)
cache.set(cacheKey, result, 180);
res.json(result);
});
// Test email
+7 -3
View File
@@ -69,7 +69,8 @@ class NotificationService {
const promises = [];
if (this.emailEnabled) {
// Use resource-specific email if provided, otherwise fall back to global config
if (this.emailEnabled && (resource.email_to || this.config.email_to)) {
promises.push(this.sendEmail(resource, message, incident.type, stats));
}
@@ -141,14 +142,17 @@ class NotificationService {
htmlContent += `</div>`;
// Use resource-specific email if provided, otherwise use global config
const emailTo = resource.email_to || this.config.email_to;
await this.transporter.sendMail({
from: this.config.email_from,
to: this.config.email_to,
to: emailTo,
subject: `Alert: ${resource.name} is ${type === 'started' ? 'DOWN' : 'UP'}`,
text: message,
html: htmlContent,
});
console.log(`Email sent for ${resource.name}`);
console.log(`Email sent for ${resource.name} to ${emailTo}`);
} catch (error) {
console.error('Email error:', error.message);
}