First iteration of healthchecking, redis locking of them, etc. still TODO locking when hsetting records on frontend for health change

develop
Thomas Lynch 1 year ago
parent 93759a0108
commit eb19b26c4b
  1. 18
      controllers/dns.js
  2. 24
      ecosystem.config.js
  3. 45
      healthcheck/main.js
  4. 88
      healthcheck/worker.js
  5. 195
      package-lock.json
  6. 4
      package.json
  7. 9
      redlock.js

@ -12,7 +12,7 @@ exports.dnsDomainPage = async (app, req, res) => {
if (!res.locals.user.domains.includes(req.params.domain)) {
return dynamicResponse(req, res, 302, { redirect: '/domains' });
}
const recordSetsRaw = await redis.hgetall(`${req.params.domain}.`);
const recordSetsRaw = await redis.hgetall(`dns:${req.params.domain}.`);
const recordSets = recordSetsRaw && Object.keys(recordSetsRaw)
.map(k => {
return { [k]: JSON.parse(recordSetsRaw[k]) };
@ -33,7 +33,7 @@ exports.dnsRecordPage = async (app, req, res) => {
}
let recordSet = [];
if (req.params.zone && req.params.type) {
let recordSetRaw = await redis.hget(`${req.params.domain}.`, req.params.zone);
let recordSetRaw = await redis.hget(`dns:${req.params.domain}.`, req.params.zone);
if (!recordSetRaw) {
recordSetRaw = {};
}
@ -54,7 +54,7 @@ exports.dnsDomainJson = async (req, res) => {
if (!res.locals.user.domains.includes(req.params.domain)) {
return dynamicResponse(req, res, 403, { error: 'No permission for this domain' });
}
const recordSetsRaw = await redis.hgetall(`${req.params.domain}.`);
const recordSetsRaw = await redis.hgetall(`dns:${req.params.domain}.`);
const recordSets = recordSetsRaw && Object.keys(recordSetsRaw)
.map(k => {
return { [k]: JSON.parse(recordSetsRaw[k]) };
@ -76,7 +76,7 @@ exports.dnsRecordJson = async (req, res) => {
}
let recordSet = [];
if (req.params.zone && req.params.type) {
let recordSetRaw = await redis.hget(`${req.params.domain}.`, req.params.zone);
let recordSetRaw = await redis.hget(`dns:${req.params.domain}.`, req.params.zone);
if (!recordSetRaw) {
recordSetRaw = {};
}
@ -100,15 +100,15 @@ exports.dnsRecordDelete = async (req, res) => {
return dynamicResponse(req, res, 302, { redirect: '/domains' });
}
if (req.params.zone && req.params.type) {
let recordSetRaw = await redis.hget(`${req.params.domain}.`, req.params.zone);
let recordSetRaw = await redis.hget(`dns:${req.params.domain}.`, req.params.zone);
if (!recordSetRaw) {
recordSetRaw = {};
}
delete recordSetRaw[req.params.type];
if (Object.keys(recordSetRaw).length === 0) {
await redis.hdel(`${req.params.domain}.`, req.params.zone);
await redis.hdel(`dns:${req.params.domain}.`, req.params.zone);
} else {
await redis.hset(`${req.params.domain}.`, req.params.zone, recordSetRaw);
await redis.hset(`dns:${req.params.domain}.`, req.params.zone, recordSetRaw);
}
}
return dynamicResponse(req, res, 302, { redirect: `/dns/${req.params.domain}` });
@ -230,7 +230,7 @@ exports.dnsRecordUpdate = async (req, res) => {
if (records.lencth === 0) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
let recordSetRaw = await redis.hget(`${req.params.domain}.`, req.params.zone);
let recordSetRaw = await redis.hget(`dns:${req.params.domain}.`, req.params.zone);
if (!recordSetRaw) {
recordSetRaw = {};
}
@ -239,6 +239,6 @@ exports.dnsRecordUpdate = async (req, res) => {
} else {
recordSetRaw[type] = records;
}
await redis.hset(`${domain}.`, zone, recordSetRaw);
await redis.hset(`dns:${domain}.`, zone, recordSetRaw);
return dynamicResponse(req, res, 302, { redirect: `/dns/${domain}` });
};

@ -23,6 +23,30 @@ module.exports = {
env_production: {
"NODE_ENV": "production",
}
},
{
name: "healthcheck-main",
script: "./healthcheck/main.js",
instances : "1",
exec_mode : "fork",
env: {
"NODE_ENV": "development"
},
env_production: {
"NODE_ENV": "production",
}
},
{
name: "healthcheck-worker",
script: "./healthcheck/worker.js",
instances : "1",
exec_mode : "fork",
env: {
"NODE_ENV": "development"
},
env_production: {
"NODE_ENV": "production",
}
}
]
}

@ -0,0 +1,45 @@
'use strict';
const dotenv = require('dotenv');
dotenv.config({ path: '.env' });
const redis = require('../redis.js');
const Queue = require('bull');
const healthCheckQueue = new Queue('healthchecks', {
host: process.env.REDIS_HOST || '127.0.0.1',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASS || '',
});
function scanKeys(pattern) {
return new Promise((resolve, reject) => {
const stream = redis.client.scanStream({
match: pattern,
count: 10,
});
stream.on('data', (keys) => {
if (!keys || keys.length === 0) { return; }
healthCheckQueue.add({ keys }, { removeOnComplete: true });
});
stream.on('end', () => {
resolve();
});
stream.on('error', (err) => {
reject(err);
});
});
}
async function main() {
const start = Date.now();
try {
await scanKeys('dns:*');
} catch(e) {
console.error(e);
setTimeout(main, 10000);
return;
}
const elapsed = Date.now() - start;
setTimeout(main, 2000-elapsed);
}
main();

@ -0,0 +1,88 @@
'use strict';
const dotenv = require('dotenv');
dotenv.config({ path: '.env' });
const redis = require('../redis.js');
const redlock = require('../redlock.js');
const Queue = require('bull');
const healthCheckQueue = new Queue('healthchecks', {
host: process.env.REDIS_HOST || '127.0.0.1',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASS || '',
});
const https = require('https');
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
async function doCheck(domainKey, hkey, record) {
// console.log(domainKey, hkey)
await new Promise(res => setTimeout(res, Math.floor(Math.random()*1000)));
try {
let recordHealth = await redis.get(`health:${record.ip}`);
if (recordHealth === null) {
try {
// console.log('healthchecking', record.ip);
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => {
controller.abort();
}, 3000);
await fetch(`https://${record.ip}/`, {
method: 'HEAD',
headers: { 'Host': 'basedflare.com' },
agent: httpsAgent,
signal,
});
recordHealth = '1'; //no error = we consider successful
} catch(e) {
console.warn('health check down for', record.ip);
recordHealth = '0';
}
await redis.client.set(`health:${record.ip}`, recordHealth, 'EX', 5, 'NX');
}
// console.log(record.ip, 'health:', recordHealth);
if (recordHealth === '1' && record.u === false) {
record.u = true;
return record;
} else if (recordHealth === '0' && record.u === true) {
record.u = false;
return record;
}
return record; //no change required, or no cache and failed fetch
} catch(e) {
console.error(e);
return record;
}
}
async function processKey(domainKey) {
try {
const domainHashKeys = await redis.client.hkeys(domainKey);
await Promise.allSettled(domainHashKeys.map(async (hkey) => {
const lock = await redlock.acquire([`lock:${domainKey}:${hkey}`], 5000);
try {
const records = await redis.hget(domainKey, hkey);
const updatedA = await Promise.all((records['a']||[]).map(async r => doCheck(domainKey, hkey, r)));
const updatedAAAA = await Promise.all((records['aaaa']||[]).map(async r => doCheck(domainKey, hkey, r)));
records['a'] = updatedA;
records['aaaa'] = updatedAAAA;
await redis.hset(domainKey, hkey, records);
} catch(e) {
console.error(e);
} finally {
await lock.release();
}
}));
} catch(e) {
console.error(e);
}
}
async function handleJob(job, done) { //job.id, job.data
const { keys } = job.data;
keys.forEach(processKey);
done();
}
healthCheckQueue.process(handleJob);

195
package-lock.json generated

@ -15,6 +15,7 @@
"body-parser": "^1.20.2",
"bootstrap": "^5.2.3",
"bootstrap-icons": "^1.10.3",
"bull": "^4.10.4",
"connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6",
"csurf": "^1.11.0",
@ -33,7 +34,8 @@
"react": "^18.2.0",
"react-content-loader": "^6.2.0",
"react-dom": "^18.2.0",
"react-select": "^5.7.3"
"react-select": "^5.7.3",
"redlock": "^5.0.0-beta.2"
},
"devDependencies": {
"eslint": "8.15.0",
@ -1501,6 +1503,78 @@
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz",
"integrity": "sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz",
"integrity": "sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz",
"integrity": "sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz",
"integrity": "sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz",
"integrity": "sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz",
"integrity": "sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
]
},
"node_modules/@next/env": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/env/-/env-12.3.4.tgz",
@ -2901,6 +2975,24 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
},
"node_modules/bull": {
"version": "4.10.4",
"resolved": "https://registry.npmjs.org/bull/-/bull-4.10.4.tgz",
"integrity": "sha512-o9m/7HjS/Or3vqRd59evBlWCXd9Lp+ALppKseoSKHaykK46SmRjAilX98PgmOz1yeVaurt8D5UtvEt4bUjM3eA==",
"dependencies": {
"cron-parser": "^4.2.1",
"debuglog": "^1.0.0",
"get-port": "^5.1.1",
"ioredis": "^5.0.0",
"lodash": "^4.17.21",
"msgpackr": "^1.5.2",
"semver": "^7.3.2",
"uuid": "^8.3.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -3360,6 +3452,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cron-parser": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.8.1.tgz",
"integrity": "sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ==",
"dependencies": {
"luxon": "^3.2.1"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -3490,6 +3593,14 @@
}
}
},
"node_modules/debuglog": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz",
"integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==",
"engines": {
"node": "*"
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
@ -5230,6 +5341,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-port": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
"integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-symbol-description": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
@ -6587,6 +6709,11 @@
"node": ">=0.10.0"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
@ -6625,6 +6752,14 @@
"node": ">=10"
}
},
"node_modules/luxon": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz",
"integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==",
"engines": {
"node": ">=12"
}
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@ -7100,6 +7235,35 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/msgpackr": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.9.1.tgz",
"integrity": "sha512-jJdrNH8tzfCtT0rjPFryBXjRDQE7rqfLkah4/8B4gYa7NNZYFBcGxqWBtfQpGC+oYyBwlkj3fARk4aooKNPHxg==",
"optionalDependencies": {
"msgpackr-extract": "^3.0.2"
}
},
"node_modules/msgpackr-extract": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz",
"integrity": "sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"node-gyp-build-optional-packages": "5.0.7"
},
"bin": {
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
},
"optionalDependencies": {
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.2",
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.2",
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.2",
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.2",
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.2",
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2"
}
},
"node_modules/mute-stdout": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz",
@ -7300,6 +7464,11 @@
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
},
"node_modules/node-abort-controller": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="
},
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
@ -7351,6 +7520,17 @@
"node": ">= 6.13.0"
}
},
"node_modules/node-gyp-build-optional-packages": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz",
"integrity": "sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==",
"optional": true,
"bin": {
"node-gyp-build-optional-packages": "bin.js",
"node-gyp-build-optional-packages-optional": "optional.js",
"node-gyp-build-optional-packages-test": "build-test.js"
}
},
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@ -8358,6 +8538,17 @@
"node": ">=4"
}
},
"node_modules/redlock": {
"version": "5.0.0-beta.2",
"resolved": "https://registry.npmjs.org/redlock/-/redlock-5.0.0-beta.2.tgz",
"integrity": "sha512-2RDWXg5jgRptDrB1w9O/JgSZC0j7y4SlaXnor93H/UJm/QyDiFgBKNtrh0TI6oCXqYSaSoXxFh6Sd3VtYfhRXw==",
"dependencies": {
"node-abort-controller": "^3.0.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
@ -9940,8 +10131,6 @@
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"optional": true,
"peer": true,
"bin": {
"uuid": "dist/bin/uuid"
}

@ -21,6 +21,7 @@
"body-parser": "^1.20.2",
"bootstrap": "^5.2.3",
"bootstrap-icons": "^1.10.3",
"bull": "^4.10.4",
"connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6",
"csurf": "^1.11.0",
@ -39,7 +40,8 @@
"react": "^18.2.0",
"react-content-loader": "^6.2.0",
"react-dom": "^18.2.0",
"react-select": "^5.7.3"
"react-select": "^5.7.3",
"redlock": "^5.0.0-beta.2"
},
"devDependencies": {
"eslint": "8.15.0",

@ -0,0 +1,9 @@
'use strict';
const Redlock = require('redlock').default;
const redis = require('./redis.js');
const redlock = new Redlock([redis.client]);
redlock.on('clientError', console.error);
module.exports = redlock;
Loading…
Cancel
Save