feat(api): enhance health check and DNS management
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 7s
quality / changes (push) Successful in 8s
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m9s
quality / api (push) Successful in 40s
CD / quality (push) Successful in 2m0s
CD / publish (push) Successful in 2m38s
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 7s
quality / changes (push) Successful in 8s
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m9s
quality / api (push) Successful in 40s
CD / quality (push) Successful in 2m0s
CD / publish (push) Successful in 2m38s
- Added batch endpoint for health status queries to reduce the number of requests. - Improved DNS record management with caching and invalidation mechanisms. - Updated health check service to handle new configurations and improve performance. - Refactored certificate service to utilize concurrency for checks, enhancing efficiency. - Removed unused dependencies and optimized package configurations. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Vendored
+50
-2
File diff suppressed because one or more lines are too long
Vendored
+314
-1
@@ -425,6 +425,9 @@ function createDb(databaseUrl) {
|
||||
sqlite.pragma("journal_mode = WAL");
|
||||
sqlite.pragma("synchronous = NORMAL");
|
||||
sqlite.pragma("foreign_keys = ON");
|
||||
sqlite.pragma("busy_timeout = 5000");
|
||||
sqlite.pragma("cache_size = -16000");
|
||||
sqlite.pragma("wal_autocheckpoint = 1000");
|
||||
const db = drizzle(sqlite, { schema });
|
||||
return { db, sqlite };
|
||||
}
|
||||
@@ -455,6 +458,9 @@ function runMigrations(sqlite) {
|
||||
function healthCheck(sqlite) {
|
||||
sqlite.prepare("SELECT 1").get();
|
||||
}
|
||||
function walCheckpointTruncate(sqlite) {
|
||||
sqlite.pragma("wal_checkpoint(TRUNCATE)");
|
||||
}
|
||||
|
||||
// src/errors.ts
|
||||
var NotFoundError = class extends Error {
|
||||
@@ -669,6 +675,7 @@ function touchVpsTrackerSync(db) {
|
||||
// src/repos.ts
|
||||
var repos_exports = {};
|
||||
__export(repos_exports, {
|
||||
LOG_RETENTION_LIMITS: () => LOG_RETENTION_LIMITS,
|
||||
addDomainTags: () => addDomainTags,
|
||||
aggregateGroupScopeHealthByIds: () => aggregateGroupScopeHealthByIds,
|
||||
aggregateIpHealthByRefs: () => aggregateIpHealthByRefs,
|
||||
@@ -723,6 +730,7 @@ __export(repos_exports, {
|
||||
getServiceGroup: () => getServiceGroup,
|
||||
getSubdomain: () => getSubdomain,
|
||||
getSyncJob: () => getSyncJob,
|
||||
hasWeightedBindings: () => hasWeightedBindings,
|
||||
insertBinding: () => insertBinding,
|
||||
insertDnsRecord: () => insertDnsRecord,
|
||||
insertFailoverLog: () => insertFailoverLog,
|
||||
@@ -734,11 +742,14 @@ __export(repos_exports, {
|
||||
listAllDomains: () => listAllDomains,
|
||||
listAllNodes: () => listAllNodes,
|
||||
listAllSubdomains: () => listAllSubdomains,
|
||||
listBindingIpHealthByBindingIds: () => listBindingIpHealthByBindingIds,
|
||||
listBindingIps: () => listBindingIps,
|
||||
listBindingIpsWithMeta: () => listBindingIpsWithMeta,
|
||||
listBindingIpsWithMetaByBindingIds: () => listBindingIpsWithMetaByBindingIds,
|
||||
listBindingNodes: () => listBindingNodes,
|
||||
listBindingsByDomain: () => listBindingsByDomain,
|
||||
listBindingsByService: () => listBindingsByService,
|
||||
listBindingsByServiceIds: () => listBindingsByServiceIds,
|
||||
listCertificates: () => listCertificates,
|
||||
listDnsByDomain: () => listDnsByDomain,
|
||||
listDnsRecords: () => listDnsRecords,
|
||||
@@ -757,19 +768,25 @@ __export(repos_exports, {
|
||||
listHealthProbeLogForService: () => listHealthProbeLogForService,
|
||||
listIpHealthByServiceIds: () => listIpHealthByServiceIds,
|
||||
listIpHealthStatus: () => listIpHealthStatus,
|
||||
listIpHealthStatusByBindingIds: () => listIpHealthStatusByBindingIds,
|
||||
listLatestLiveHealthByServiceIds: () => listLatestLiveHealthByServiceIds,
|
||||
listNodes: () => listNodes,
|
||||
listNotificationLog: () => listNotificationLog,
|
||||
listOriginIpsForFqdn: () => listOriginIpsForFqdn,
|
||||
listRecordsByBindingIds: () => listRecordsByBindingIds,
|
||||
listRecordsForBinding: () => listRecordsForBinding,
|
||||
listServiceGroups: () => listServiceGroups,
|
||||
listServiceIpRows: () => listServiceIpRows,
|
||||
listServiceIpRowsByServiceIds: () => listServiceIpRowsByServiceIds,
|
||||
listServiceIps: () => listServiceIps,
|
||||
listServices: () => listServices,
|
||||
listServicesByGroup: () => listServicesByGroup,
|
||||
listServicesByGroupIds: () => listServicesByGroupIds,
|
||||
listSubdomainsByDomain: () => listSubdomainsByDomain,
|
||||
listUngroupedServices: () => listUngroupedServices,
|
||||
markDnsPendingDelete: () => markDnsPendingDelete,
|
||||
mergeHealthAggregates: () => mergeHealthAggregates,
|
||||
pruneLogs: () => pruneLogs,
|
||||
pruneStaleIpHealthStatus: () => pruneStaleIpHealthStatus,
|
||||
reorderServices: () => reorderServices,
|
||||
replaceBindingIps: () => replaceBindingIps,
|
||||
@@ -1135,6 +1152,29 @@ function listServices(db) {
|
||||
function listServicesByGroup(db, groupId) {
|
||||
return db.select().from(services).where(eq3(services.service_group_id, groupId)).orderBy(asc(services.sort_order), asc(services.name)).all();
|
||||
}
|
||||
function listServicesByGroupIds(db, groupIds) {
|
||||
const result = /* @__PURE__ */ new Map();
|
||||
if (groupIds.length === 0) return result;
|
||||
const idList = sql2.join(
|
||||
groupIds.map((id) => sql2`${id}`),
|
||||
sql2`, `
|
||||
);
|
||||
const rows = db.all(sql2`
|
||||
SELECT * FROM services
|
||||
WHERE service_group_id IN (${idList})
|
||||
ORDER BY service_group_id, sort_order, name
|
||||
`);
|
||||
for (const row of rows) {
|
||||
const groupId = row.service_group_id;
|
||||
let list = result.get(groupId);
|
||||
if (!list) {
|
||||
list = [];
|
||||
result.set(groupId, list);
|
||||
}
|
||||
list.push(row);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function listUngroupedServices(db) {
|
||||
return db.select().from(services).where(isNull(services.service_group_id)).orderBy(asc(services.sort_order), asc(services.name)).all();
|
||||
}
|
||||
@@ -1772,6 +1812,16 @@ function listAllBindings(db) {
|
||||
ORDER BY d.zone_name, s.name
|
||||
`).map((row) => enrichServiceBindingView(db, row));
|
||||
}
|
||||
function hasWeightedBindings(db) {
|
||||
const row = db.get(sql2`
|
||||
SELECT (EXISTS (
|
||||
SELECT 1 FROM service_bindings WHERE lb_mode = 'weighted'
|
||||
) OR EXISTS (
|
||||
SELECT 1 FROM service_groups WHERE lb_mode = 'weighted'
|
||||
)) AS n
|
||||
`);
|
||||
return Boolean(row?.n);
|
||||
}
|
||||
function listBindingsByDomain(db, domainId) {
|
||||
return db.all(sql2`
|
||||
SELECT ${sql2.raw(SERVICE_BINDING_SELECT_COLUMNS)}
|
||||
@@ -1800,6 +1850,123 @@ function getBinding(db, id) {
|
||||
if (!row) throw new NotFoundError(`service binding ${id}`);
|
||||
return mapServiceBinding(row);
|
||||
}
|
||||
function listBindingsByServiceIds(db, serviceIds) {
|
||||
const result = /* @__PURE__ */ new Map();
|
||||
if (serviceIds.length === 0) return result;
|
||||
const idList = sql2.join(
|
||||
serviceIds.map((id) => sql2`${id}`),
|
||||
sql2`, `
|
||||
);
|
||||
const rows = db.all(sql2`
|
||||
SELECT sb.*, d.zone_name FROM service_bindings sb
|
||||
JOIN domains d ON d.id = sb.domain_id
|
||||
WHERE sb.service_id IN (${idList})
|
||||
ORDER BY sb.id
|
||||
`);
|
||||
for (const row of rows) {
|
||||
const { service_id, ...binding } = row;
|
||||
let list = result.get(service_id);
|
||||
if (!list) {
|
||||
list = [];
|
||||
result.set(service_id, list);
|
||||
}
|
||||
list.push({
|
||||
...mapServiceBinding({ ...binding, service_id }),
|
||||
zone_name: row.zone_name
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function listBindingIpsWithMetaByBindingIds(db, bindingIds) {
|
||||
const result = /* @__PURE__ */ new Map();
|
||||
if (bindingIds.length === 0) return result;
|
||||
const idList = sql2.join(
|
||||
bindingIds.map((id) => sql2`${id}`),
|
||||
sql2`, `
|
||||
);
|
||||
const rows = db.all(sql2`
|
||||
SELECT binding_id, ip, weight, priority FROM service_binding_ips
|
||||
WHERE binding_id IN (${idList})
|
||||
ORDER BY binding_id, rowid
|
||||
`);
|
||||
for (const row of rows) {
|
||||
let list = result.get(row.binding_id);
|
||||
if (!list) {
|
||||
list = [];
|
||||
result.set(row.binding_id, list);
|
||||
}
|
||||
list.push({ ip: row.ip, weight: row.weight, priority: row.priority });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function listRecordsByBindingIds(db, bindingIds) {
|
||||
const result = /* @__PURE__ */ new Map();
|
||||
if (bindingIds.length === 0) return result;
|
||||
const idList = sql2.join(
|
||||
bindingIds.map((id) => sql2`${id}`),
|
||||
sql2`, `
|
||||
);
|
||||
const rows = db.all(sql2`
|
||||
SELECT dr.*, sbr.binding_id AS binding_id FROM dns_records dr
|
||||
INNER JOIN service_binding_records sbr ON sbr.dns_record_id = dr.id
|
||||
WHERE sbr.binding_id IN (${idList})
|
||||
ORDER BY sbr.binding_id, dr.id
|
||||
`);
|
||||
for (const row of rows) {
|
||||
const { binding_id, ...record } = row;
|
||||
let list = result.get(binding_id);
|
||||
if (!list) {
|
||||
list = [];
|
||||
result.set(binding_id, list);
|
||||
}
|
||||
list.push(record);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function listBindingIpHealthByBindingIds(db, bindingIds) {
|
||||
const result = /* @__PURE__ */ new Map();
|
||||
if (bindingIds.length === 0) return result;
|
||||
const idList = sql2.join(
|
||||
bindingIds.map((id) => sql2`${id}`),
|
||||
sql2`, `
|
||||
);
|
||||
const rows = db.all(sql2`
|
||||
SELECT ref_id, ip, status, latency_ms
|
||||
FROM ip_health_status
|
||||
WHERE scope = 'binding' AND ref_id IN (${idList})
|
||||
`);
|
||||
for (const row of rows) {
|
||||
let byIp = result.get(row.ref_id);
|
||||
if (!byIp) {
|
||||
byIp = /* @__PURE__ */ new Map();
|
||||
result.set(row.ref_id, byIp);
|
||||
}
|
||||
byIp.set(row.ip, { status: row.status, latency_ms: row.latency_ms });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function listServiceIpRowsByServiceIds(db, serviceIds) {
|
||||
const result = /* @__PURE__ */ new Map();
|
||||
if (serviceIds.length === 0) return result;
|
||||
const idList = sql2.join(
|
||||
serviceIds.map((id) => sql2`${id}`),
|
||||
sql2`, `
|
||||
);
|
||||
const rows = db.all(sql2`
|
||||
SELECT service_id, ip, enabled FROM service_ips
|
||||
WHERE service_id IN (${idList})
|
||||
ORDER BY service_id, rowid
|
||||
`);
|
||||
for (const row of rows) {
|
||||
let list = result.get(row.service_id);
|
||||
if (!list) {
|
||||
list = [];
|
||||
result.set(row.service_id, list);
|
||||
}
|
||||
list.push({ ip: row.ip, enabled: Boolean(row.enabled) });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function getBindingView(db, id) {
|
||||
const rows = db.all(sql2`
|
||||
SELECT ${sql2.raw(SERVICE_BINDING_SELECT_COLUMNS)}
|
||||
@@ -1972,6 +2139,29 @@ function listIpHealthStatus(db, scope, refId) {
|
||||
WHERE scope = ${scope} AND ref_id = ${refId}
|
||||
`);
|
||||
}
|
||||
function listIpHealthStatusByBindingIds(db, bindingIds) {
|
||||
const result = /* @__PURE__ */ new Map();
|
||||
if (bindingIds.length === 0) return result;
|
||||
const idList = sql2.join(
|
||||
bindingIds.map((id) => sql2`${id}`),
|
||||
sql2`, `
|
||||
);
|
||||
const rows = db.all(sql2`
|
||||
SELECT scope, ref_id, ip, status, latency_ms, consecutive_failures,
|
||||
last_checked_at, last_error, colo, provider
|
||||
FROM ip_health_status
|
||||
WHERE scope = 'binding' AND ref_id IN (${idList})
|
||||
`);
|
||||
for (const row of rows) {
|
||||
let list = result.get(row.ref_id);
|
||||
if (!list) {
|
||||
list = [];
|
||||
result.set(row.ref_id, list);
|
||||
}
|
||||
list.push(row);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
var UNKNOWN_HEALTH = {
|
||||
health_status: "unknown",
|
||||
health_latency_ms: null
|
||||
@@ -2127,6 +2317,83 @@ function listIpHealthByServiceIds(db, serviceIds) {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function parseIpHealthState(status) {
|
||||
if (status === "up" || status === "down" || status === "degraded" || status === "unknown") {
|
||||
return status;
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
function bestAliveIpState(statuses) {
|
||||
if (statuses.some((status) => status === "up")) return "up";
|
||||
if (statuses.some((status) => status === "degraded")) return "degraded";
|
||||
if (statuses.some((status) => status === "down")) return "down";
|
||||
return "unknown";
|
||||
}
|
||||
function listLatestLiveHealthByServiceIds(db, serviceIds) {
|
||||
const result = /* @__PURE__ */ new Map();
|
||||
if (serviceIds.length === 0) return result;
|
||||
const idList = sql2.join(
|
||||
serviceIds.map((id) => sql2`${id}`),
|
||||
sql2`, `
|
||||
);
|
||||
const rows = db.all(sql2`
|
||||
SELECT sb.service_id AS service_id,
|
||||
l.ip AS ip,
|
||||
l.provider AS provider,
|
||||
l.status AS status,
|
||||
l.latency_ms AS latency_ms,
|
||||
l.error AS last_error,
|
||||
l.colo AS colo,
|
||||
l.checked_at AS checked_at
|
||||
FROM health_probe_log l
|
||||
INNER JOIN service_bindings sb
|
||||
ON l.scope = 'binding' AND l.ref_id = sb.id
|
||||
INNER JOIN (
|
||||
SELECT sb2.service_id AS service_id,
|
||||
l2.ip AS ip,
|
||||
l2.provider AS provider,
|
||||
MAX(l2.id) AS max_id
|
||||
FROM health_probe_log l2
|
||||
INNER JOIN service_bindings sb2
|
||||
ON l2.scope = 'binding' AND l2.ref_id = sb2.id
|
||||
WHERE sb2.service_id IN (${idList})
|
||||
GROUP BY sb2.service_id, l2.ip, l2.provider
|
||||
) latest
|
||||
ON latest.max_id = l.id
|
||||
`);
|
||||
const byServiceIp = /* @__PURE__ */ new Map();
|
||||
for (const row of rows) {
|
||||
const status = parseIpHealthState(row.status);
|
||||
if (!status) continue;
|
||||
const key = `${row.service_id}\0${row.ip}`;
|
||||
const probe = {
|
||||
ip: row.ip,
|
||||
status,
|
||||
latency_ms: row.latency_ms,
|
||||
last_checked_at: row.checked_at,
|
||||
last_error: row.last_error,
|
||||
provider: normalizeStatusProvider(row.provider),
|
||||
colo: row.colo
|
||||
};
|
||||
const bucket = byServiceIp.get(key);
|
||||
if (bucket) bucket.probes.push(probe);
|
||||
else {
|
||||
byServiceIp.set(key, {
|
||||
serviceId: row.service_id,
|
||||
ip: row.ip,
|
||||
probes: [probe]
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const { serviceId, ip, probes } of byServiceIp.values()) {
|
||||
const status = bestAliveIpState(probes.map((probe) => probe.status));
|
||||
const preferred = probes.find((probe) => probe.status === status) ?? probes[0];
|
||||
const list = result.get(serviceId) ?? [];
|
||||
list.push({ ...preferred, ip, status });
|
||||
result.set(serviceId, list);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function mergeHealthAggregates(parts) {
|
||||
const rank = {
|
||||
unknown: 0,
|
||||
@@ -2563,6 +2830,51 @@ function listNotificationLog(db, limit = 50) {
|
||||
LIMIT ${limit}
|
||||
`);
|
||||
}
|
||||
var LOG_RETENTION_LIMITS = {
|
||||
notification_log: 500,
|
||||
failover_log: 1e3,
|
||||
audit_log: 2e3,
|
||||
sync_jobs: 500,
|
||||
health_probe_log: 2e3
|
||||
};
|
||||
function pruneLogs(db) {
|
||||
let deleted = 0;
|
||||
const byId = [
|
||||
{ table: "notification_log", limit: LOG_RETENTION_LIMITS.notification_log },
|
||||
{ table: "failover_log", limit: LOG_RETENTION_LIMITS.failover_log },
|
||||
{ table: "health_probe_log", limit: LOG_RETENTION_LIMITS.health_probe_log }
|
||||
];
|
||||
for (const { table, limit } of byId) {
|
||||
const res = db.run(sql2`
|
||||
DELETE FROM ${sql2.identifier(table)}
|
||||
WHERE id NOT IN (
|
||||
SELECT id FROM ${sql2.identifier(table)}
|
||||
ORDER BY id DESC
|
||||
LIMIT ${limit}
|
||||
)
|
||||
`);
|
||||
deleted += res.changes;
|
||||
}
|
||||
const audit = db.run(sql2`
|
||||
DELETE FROM audit_log
|
||||
WHERE id NOT IN (
|
||||
SELECT id FROM audit_log
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT ${LOG_RETENTION_LIMITS.audit_log}
|
||||
)
|
||||
`);
|
||||
deleted += audit.changes;
|
||||
const jobs = db.run(sql2`
|
||||
DELETE FROM sync_jobs
|
||||
WHERE id NOT IN (
|
||||
SELECT id FROM sync_jobs
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT ${LOG_RETENTION_LIMITS.sync_jobs}
|
||||
)
|
||||
`);
|
||||
deleted += jobs.changes;
|
||||
return deleted;
|
||||
}
|
||||
function insertFailoverLog(db, input) {
|
||||
for (const entry of input.entries) {
|
||||
db.insert(failoverLog).values({
|
||||
@@ -2623,5 +2935,6 @@ export {
|
||||
subdomains,
|
||||
syncJobs,
|
||||
touchVpsTrackerSync,
|
||||
updateAppSettings
|
||||
updateAppSettings,
|
||||
walCheckpointTruncate
|
||||
};
|
||||
|
||||
@@ -23,6 +23,9 @@ export function createDb(databaseUrl: string): { db: Db; sqlite: Sqlite } {
|
||||
sqlite.pragma("journal_mode = WAL");
|
||||
sqlite.pragma("synchronous = NORMAL");
|
||||
sqlite.pragma("foreign_keys = ON");
|
||||
sqlite.pragma("busy_timeout = 5000");
|
||||
sqlite.pragma("cache_size = -16000");
|
||||
sqlite.pragma("wal_autocheckpoint = 1000");
|
||||
const db = drizzle(sqlite, { schema });
|
||||
return { db, sqlite };
|
||||
}
|
||||
@@ -63,3 +66,8 @@ export function runMigrations(sqlite: Sqlite): void {
|
||||
export function healthCheck(sqlite: Sqlite): void {
|
||||
sqlite.prepare("SELECT 1").get();
|
||||
}
|
||||
|
||||
/** Truncate the WAL back into the main DB file; safe to run periodically. */
|
||||
export function walCheckpointTruncate(sqlite: Sqlite): void {
|
||||
sqlite.pragma("wal_checkpoint(TRUNCATE)");
|
||||
}
|
||||
|
||||
@@ -648,6 +648,34 @@ export function listServicesByGroup(db: Db, groupId: number): Service[] {
|
||||
.all() as Service[];
|
||||
}
|
||||
|
||||
/** Batch: services for many groups in one query (hot path: /service-groups). */
|
||||
export function listServicesByGroupIds(
|
||||
db: Db,
|
||||
groupIds: number[],
|
||||
): Map<number, Service[]> {
|
||||
const result = new Map<number, Service[]>();
|
||||
if (groupIds.length === 0) return result;
|
||||
const idList = sql.join(
|
||||
groupIds.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
);
|
||||
const rows = db.all<Service & { service_group_id: number }>(sql`
|
||||
SELECT * FROM services
|
||||
WHERE service_group_id IN (${idList})
|
||||
ORDER BY service_group_id, sort_order, name
|
||||
`);
|
||||
for (const row of rows) {
|
||||
const groupId = row.service_group_id;
|
||||
let list = result.get(groupId);
|
||||
if (!list) {
|
||||
list = [];
|
||||
result.set(groupId, list);
|
||||
}
|
||||
list.push(row);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function listUngroupedServices(db: Db): Service[] {
|
||||
return db
|
||||
.select()
|
||||
@@ -1762,6 +1790,18 @@ export function listAllBindings(db: Db): ServiceBindingView[] {
|
||||
.map((row) => enrichServiceBindingView(db, row));
|
||||
}
|
||||
|
||||
/** Cheap predicate for the weighted-dns scheduler: any weighted row at all? */
|
||||
export function hasWeightedBindings(db: Db): boolean {
|
||||
const row = db.get<{ n: number }>(sql`
|
||||
SELECT (EXISTS (
|
||||
SELECT 1 FROM service_bindings WHERE lb_mode = 'weighted'
|
||||
) OR EXISTS (
|
||||
SELECT 1 FROM service_groups WHERE lb_mode = 'weighted'
|
||||
)) AS n
|
||||
`);
|
||||
return Boolean(row?.n);
|
||||
}
|
||||
|
||||
export function listBindingsByDomain(db: Db, domainId: number): ServiceBindingView[] {
|
||||
return db
|
||||
.all<Omit<ServiceBindingView, "target_ips" | "target_ip_weights" | "target_ip_priorities">>(sql`
|
||||
@@ -1799,6 +1839,163 @@ export function getBinding(db: Db, id: number): ServiceBinding {
|
||||
return mapServiceBinding(row);
|
||||
}
|
||||
|
||||
/** Batch: bindings for many services in one query (hot path: /service-groups). */
|
||||
export function listBindingsByServiceIds(
|
||||
db: Db,
|
||||
serviceIds: number[],
|
||||
): Map<number, Array<ServiceBinding & { zone_name: string }>> {
|
||||
const result = new Map<
|
||||
number,
|
||||
Array<ServiceBinding & { zone_name: string }>
|
||||
>();
|
||||
if (serviceIds.length === 0) return result;
|
||||
const idList = sql.join(
|
||||
serviceIds.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
);
|
||||
const rows = db.all<
|
||||
typeof serviceBindings.$inferSelect & { service_id: number; zone_name: string }
|
||||
>(sql`
|
||||
SELECT sb.*, d.zone_name FROM service_bindings sb
|
||||
JOIN domains d ON d.id = sb.domain_id
|
||||
WHERE sb.service_id IN (${idList})
|
||||
ORDER BY sb.id
|
||||
`);
|
||||
for (const row of rows) {
|
||||
const { service_id, ...binding } = row;
|
||||
let list = result.get(service_id);
|
||||
if (!list) {
|
||||
list = [];
|
||||
result.set(service_id, list);
|
||||
}
|
||||
list.push({
|
||||
...mapServiceBinding({ ...binding, service_id }),
|
||||
zone_name: row.zone_name,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Batch: binding IPs with metadata for many bindings in one query. */
|
||||
export function listBindingIpsWithMetaByBindingIds(
|
||||
db: Db,
|
||||
bindingIds: number[],
|
||||
): Map<number, BindingIpMeta[]> {
|
||||
const result = new Map<number, BindingIpMeta[]>();
|
||||
if (bindingIds.length === 0) return result;
|
||||
const idList = sql.join(
|
||||
bindingIds.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
);
|
||||
const rows = db.all<{
|
||||
binding_id: number;
|
||||
ip: string;
|
||||
weight: number;
|
||||
priority: number;
|
||||
}>(sql`
|
||||
SELECT binding_id, ip, weight, priority FROM service_binding_ips
|
||||
WHERE binding_id IN (${idList})
|
||||
ORDER BY binding_id, rowid
|
||||
`);
|
||||
for (const row of rows) {
|
||||
let list = result.get(row.binding_id);
|
||||
if (!list) {
|
||||
list = [];
|
||||
result.set(row.binding_id, list);
|
||||
}
|
||||
list.push({ ip: row.ip, weight: row.weight, priority: row.priority });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Batch: dns records linked to many bindings in one query. */
|
||||
export function listRecordsByBindingIds(
|
||||
db: Db,
|
||||
bindingIds: number[],
|
||||
): Map<number, DnsRecord[]> {
|
||||
const result = new Map<number, DnsRecord[]>();
|
||||
if (bindingIds.length === 0) return result;
|
||||
const idList = sql.join(
|
||||
bindingIds.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
);
|
||||
const rows = db.all<DnsRecord & { binding_id: number }>(sql`
|
||||
SELECT dr.*, sbr.binding_id AS binding_id FROM dns_records dr
|
||||
INNER JOIN service_binding_records sbr ON sbr.dns_record_id = dr.id
|
||||
WHERE sbr.binding_id IN (${idList})
|
||||
ORDER BY sbr.binding_id, dr.id
|
||||
`);
|
||||
for (const row of rows) {
|
||||
const { binding_id, ...record } = row;
|
||||
let list = result.get(binding_id);
|
||||
if (!list) {
|
||||
list = [];
|
||||
result.set(binding_id, list);
|
||||
}
|
||||
list.push(record);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Batch: binding-scope IP health status for many bindings in one query. */
|
||||
export function listBindingIpHealthByBindingIds(
|
||||
db: Db,
|
||||
bindingIds: number[],
|
||||
): Map<number, Map<string, { status: string; latency_ms: number | null }>> {
|
||||
const result = new Map<number, Map<string, { status: string; latency_ms: number | null }>>();
|
||||
if (bindingIds.length === 0) return result;
|
||||
const idList = sql.join(
|
||||
bindingIds.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
);
|
||||
const rows = db.all<{
|
||||
ref_id: number;
|
||||
ip: string;
|
||||
status: string;
|
||||
latency_ms: number | null;
|
||||
}>(sql`
|
||||
SELECT ref_id, ip, status, latency_ms
|
||||
FROM ip_health_status
|
||||
WHERE scope = 'binding' AND ref_id IN (${idList})
|
||||
`);
|
||||
for (const row of rows) {
|
||||
let byIp = result.get(row.ref_id);
|
||||
if (!byIp) {
|
||||
byIp = new Map();
|
||||
result.set(row.ref_id, byIp);
|
||||
}
|
||||
byIp.set(row.ip, { status: row.status, latency_ms: row.latency_ms });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Batch: service IP rows for many services in one query. */
|
||||
export function listServiceIpRowsByServiceIds(
|
||||
db: Db,
|
||||
serviceIds: number[],
|
||||
): Map<number, ServiceIpRow[]> {
|
||||
const result = new Map<number, ServiceIpRow[]>();
|
||||
if (serviceIds.length === 0) return result;
|
||||
const idList = sql.join(
|
||||
serviceIds.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
);
|
||||
const rows = db.all<{ service_id: number; ip: string; enabled: number }>(sql`
|
||||
SELECT service_id, ip, enabled FROM service_ips
|
||||
WHERE service_id IN (${idList})
|
||||
ORDER BY service_id, rowid
|
||||
`);
|
||||
for (const row of rows) {
|
||||
let list = result.get(row.service_id);
|
||||
if (!list) {
|
||||
list = [];
|
||||
result.set(row.service_id, list);
|
||||
}
|
||||
list.push({ ip: row.ip, enabled: Boolean(row.enabled) });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getBindingView(db: Db, id: number): ServiceBindingView {
|
||||
const rows = db.all<Omit<ServiceBindingView, "target_ips" | "target_ip_weights" | "target_ip_priorities">>(sql`
|
||||
SELECT ${sql.raw(SERVICE_BINDING_SELECT_COLUMNS)}
|
||||
@@ -2113,6 +2310,34 @@ export function listIpHealthStatus(
|
||||
`);
|
||||
}
|
||||
|
||||
/** Batch: latest IP health rows for many bindings in one query. */
|
||||
export function listIpHealthStatusByBindingIds(
|
||||
db: Db,
|
||||
bindingIds: number[],
|
||||
): Map<number, IpHealthStatus[]> {
|
||||
const result = new Map<number, IpHealthStatus[]>();
|
||||
if (bindingIds.length === 0) return result;
|
||||
const idList = sql.join(
|
||||
bindingIds.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
);
|
||||
const rows = db.all<IpHealthStatus>(sql`
|
||||
SELECT scope, ref_id, ip, status, latency_ms, consecutive_failures,
|
||||
last_checked_at, last_error, colo, provider
|
||||
FROM ip_health_status
|
||||
WHERE scope = 'binding' AND ref_id IN (${idList})
|
||||
`);
|
||||
for (const row of rows) {
|
||||
let list = result.get(row.ref_id);
|
||||
if (!list) {
|
||||
list = [];
|
||||
result.set(row.ref_id, list);
|
||||
}
|
||||
list.push(row);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export type HealthAggregate = {
|
||||
health_status: IpHealthState;
|
||||
health_latency_ms: number | null;
|
||||
@@ -3171,6 +3396,59 @@ export function listNotificationLog(
|
||||
`);
|
||||
}
|
||||
|
||||
// --- Log retention ---
|
||||
|
||||
export const LOG_RETENTION_LIMITS = {
|
||||
notification_log: 500,
|
||||
failover_log: 1000,
|
||||
audit_log: 2000,
|
||||
sync_jobs: 500,
|
||||
health_probe_log: 2000,
|
||||
} as const;
|
||||
|
||||
/** Keep only the newest N rows per log table; returns total deleted rows. */
|
||||
export function pruneLogs(db: Db): number {
|
||||
let deleted = 0;
|
||||
// Integer autoincrement PKs — order by id.
|
||||
const byId: Array<{ table: string; limit: number }> = [
|
||||
{ table: "notification_log", limit: LOG_RETENTION_LIMITS.notification_log },
|
||||
{ table: "failover_log", limit: LOG_RETENTION_LIMITS.failover_log },
|
||||
{ table: "health_probe_log", limit: LOG_RETENTION_LIMITS.health_probe_log },
|
||||
];
|
||||
for (const { table, limit } of byId) {
|
||||
const res = db.run(sql`
|
||||
DELETE FROM ${sql.identifier(table)}
|
||||
WHERE id NOT IN (
|
||||
SELECT id FROM ${sql.identifier(table)}
|
||||
ORDER BY id DESC
|
||||
LIMIT ${limit}
|
||||
)
|
||||
`);
|
||||
deleted += res.changes;
|
||||
}
|
||||
// audit_log has TEXT (uuid) PK — order by created_at, id.
|
||||
const audit = db.run(sql`
|
||||
DELETE FROM audit_log
|
||||
WHERE id NOT IN (
|
||||
SELECT id FROM audit_log
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT ${LOG_RETENTION_LIMITS.audit_log}
|
||||
)
|
||||
`);
|
||||
deleted += audit.changes;
|
||||
// sync_jobs has TEXT PK — order by created_at, id.
|
||||
const jobs = db.run(sql`
|
||||
DELETE FROM sync_jobs
|
||||
WHERE id NOT IN (
|
||||
SELECT id FROM sync_jobs
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT ${LOG_RETENTION_LIMITS.sync_jobs}
|
||||
)
|
||||
`);
|
||||
deleted += jobs.changes;
|
||||
return deleted;
|
||||
}
|
||||
|
||||
export type FailoverLogAction = "added" | "removed";
|
||||
|
||||
export type FailoverLogRow = {
|
||||
|
||||
Reference in New Issue
Block a user