perf: avoid repeated client scans during CIDR changes (#2707)
* perf: avoid repeated client scans during CIDR changes (#2706) * refactor: retain CIDR change guards * perf: avoid repeated client reads during migration --------- Co-authored-by: potatosips <potatosips@users.noreply.github.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import { z } from 'zod';
|
||||
|
||||
import Database from '#server/utils/Database';
|
||||
import { defineSetupEventHandler } from '#server/utils/handler';
|
||||
import { nextIP } from '#server/utils/ip';
|
||||
import { nextIPFromUsedAddresses } from '#server/utils/ip';
|
||||
import { FileSchema, validateZod } from '#server/utils/types';
|
||||
|
||||
export default defineSetupEventHandler('migrate', async ({ event }) => {
|
||||
@@ -58,6 +58,9 @@ export default defineSetupEventHandler('migrate', async ({ event }) => {
|
||||
`/${ipv4Cidr.prefix}`,
|
||||
ipv6Cidr: ipv6Cidr.cidr,
|
||||
});
|
||||
const ipv6Addresses = new Set(
|
||||
(await Database.clients.getAll()).map((client) => client.ipv6Address)
|
||||
);
|
||||
|
||||
for (const clientId in oldConfig.clients) {
|
||||
const clientConfig = oldConfig.clients[clientId];
|
||||
@@ -66,15 +69,14 @@ export default defineSetupEventHandler('migrate', async ({ event }) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
const clients = await Database.clients.getAll();
|
||||
|
||||
const ipv6Address = nextIP(6, ipv6Cidr, clients);
|
||||
const ipv6Address = nextIPFromUsedAddresses(6, ipv6Cidr, ipv6Addresses);
|
||||
|
||||
await Database.clients.createFromExisting({
|
||||
...clientConfig,
|
||||
ipv4Address: clientConfig.address,
|
||||
ipv6Address,
|
||||
});
|
||||
ipv6Addresses.add(ipv6Address);
|
||||
}
|
||||
|
||||
await Database.general.setSetupStep(0);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { parseCidr } from 'cidr-tools';
|
||||
import { wgInterface } from './schema';
|
||||
import type { InterfaceCidrUpdateType, InterfaceUpdateType } from './types';
|
||||
|
||||
import { nextIP } from '#server/utils/ip';
|
||||
import { nextIPFromUsedAddresses } from '#server/utils/ip';
|
||||
import { client as clientSchema } from '#db/schema';
|
||||
import type { DBType } from '#db/sqlite';
|
||||
|
||||
@@ -93,21 +93,36 @@ export class InterfaceService {
|
||||
.execute();
|
||||
|
||||
const clients = await tx.query.client.findMany().execute();
|
||||
const ipv4Addresses = new Set(
|
||||
clients.map((client) => client.ipv4Address)
|
||||
);
|
||||
const ipv6Addresses = new Set(
|
||||
clients.map((client) => client.ipv6Address)
|
||||
);
|
||||
|
||||
for (const client of clients) {
|
||||
// TODO: optimize
|
||||
const clients = await tx.query.client.findMany().execute();
|
||||
|
||||
// only calculate ip if cidr has changed
|
||||
|
||||
let nextIpv4 = client.ipv4Address;
|
||||
if (data.ipv4Cidr !== oldCidr.ipv4Cidr) {
|
||||
nextIpv4 = nextIP(4, parseCidr(data.ipv4Cidr), clients);
|
||||
nextIpv4 = nextIPFromUsedAddresses(
|
||||
4,
|
||||
parseCidr(data.ipv4Cidr),
|
||||
ipv4Addresses
|
||||
);
|
||||
ipv4Addresses.add(nextIpv4);
|
||||
ipv4Addresses.delete(client.ipv4Address);
|
||||
}
|
||||
|
||||
let nextIpv6 = client.ipv6Address;
|
||||
if (data.ipv6Cidr !== oldCidr.ipv6Cidr) {
|
||||
nextIpv6 = nextIP(6, parseCidr(data.ipv6Cidr), clients);
|
||||
nextIpv6 = nextIPFromUsedAddresses(
|
||||
6,
|
||||
parseCidr(data.ipv6Cidr),
|
||||
ipv6Addresses
|
||||
);
|
||||
ipv6Addresses.add(nextIpv6);
|
||||
ipv6Addresses.delete(client.ipv6Address);
|
||||
}
|
||||
|
||||
await tx
|
||||
|
||||
+13
-4
@@ -13,15 +13,24 @@ export function nextIP(
|
||||
version: 4 | 6,
|
||||
cidr: ParsedCidr,
|
||||
clients: ClientNextIpType[]
|
||||
) {
|
||||
const usedAddresses = new Set(
|
||||
clients.map((client) => client[`ipv${version}Address`])
|
||||
);
|
||||
|
||||
return nextIPFromUsedAddresses(version, cidr, usedAddresses);
|
||||
}
|
||||
|
||||
export function nextIPFromUsedAddresses(
|
||||
version: 4 | 6,
|
||||
cidr: ParsedCidr,
|
||||
usedAddresses: Set<string>
|
||||
) {
|
||||
let address;
|
||||
for (let i = cidr.start + 2n; i <= cidr.end - 1n; i++) {
|
||||
const currentIp = stringifyIp({ number: i, version: version });
|
||||
const client = clients.find((client) => {
|
||||
return client[`ipv${version}Address`] === currentIp;
|
||||
});
|
||||
|
||||
if (!client) {
|
||||
if (!usedAddresses.has(currentIp)) {
|
||||
address = currentIp;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { parseCidr } from 'cidr-tools';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { nextIPFromUsedAddresses } from '#server/utils/ip';
|
||||
|
||||
describe('nextIPFromUsedAddresses', () => {
|
||||
test('returns the first available address without scanning client records', () => {
|
||||
const usedAddresses = new Set(['10.0.0.2', '10.0.0.3']);
|
||||
|
||||
expect(
|
||||
nextIPFromUsedAddresses(4, parseCidr('10.0.0.0/29'), usedAddresses)
|
||||
).toBe('10.0.0.4');
|
||||
});
|
||||
|
||||
test('throws when every usable address is allocated', () => {
|
||||
const usedAddresses = new Set([
|
||||
'10.0.0.2',
|
||||
'10.0.0.3',
|
||||
'10.0.0.4',
|
||||
'10.0.0.5',
|
||||
'10.0.0.6',
|
||||
]);
|
||||
|
||||
expect(() =>
|
||||
nextIPFromUsedAddresses(4, parseCidr('10.0.0.0/29'), usedAddresses)
|
||||
).toThrow('Maximum number of clients reached');
|
||||
});
|
||||
|
||||
test('supports replacing addresses while retaining the current allocation state', () => {
|
||||
const cidr = parseCidr('10.0.0.0/29');
|
||||
const usedAddresses = new Set(['10.0.0.2', '10.0.0.3']);
|
||||
|
||||
const firstReplacement = nextIPFromUsedAddresses(4, cidr, usedAddresses);
|
||||
usedAddresses.add(firstReplacement);
|
||||
usedAddresses.delete('10.0.0.2');
|
||||
|
||||
expect(firstReplacement).toBe('10.0.0.4');
|
||||
expect(nextIPFromUsedAddresses(4, cidr, usedAddresses)).toBe('10.0.0.2');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user