mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-30 04:32:53 +00:00
Add resource column to hc and remove —
This commit is contained in:
@@ -189,9 +189,11 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
|
||||
targetId: integer("targetId").references(() => targets.targetId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
orgId: varchar("orgId").references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
orgId: varchar("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
name: varchar("name"),
|
||||
hcEnabled: boolean("hcEnabled").notNull().default(false),
|
||||
hcPath: varchar("hcPath"),
|
||||
|
||||
@@ -209,11 +209,14 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
||||
targetHealthCheckId: integer("targetHealthCheckId").primaryKey({
|
||||
autoIncrement: true
|
||||
}),
|
||||
targetId: integer("targetId")
|
||||
.references(() => targets.targetId, { onDelete: "cascade" }),
|
||||
orgId: text("orgId").references(() => orgs.orgId, {
|
||||
targetId: integer("targetId").references(() => targets.targetId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
orgId: text("orgId")
|
||||
.references(() => orgs.orgId, {
|
||||
onDelete: "cascade"
|
||||
})
|
||||
.notNull(),
|
||||
name: text("name"),
|
||||
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
@@ -294,7 +297,7 @@ export const siteResources = sqliteTable("siteResources", {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
subdomain: text("subdomain"),
|
||||
fullDomain: text("fullDomain"),
|
||||
fullDomain: text("fullDomain")
|
||||
});
|
||||
|
||||
export const networks = sqliteTable("networks", {
|
||||
|
||||
@@ -83,7 +83,7 @@ function formatDataItems(
|
||||
.replace(/([A-Z])/g, " $1")
|
||||
.replace(/^./, (s) => s.toUpperCase())
|
||||
.trim(),
|
||||
value: String(value ?? "—")
|
||||
value: String(value ?? "-")
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -137,4 +137,4 @@ export const AlertNotification = ({ eventType, orgId, data }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertNotification;
|
||||
export default AlertNotification;
|
||||
|
||||
@@ -32,7 +32,7 @@ export const EnterpriseEditionKeyGenerated = ({
|
||||
}: EnterpriseEditionKeyGeneratedProps) => {
|
||||
const previewText = personalUseOnly
|
||||
? "Your Enterprise Edition key for personal use is ready"
|
||||
: "Thank you for your purchase — your Enterprise Edition key is ready";
|
||||
: "Thank you for your purchase - your Enterprise Edition key is ready";
|
||||
|
||||
return (
|
||||
<Html>
|
||||
|
||||
@@ -825,7 +825,7 @@ async function handleSubnetProxyTargetUpdates(
|
||||
// Check if this client still has access to another resource
|
||||
// on this specific site with the same destination. We scope
|
||||
// by siteId (via siteNetworks) rather than networkId because
|
||||
// removePeerData operates per-site — a resource on a different
|
||||
// removePeerData operates per-site - a resource on a different
|
||||
// site sharing the same network should not block removal here.
|
||||
const destinationStillInUse = await trx
|
||||
.select()
|
||||
@@ -980,7 +980,7 @@ export async function rebuildClientAssociationsFromClient(
|
||||
)
|
||||
: [];
|
||||
|
||||
// Group by siteId for site-level associations — look up via siteNetworks since
|
||||
// Group by siteId for site-level associations - look up via siteNetworks since
|
||||
// siteResources no longer carries a direct siteId column.
|
||||
const networkIds = Array.from(
|
||||
new Set(
|
||||
@@ -1459,7 +1459,7 @@ async function handleMessagesForClientResources(
|
||||
// Check if this client still has access to another resource
|
||||
// on this specific site with the same destination. We scope
|
||||
// by siteId (via siteNetworks) rather than networkId because
|
||||
// removePeerData operates per-site — a resource on a different
|
||||
// removePeerData operates per-site - a resource on a different
|
||||
// site sharing the same network should not block removal here.
|
||||
const destinationStillInUse = await trx
|
||||
.select()
|
||||
|
||||
@@ -1011,7 +1011,7 @@ export class TraefikConfigManager {
|
||||
);
|
||||
|
||||
if (!isUnused) {
|
||||
// Domain is still active — remove from pending deletion if it was queued
|
||||
// Domain is still active - remove from pending deletion if it was queued
|
||||
if (this.pendingDeletion.has(dirName)) {
|
||||
logger.info(
|
||||
`Certificate ${dirName} is active again, cancelling pending deletion`
|
||||
@@ -1021,7 +1021,7 @@ export class TraefikConfigManager {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Domain is unused — add to pending deletion or decrement its counter
|
||||
// Domain is unused - add to pending deletion or decrement its counter
|
||||
if (!this.pendingDeletion.has(dirName)) {
|
||||
const graceCycles = 3;
|
||||
logger.info(
|
||||
@@ -1036,7 +1036,7 @@ export class TraefikConfigManager {
|
||||
);
|
||||
this.pendingDeletion.set(dirName, remaining);
|
||||
} else {
|
||||
// Grace period expired — actually delete now
|
||||
// Grace period expired - actually delete now
|
||||
this.pendingDeletion.delete(dirName);
|
||||
|
||||
const domainDir = path.join(certsPath, dirName);
|
||||
|
||||
@@ -24,7 +24,7 @@ function encodePath(path: string | null | undefined): string {
|
||||
|
||||
/**
|
||||
* Exact replica of the OLD key computation from upstream main.
|
||||
* Uses sanitize() for paths — this is what had the collision bug.
|
||||
* Uses sanitize() for paths - this is what had the collision bug.
|
||||
*/
|
||||
function oldKeyComputation(
|
||||
resourceId: number,
|
||||
@@ -44,7 +44,7 @@ function oldKeyComputation(
|
||||
|
||||
/**
|
||||
* Replica of the NEW key computation from our fix.
|
||||
* Uses encodePath() for paths — collision-free.
|
||||
* Uses encodePath() for paths - collision-free.
|
||||
*/
|
||||
function newKeyComputation(
|
||||
resourceId: number,
|
||||
@@ -195,11 +195,11 @@ function runTests() {
|
||||
true,
|
||||
"/a/b and /a-b MUST have different keys"
|
||||
);
|
||||
console.log(" PASS: collision fix — /a/b vs /a-b have different keys");
|
||||
console.log(" PASS: collision fix - /a/b vs /a-b have different keys");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 9: demonstrate the old bug — old code maps /a/b and /a-b to same key
|
||||
// Test 9: demonstrate the old bug - old code maps /a/b and /a-b to same key
|
||||
{
|
||||
const oldKeyAB = oldKeyComputation(1, "/a/b", "prefix", null, null);
|
||||
const oldKeyDash = oldKeyComputation(1, "/a-b", "prefix", null, null);
|
||||
@@ -208,11 +208,11 @@ function runTests() {
|
||||
oldKeyDash,
|
||||
"old code MUST have this collision (confirms the bug exists)"
|
||||
);
|
||||
console.log(" PASS: confirmed old code bug — /a/b and /a-b collided");
|
||||
console.log(" PASS: confirmed old code bug - /a/b and /a-b collided");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 10: /api/v1 and /api-v1 — old code collision, new code fixes it
|
||||
// Test 10: /api/v1 and /api-v1 - old code collision, new code fixes it
|
||||
{
|
||||
const oldKey1 = oldKeyComputation(1, "/api/v1", "prefix", null, null);
|
||||
const oldKey2 = oldKeyComputation(1, "/api-v1", "prefix", null, null);
|
||||
@@ -229,11 +229,11 @@ function runTests() {
|
||||
true,
|
||||
"new code must separate /api/v1 and /api-v1"
|
||||
);
|
||||
console.log(" PASS: collision fix — /api/v1 vs /api-v1");
|
||||
console.log(" PASS: collision fix - /api/v1 vs /api-v1");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 11: /app.v2 and /app/v2 and /app-v2 — three-way collision fixed
|
||||
// Test 11: /app.v2 and /app/v2 and /app-v2 - three-way collision fixed
|
||||
{
|
||||
const a = newKeyComputation(1, "/app.v2", "prefix", null, null);
|
||||
const b = newKeyComputation(1, "/app/v2", "prefix", null, null);
|
||||
@@ -245,14 +245,14 @@ function runTests() {
|
||||
"three paths must produce three unique keys"
|
||||
);
|
||||
console.log(
|
||||
" PASS: collision fix — three-way /app.v2, /app/v2, /app-v2"
|
||||
" PASS: collision fix - three-way /app.v2, /app/v2, /app-v2"
|
||||
);
|
||||
passed++;
|
||||
}
|
||||
|
||||
// ── Edge cases ───────────────────────────────────────────────────
|
||||
|
||||
// Test 12: same path in different resources — always separate
|
||||
// Test 12: same path in different resources - always separate
|
||||
{
|
||||
const key1 = newKeyComputation(1, "/api", "prefix", null, null);
|
||||
const key2 = newKeyComputation(2, "/api", "prefix", null, null);
|
||||
@@ -261,11 +261,11 @@ function runTests() {
|
||||
true,
|
||||
"different resources with same path must have different keys"
|
||||
);
|
||||
console.log(" PASS: edge case — same path, different resources");
|
||||
console.log(" PASS: edge case - same path, different resources");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 13: same resource, different pathMatchType — separate keys
|
||||
// Test 13: same resource, different pathMatchType - separate keys
|
||||
{
|
||||
const exact = newKeyComputation(1, "/api", "exact", null, null);
|
||||
const prefix = newKeyComputation(1, "/api", "prefix", null, null);
|
||||
@@ -274,11 +274,11 @@ function runTests() {
|
||||
true,
|
||||
"exact vs prefix must have different keys"
|
||||
);
|
||||
console.log(" PASS: edge case — same path, different match types");
|
||||
console.log(" PASS: edge case - same path, different match types");
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 14: same resource and path, different rewrite config — separate keys
|
||||
// Test 14: same resource and path, different rewrite config - separate keys
|
||||
{
|
||||
const noRewrite = newKeyComputation(1, "/api", "prefix", null, null);
|
||||
const withRewrite = newKeyComputation(
|
||||
@@ -293,7 +293,7 @@ function runTests() {
|
||||
true,
|
||||
"with vs without rewrite must have different keys"
|
||||
);
|
||||
console.log(" PASS: edge case — same path, different rewrite config");
|
||||
console.log(" PASS: edge case - same path, different rewrite config");
|
||||
passed++;
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ function runTests() {
|
||||
paths.length,
|
||||
"special URL chars must produce unique keys"
|
||||
);
|
||||
console.log(" PASS: edge case — special URL characters in paths");
|
||||
console.log(" PASS: edge case - special URL characters in paths");
|
||||
passed++;
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ async function pushCertUpdateToAffectedNewts(
|
||||
await cache.del(`cert:${resource.fullDomain}`);
|
||||
}
|
||||
|
||||
// Generate target once — same cert applies to all sites for this resource
|
||||
// Generate target once - same cert applies to all sites for this resource
|
||||
const newTargets = await generateSubnetProxyTargetV2(
|
||||
resource,
|
||||
resourceClients
|
||||
@@ -157,7 +157,7 @@ async function pushCertUpdateToAffectedNewts(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Construct the old targets — same routing shape but with the previous cert/key.
|
||||
// Construct the old targets - same routing shape but with the previous cert/key.
|
||||
// The newt only uses destPrefix/sourcePrefixes for removal, but we keep the
|
||||
// semantics correct so the update message accurately reflects what changed.
|
||||
const oldTargets: SubnetProxyTargetV2[] = newTargets.map((t) => ({
|
||||
|
||||
@@ -153,7 +153,7 @@ export async function flushConnectionLogToDb(): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
// Stop processing further batches from this snapshot — they will
|
||||
// Stop processing further batches from this snapshot - they will
|
||||
// be picked up via the re-queued records on the next flush.
|
||||
const remaining = snapshot.slice(i + INSERT_BATCH_SIZE);
|
||||
if (remaining.length > 0) {
|
||||
@@ -180,7 +180,7 @@ const flushTimer = setInterval(async () => {
|
||||
}, FLUSH_INTERVAL_MS);
|
||||
|
||||
// Calling unref() means this timer will not keep the Node.js event loop alive
|
||||
// on its own — the process can still exit normally when there is no other work
|
||||
// on its own - the process can still exit normally when there is no other work
|
||||
// left. The graceful-shutdown path will call flushConnectionLogToDb() explicitly
|
||||
// before process.exit(), so no data is lost.
|
||||
flushTimer.unref();
|
||||
@@ -223,7 +223,7 @@ export function logConnectionAudit(record: ConnectionLogRecord): void {
|
||||
buffer.push(record);
|
||||
|
||||
if (buffer.length >= MAX_BUFFERED_RECORDS) {
|
||||
// Fire and forget — errors are handled inside flushConnectionLogToDb
|
||||
// Fire and forget - errors are handled inside flushConnectionLogToDb
|
||||
flushConnectionLogToDb().catch((error) => {
|
||||
logger.error(
|
||||
"Unexpected error during size-triggered connection log flush:",
|
||||
@@ -231,4 +231,4 @@ export function logConnectionAudit(record: ConnectionLogRecord): void {
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ const DEFAULT_FORMAT: PayloadFormat = "json_array";
|
||||
*
|
||||
* **Payload formats** (controlled by `config.format`):
|
||||
*
|
||||
* - `json_array` (default) — one POST per batch, body is a JSON array:
|
||||
* - `json_array` (default) - one POST per batch, body is a JSON array:
|
||||
* ```json
|
||||
* [
|
||||
* { "event": "request", "timestamp": "2024-01-01T00:00:00.000Z", "data": { … } },
|
||||
@@ -46,7 +46,7 @@ const DEFAULT_FORMAT: PayloadFormat = "json_array";
|
||||
* ```
|
||||
* `Content-Type: application/json`
|
||||
*
|
||||
* - `ndjson` — one POST per batch, body is newline-delimited JSON (one object
|
||||
* - `ndjson` - one POST per batch, body is newline-delimited JSON (one object
|
||||
* per line, no outer array). Required by Splunk HEC, Elastic/OpenSearch,
|
||||
* and Grafana Loki:
|
||||
* ```
|
||||
@@ -55,7 +55,7 @@ const DEFAULT_FORMAT: PayloadFormat = "json_array";
|
||||
* ```
|
||||
* `Content-Type: application/x-ndjson`
|
||||
*
|
||||
* - `json_single` — one POST **per event**, body is a plain JSON object.
|
||||
* - `json_single` - one POST **per event**, body is a plain JSON object.
|
||||
* Use only for endpoints that cannot handle batches at all.
|
||||
*
|
||||
* With a body template each event is rendered through the template before
|
||||
@@ -319,4 +319,4 @@ function epochSecondsToIso(epochSeconds: number): string {
|
||||
function escapeJsonString(value: string): string {
|
||||
// JSON.stringify produces `"<escaped>"` – strip the outer quotes.
|
||||
return JSON.stringify(value).slice(1, -1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +60,9 @@ export type AuthType = "none" | "bearer" | "basic" | "custom";
|
||||
/**
|
||||
* Controls how the batch of events is serialised into the HTTP request body.
|
||||
*
|
||||
* - `json_array` – `[{…}, {…}]` — default; one POST per batch wrapped in a
|
||||
* - `json_array` – `[{…}, {…}]` - default; one POST per batch wrapped in a
|
||||
* JSON array. Works with most generic webhooks and Datadog.
|
||||
* - `ndjson` – `{…}\n{…}` — newline-delimited JSON, one object per
|
||||
* - `ndjson` – `{…}\n{…}` - newline-delimited JSON, one object per
|
||||
* line. Required by Splunk HEC, Elastic/OpenSearch, Loki.
|
||||
* - `json_single` – one HTTP POST per event, body is a plain JSON object.
|
||||
* Use only for endpoints that cannot handle batches at all.
|
||||
@@ -131,4 +131,4 @@ export interface DestinationFailureState {
|
||||
nextRetryAt: number;
|
||||
/** Date.now() value of the very first failure in the current streak */
|
||||
firstFailedAt: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ export async function getTraefikConfig(
|
||||
});
|
||||
});
|
||||
|
||||
// Query siteResources in HTTP mode with SSL enabled and aliases — cert generation / HTTPS edge
|
||||
// Query siteResources in HTTP mode with SSL enabled and aliases - cert generation / HTTPS edge
|
||||
const siteResourcesWithFullDomain = await db
|
||||
.select({
|
||||
siteResourceId: siteResources.siteResourceId,
|
||||
@@ -1010,7 +1010,7 @@ export async function getTraefikConfig(
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPS router — presence of this entry triggers cert generation
|
||||
// HTTPS router - presence of this entry triggers cert generation
|
||||
config_output.http.routers[siteResourceRouterName] = {
|
||||
entryPoints: [
|
||||
config.getRawConfig().traefik.https_entrypoint
|
||||
@@ -1022,7 +1022,7 @@ export async function getTraefikConfig(
|
||||
tls
|
||||
};
|
||||
|
||||
// Assets bypass router — lets Next.js static files load without rewrite
|
||||
// Assets bypass router - lets Next.js static files load without rewrite
|
||||
config_output.http.routers[`${siteResourceRouterName}-assets`] = {
|
||||
entryPoints: [
|
||||
config.getRawConfig().traefik.https_entrypoint
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { db, targetHealthCheck } from "@server/db";
|
||||
import { db, targetHealthCheck, targets, resources } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -84,12 +84,36 @@ export async function listHealthChecks(
|
||||
|
||||
const whereClause = and(
|
||||
eq(targetHealthCheck.orgId, orgId),
|
||||
isNull(targetHealthCheck.targetId)
|
||||
);
|
||||
|
||||
const list = await db
|
||||
.select()
|
||||
.select({
|
||||
targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
|
||||
name: targetHealthCheck.name,
|
||||
hcEnabled: targetHealthCheck.hcEnabled,
|
||||
hcHealth: targetHealthCheck.hcHealth,
|
||||
hcMode: targetHealthCheck.hcMode,
|
||||
hcHostname: targetHealthCheck.hcHostname,
|
||||
hcPort: targetHealthCheck.hcPort,
|
||||
hcPath: targetHealthCheck.hcPath,
|
||||
hcScheme: targetHealthCheck.hcScheme,
|
||||
hcMethod: targetHealthCheck.hcMethod,
|
||||
hcInterval: targetHealthCheck.hcInterval,
|
||||
hcUnhealthyInterval: targetHealthCheck.hcUnhealthyInterval,
|
||||
hcTimeout: targetHealthCheck.hcTimeout,
|
||||
hcHeaders: targetHealthCheck.hcHeaders,
|
||||
hcFollowRedirects: targetHealthCheck.hcFollowRedirects,
|
||||
hcStatus: targetHealthCheck.hcStatus,
|
||||
hcTlsServerName: targetHealthCheck.hcTlsServerName,
|
||||
hcHealthyThreshold: targetHealthCheck.hcHealthyThreshold,
|
||||
hcUnhealthyThreshold: targetHealthCheck.hcUnhealthyThreshold,
|
||||
resourceId: resources.resourceId,
|
||||
resourceName: resources.name,
|
||||
resourceNiceId: resources.niceId
|
||||
})
|
||||
.from(targetHealthCheck)
|
||||
.leftJoin(targets, eq(targetHealthCheck.targetId, targets.targetId))
|
||||
.leftJoin(resources, eq(targets.resourceId, resources.resourceId))
|
||||
.where(whereClause)
|
||||
.orderBy(sql`${targetHealthCheck.targetHealthCheckId} DESC`)
|
||||
.limit(limit)
|
||||
@@ -124,7 +148,10 @@ export async function listHealthChecks(
|
||||
hcStatus: row.hcStatus ?? null,
|
||||
hcTlsServerName: row.hcTlsServerName ?? null,
|
||||
hcHealthyThreshold: row.hcHealthyThreshold ?? null,
|
||||
hcUnhealthyThreshold: row.hcUnhealthyThreshold ?? null
|
||||
hcUnhealthyThreshold: row.hcUnhealthyThreshold ?? null,
|
||||
resourceId: row.resourceId ?? null,
|
||||
resourceName: row.resourceName ?? null,
|
||||
resourceNiceId: row.resourceNiceId ?? null
|
||||
})),
|
||||
pagination: {
|
||||
total: count,
|
||||
|
||||
@@ -88,11 +88,11 @@ async function dbQueryRows<T extends Record<string, unknown>>(
|
||||
): Promise<T[]> {
|
||||
const anyDb = db as any;
|
||||
if (typeof anyDb.execute === "function") {
|
||||
// PostgreSQL (node-postgres via Drizzle) — returns { rows: [...] } or an array
|
||||
// PostgreSQL (node-postgres via Drizzle) - returns { rows: [...] } or an array
|
||||
const result = await anyDb.execute(query);
|
||||
return (Array.isArray(result) ? result : (result.rows ?? [])) as T[];
|
||||
}
|
||||
// SQLite (better-sqlite3 via Drizzle) — returns an array directly
|
||||
// SQLite (better-sqlite3 via Drizzle) - returns an array directly
|
||||
return (await anyDb.all(query)) as T[];
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ function isSQLite(): boolean {
|
||||
* Swaps out the accumulator before writing so that any bandwidth messages
|
||||
* received during the flush are captured in the new accumulator rather than
|
||||
* being lost or causing contention. Sites are updated in chunks via a single
|
||||
* batch UPDATE per chunk. Failed chunks are discarded — exact per-flush
|
||||
* batch UPDATE per chunk. Failed chunks are discarded - exact per-flush
|
||||
* accuracy is not critical and re-queuing is not worth the added complexity.
|
||||
*
|
||||
* This function is exported so that the application's graceful-shutdown
|
||||
@@ -125,7 +125,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
||||
const currentTime = new Date().toISOString();
|
||||
|
||||
// Sort by publicKey for consistent lock ordering across concurrent
|
||||
// writers — deadlock-prevention strategy.
|
||||
// writers - deadlock-prevention strategy.
|
||||
const sortedEntries = [...snapshot.entries()].sort(([a], [b]) =>
|
||||
a.localeCompare(b)
|
||||
);
|
||||
@@ -150,7 +150,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
||||
try {
|
||||
rows = await withDeadlockRetry(async () => {
|
||||
if (isSQLite()) {
|
||||
// SQLite: one UPDATE per row — no need for batch efficiency here.
|
||||
// SQLite: one UPDATE per row - no need for batch efficiency here.
|
||||
const results: { orgId: string; pubKey: string }[] = [];
|
||||
for (const [publicKey, { bytesIn, bytesOut }] of chunk) {
|
||||
const result = await dbQueryRows<{
|
||||
@@ -170,7 +170,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
||||
return results;
|
||||
}
|
||||
|
||||
// PostgreSQL: batch UPDATE … FROM (VALUES …) — single round-trip per chunk.
|
||||
// PostgreSQL: batch UPDATE … FROM (VALUES …) - single round-trip per chunk.
|
||||
const valuesList = chunk.map(([publicKey, { bytesIn, bytesOut }]) =>
|
||||
sql`(${publicKey}::text, ${bytesIn}::real, ${bytesOut}::real)`
|
||||
);
|
||||
@@ -191,7 +191,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
||||
`Failed to flush bandwidth chunk [${i}–${chunkEnd}], discarding ${chunk.length} site(s):`,
|
||||
error
|
||||
);
|
||||
// Discard the chunk — exact per-flush accuracy is not critical.
|
||||
// Discard the chunk - exact per-flush accuracy is not critical.
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
||||
totalBandwidth
|
||||
);
|
||||
if (bandwidthUsage) {
|
||||
// Fire-and-forget — don't block the flush on limit checking.
|
||||
// Fire-and-forget - don't block the flush on limit checking.
|
||||
usageService
|
||||
.checkLimitSet(
|
||||
orgId,
|
||||
@@ -298,7 +298,7 @@ export async function updateSiteBandwidth(
|
||||
exitNodeId?: number
|
||||
): Promise<void> {
|
||||
for (const { publicKey, bytesIn, bytesOut } of bandwidthData) {
|
||||
// Skip peers that haven't transferred any data — writing zeros to the
|
||||
// Skip peers that haven't transferred any data - writing zeros to the
|
||||
// database would be a no-op anyway.
|
||||
if (bytesIn <= 0 && bytesOut <= 0) {
|
||||
continue;
|
||||
|
||||
@@ -19,6 +19,9 @@ export type ListHealthChecksResponse = {
|
||||
hcTlsServerName: string | null;
|
||||
hcHealthyThreshold: number | null;
|
||||
hcUnhealthyThreshold: number | null;
|
||||
resourceId: number | null;
|
||||
resourceName: string | null;
|
||||
resourceNiceId: string | null;
|
||||
}[];
|
||||
pagination: {
|
||||
total: number;
|
||||
|
||||
@@ -88,7 +88,7 @@ export async function flushBandwidthToDb(): Promise<void> {
|
||||
const currentTime = new Date().toISOString();
|
||||
|
||||
// Sort by publicKey for consistent lock ordering across concurrent
|
||||
// writers — this is the same deadlock-prevention strategy used in the
|
||||
// writers - this is the same deadlock-prevention strategy used in the
|
||||
// original per-message implementation.
|
||||
const sortedEntries = [...snapshot.entries()].sort(([a], [b]) =>
|
||||
a.localeCompare(b)
|
||||
@@ -143,7 +143,7 @@ const flushTimer = setInterval(async () => {
|
||||
}, FLUSH_INTERVAL_MS);
|
||||
|
||||
// Calling unref() means this timer will not keep the Node.js event loop alive
|
||||
// on its own — the process can still exit normally when there is no other work
|
||||
// on its own - the process can still exit normally when there is no other work
|
||||
// left. The graceful-shutdown path (see server/cleanup.ts) will call
|
||||
// flushBandwidthToDb() explicitly before process.exit(), so no data is lost.
|
||||
flushTimer.unref();
|
||||
@@ -167,7 +167,7 @@ export const handleReceiveBandwidthMessage: MessageHandler = async (
|
||||
// Accumulate the incoming data in memory; the periodic timer (and the
|
||||
// shutdown hook) will take care of writing it to the database.
|
||||
for (const { publicKey, bytesIn, bytesOut } of bandwidthData) {
|
||||
// Skip peers that haven't transferred any data — writing zeros to the
|
||||
// Skip peers that haven't transferred any data - writing zeros to the
|
||||
// database would be a no-op anyway.
|
||||
if (bytesIn <= 0 && bytesOut <= 0) {
|
||||
continue;
|
||||
|
||||
@@ -16,7 +16,7 @@ const OFFLINE_THRESHOLD_BANDWIDTH_MS = 8 * 60 * 1000; // 8 minutes
|
||||
* Starts the background interval that checks for newt sites that haven't
|
||||
* pinged recently and marks them as offline. For backward compatibility,
|
||||
* a site is only marked offline when there is no active WebSocket connection
|
||||
* either — so older newt versions that don't send pings but remain connected
|
||||
* either - so older newt versions that don't send pings but remain connected
|
||||
* continue to be treated as online.
|
||||
*/
|
||||
export const startNewtOfflineChecker = (): void => {
|
||||
@@ -63,7 +63,7 @@ export const startNewtOfflineChecker = (): void => {
|
||||
);
|
||||
if (isConnected) {
|
||||
logger.debug(
|
||||
`Newt ${staleSite.newtId} has not pinged recently but is still connected via WebSocket — keeping site ${staleSite.siteId} online`
|
||||
`Newt ${staleSite.newtId} has not pinged recently but is still connected via WebSocket - keeping site ${staleSite.siteId} online`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ async function flushSitePingsToDb(): Promise<void> {
|
||||
|
||||
try {
|
||||
const newlyOnlineSites = await withRetry(async () => {
|
||||
// Only update sites that were offline — these are the
|
||||
// Only update sites that were offline - these are the
|
||||
// offline→online transitions. .returning() gives us exactly
|
||||
// the site IDs that changed state.
|
||||
const transitioned = await db
|
||||
@@ -249,7 +249,7 @@ async function flushClientPingsToDb(): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush everything — called by the interval timer and during shutdown.
|
||||
* Flush everything - called by the interval timer and during shutdown.
|
||||
*/
|
||||
export async function flushPingsToDb(): Promise<void> {
|
||||
await flushSitePingsToDb();
|
||||
@@ -314,7 +314,7 @@ function isTransientError(error: any): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// PostgreSQL deadlock detected — always safe to retry (one winner guaranteed)
|
||||
// PostgreSQL deadlock detected - always safe to retry (one winner guaranteed)
|
||||
if (code === "40P01" || message.includes("deadlock")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ export async function registerNewt(
|
||||
dateCreated: moment().toISOString()
|
||||
});
|
||||
|
||||
// Consume the provisioning key — cascade removes siteProvisioningKeyOrg
|
||||
// Consume the provisioning key - cascade removes siteProvisioningKeyOrg
|
||||
await trx
|
||||
.update(siteProvisioningKeys)
|
||||
.set({
|
||||
|
||||
@@ -211,7 +211,7 @@ export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trigger the peer add handshake — if the peer was already added this will be a no-op
|
||||
// Trigger the peer add handshake - if the peer was already added this will be a no-op
|
||||
await initPeerAddHandshake(
|
||||
client.clientId,
|
||||
{
|
||||
@@ -236,4 +236,4 @@ export const handleOlmServerInitAddPeerHandshake: MessageHandler = async (
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -228,7 +228,7 @@ export async function createTarget(
|
||||
healthCheck = await db
|
||||
.insert(targetHealthCheck)
|
||||
.values({
|
||||
name: `${targetData.ip}:${targetData.port}`,
|
||||
orgId: resource.orgId,
|
||||
targetId: newTarget[0].targetId,
|
||||
hcEnabled: targetData.hcEnabled ?? false,
|
||||
hcPath: targetData.hcPath ?? null,
|
||||
|
||||
@@ -47,7 +47,7 @@ export const messageHandlers: Record<string, MessageHandler> = {
|
||||
"ws/round-trip/complete": handleRoundTripMessage
|
||||
};
|
||||
|
||||
// Start the ping accumulator for all builds — it batches per-site online/lastPing
|
||||
// Start the ping accumulator for all builds - it batches per-site online/lastPing
|
||||
// updates into periodic bulk writes, preventing connection pool exhaustion.
|
||||
startPingAccumulator();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user