Add in-memory caching strategy for aggregated queries with TTL and automatic invalidation on data changes
This commit is contained in:
1 parent
a4206b9a0f
commit
a4295d8081
4 files changed
+110
-5
No files matched your search
@@ -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();
|
||||||
@@ -100,6 +100,10 @@ try {
|
|||||||
db.prepare("ALTER TABLE resources ADD COLUMN sla_target REAL DEFAULT 99.9").run();
|
db.prepare("ALTER TABLE resources ADD COLUMN sla_target REAL DEFAULT 99.9").run();
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.prepare("ALTER TABLE resources ADD COLUMN email_to TEXT").run();
|
||||||
|
} catch (err) {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.prepare("ALTER TABLE incidents ADD COLUMN acknowledged INTEGER DEFAULT 0").run();
|
db.prepare("ALTER TABLE incidents ADD COLUMN acknowledged INTEGER DEFAULT 0").run();
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
|
|||||||
+45
-2
@@ -7,6 +7,7 @@ const db = require('./database');
|
|||||||
const scheduler = require('./scheduler');
|
const scheduler = require('./scheduler');
|
||||||
const monitorService = require('./monitorService');
|
const monitorService = require('./monitorService');
|
||||||
const notificationService = require('./notificationService');
|
const notificationService = require('./notificationService');
|
||||||
|
const cache = require('./cache');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
@@ -99,6 +100,10 @@ app.post('/api/resources', (req, res) => {
|
|||||||
email_to || null
|
email_to || null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Invalidate related cache entries
|
||||||
|
cache.invalidatePattern('history:');
|
||||||
|
cache.invalidatePattern('sla:');
|
||||||
|
|
||||||
res.json({ id: result.lastInsertRowid, message: 'Resource created' });
|
res.json({ id: result.lastInsertRowid, message: 'Resource created' });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,12 +134,22 @@ app.put('/api/resources/:id', (req, res) => {
|
|||||||
email_to || null,
|
email_to || null,
|
||||||
req.params.id
|
req.params.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Invalidate related cache entries
|
||||||
|
cache.invalidatePattern('history:');
|
||||||
|
cache.invalidatePattern('sla:');
|
||||||
|
|
||||||
res.json({ message: 'Resource updated' });
|
res.json({ message: 'Resource updated' });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete resource
|
// Delete resource
|
||||||
app.delete('/api/resources/:id', (req, res) => {
|
app.delete('/api/resources/:id', (req, res) => {
|
||||||
db.prepare('DELETE FROM resources WHERE id = ?').run(req.params.id);
|
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' });
|
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 currentPage = Math.max(1, parseInt(pageParam || page || 1));
|
||||||
const offset = (currentPage - 1) * pageLimit;
|
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
|
// Get total count of enabled resources
|
||||||
const totalCount = db.prepare('SELECT COUNT(*) as count FROM resources WHERE enabled = 1').get();
|
const totalCount = db.prepare('SELECT COUNT(*) as count FROM resources WHERE enabled = 1').get();
|
||||||
const total = totalCount.count;
|
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
|
// Acknowledge incident
|
||||||
@@ -407,6 +436,15 @@ app.get('/api/sla', (req, res) => {
|
|||||||
const currentPage = Math.max(1, parseInt(page));
|
const currentPage = Math.max(1, parseInt(page));
|
||||||
const offset = (currentPage - 1) * pageLimit;
|
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
|
// Get total count
|
||||||
const totalCount = db.prepare('SELECT COUNT(*) as count FROM resources WHERE enabled = 1').get();
|
const totalCount = db.prepare('SELECT COUNT(*) as count FROM resources WHERE enabled = 1').get();
|
||||||
const total = totalCount.count;
|
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
|
// Test email
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ class NotificationService {
|
|||||||
|
|
||||||
const promises = [];
|
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));
|
promises.push(this.sendEmail(resource, message, incident.type, stats));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,14 +142,17 @@ class NotificationService {
|
|||||||
|
|
||||||
htmlContent += `</div>`;
|
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({
|
await this.transporter.sendMail({
|
||||||
from: this.config.email_from,
|
from: this.config.email_from,
|
||||||
to: this.config.email_to,
|
to: emailTo,
|
||||||
subject: `Alert: ${resource.name} is ${type === 'started' ? 'DOWN' : 'UP'}`,
|
subject: `Alert: ${resource.name} is ${type === 'started' ? 'DOWN' : 'UP'}`,
|
||||||
text: message,
|
text: message,
|
||||||
html: htmlContent,
|
html: htmlContent,
|
||||||
});
|
});
|
||||||
console.log(`Email sent for ${resource.name}`);
|
console.log(`Email sent for ${resource.name} to ${emailTo}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Email error:', error.message);
|
console.error('Email error:', error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user