feat(performance): Add async utility functions and filesystem utilities
- Implemented async utilities including delay, retryWithBackoff, withTimeout, parallelLimit, debounceAsync, AsyncMutex, and CircuitBreaker. - Created tests for async utilities to ensure functionality and reliability. - Developed AsyncFileSystem class with methods for file and directory operations, including ensureDir, readFile, writeFile, remove, and more. - Added tests for filesystem utilities to validate file operations and error handling.
This commit is contained in:
@ -3,6 +3,8 @@ import { promisify } from 'util';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { delay } from '../../core/utils/async-utils.js';
|
||||
import { AsyncFileSystem } from '../../core/utils/fs-utils.js';
|
||||
import {
|
||||
NftBaseError,
|
||||
NftValidationError,
|
||||
@ -208,7 +210,7 @@ export class NfTablesProxy {
|
||||
|
||||
// Wait before retry, unless it's the last attempt
|
||||
if (i < maxRetries - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelayMs));
|
||||
await delay(retryDelayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -218,8 +220,13 @@ export class NfTablesProxy {
|
||||
|
||||
/**
|
||||
* Execute system command synchronously with multiple attempts
|
||||
* @deprecated This method blocks the event loop and should be avoided. Use executeWithRetry instead.
|
||||
* WARNING: This method contains a busy wait loop that will block the entire Node.js event loop!
|
||||
*/
|
||||
private executeWithRetrySync(command: string, maxRetries = 3, retryDelayMs = 1000): string {
|
||||
// Log deprecation warning
|
||||
console.warn('[DEPRECATION WARNING] executeWithRetrySync blocks the event loop and should not be used. Consider using the async executeWithRetry method instead.');
|
||||
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
@ -231,10 +238,12 @@ export class NfTablesProxy {
|
||||
|
||||
// Wait before retry, unless it's the last attempt
|
||||
if (i < maxRetries - 1) {
|
||||
// A naive sleep in sync context
|
||||
// CRITICAL: This busy wait loop blocks the entire event loop!
|
||||
// This is a temporary fallback for sync contexts only.
|
||||
// TODO: Remove this method entirely and make all callers async
|
||||
const waitUntil = Date.now() + retryDelayMs;
|
||||
while (Date.now() < waitUntil) {
|
||||
// busy wait - not great, but this is a fallback method
|
||||
// Busy wait - blocks event loop
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -243,6 +252,26 @@ export class NfTablesProxy {
|
||||
throw new NftExecutionError(`Failed after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute nftables commands with a temporary file
|
||||
* This helper handles the common pattern of writing rules to a temp file,
|
||||
* executing nftables with the file, and cleaning up
|
||||
*/
|
||||
private async executeWithTempFile(rulesetContent: string): Promise<void> {
|
||||
await AsyncFileSystem.writeFile(this.tempFilePath, rulesetContent);
|
||||
|
||||
try {
|
||||
await this.executeWithRetry(
|
||||
`${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
|
||||
this.settings.maxRetries,
|
||||
this.settings.retryDelayMs
|
||||
);
|
||||
} finally {
|
||||
// Always clean up the temp file
|
||||
await AsyncFileSystem.remove(this.tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if nftables is available and the required modules are loaded
|
||||
*/
|
||||
@ -545,15 +574,8 @@ export class NfTablesProxy {
|
||||
|
||||
// Only write and apply if we have rules to add
|
||||
if (rulesetContent) {
|
||||
// Write the ruleset to a temporary file
|
||||
fs.writeFileSync(this.tempFilePath, rulesetContent);
|
||||
|
||||
// Apply the ruleset
|
||||
await this.executeWithRetry(
|
||||
`${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
|
||||
this.settings.maxRetries,
|
||||
this.settings.retryDelayMs
|
||||
);
|
||||
// Apply the ruleset using the helper
|
||||
await this.executeWithTempFile(rulesetContent);
|
||||
|
||||
this.log('info', `Added source IP filter rules for ${family}`);
|
||||
|
||||
@ -566,9 +588,6 @@ export class NfTablesProxy {
|
||||
await this.verifyRuleApplication(rule);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the temporary file
|
||||
fs.unlinkSync(this.tempFilePath);
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -663,13 +682,7 @@ export class NfTablesProxy {
|
||||
|
||||
// Apply the rules if we have any
|
||||
if (rulesetContent) {
|
||||
fs.writeFileSync(this.tempFilePath, rulesetContent);
|
||||
|
||||
await this.executeWithRetry(
|
||||
`${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
|
||||
this.settings.maxRetries,
|
||||
this.settings.retryDelayMs
|
||||
);
|
||||
await this.executeWithTempFile(rulesetContent);
|
||||
|
||||
this.log('info', `Added advanced NAT rules for ${family}`);
|
||||
|
||||
@ -682,9 +695,6 @@ export class NfTablesProxy {
|
||||
await this.verifyRuleApplication(rule);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the temporary file
|
||||
fs.unlinkSync(this.tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
@ -816,15 +826,8 @@ export class NfTablesProxy {
|
||||
|
||||
// Apply the ruleset if we have any rules
|
||||
if (rulesetContent) {
|
||||
// Write to temporary file
|
||||
fs.writeFileSync(this.tempFilePath, rulesetContent);
|
||||
|
||||
// Apply the ruleset
|
||||
await this.executeWithRetry(
|
||||
`${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
|
||||
this.settings.maxRetries,
|
||||
this.settings.retryDelayMs
|
||||
);
|
||||
// Apply the ruleset using the helper
|
||||
await this.executeWithTempFile(rulesetContent);
|
||||
|
||||
this.log('info', `Added port forwarding rules for ${family}`);
|
||||
|
||||
@ -837,9 +840,6 @@ export class NfTablesProxy {
|
||||
await this.verifyRuleApplication(rule);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove temporary file
|
||||
fs.unlinkSync(this.tempFilePath);
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -931,15 +931,7 @@ export class NfTablesProxy {
|
||||
|
||||
// Apply the ruleset if we have any rules
|
||||
if (rulesetContent) {
|
||||
// Write to temporary file
|
||||
fs.writeFileSync(this.tempFilePath, rulesetContent);
|
||||
|
||||
// Apply the ruleset
|
||||
await this.executeWithRetry(
|
||||
`${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
|
||||
this.settings.maxRetries,
|
||||
this.settings.retryDelayMs
|
||||
);
|
||||
await this.executeWithTempFile(rulesetContent);
|
||||
|
||||
this.log('info', `Added port forwarding rules for ${family}`);
|
||||
|
||||
@ -952,9 +944,6 @@ export class NfTablesProxy {
|
||||
await this.verifyRuleApplication(rule);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove temporary file
|
||||
fs.unlinkSync(this.tempFilePath);
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -1027,15 +1016,8 @@ export class NfTablesProxy {
|
||||
|
||||
// Apply the ruleset if we have any rules
|
||||
if (rulesetContent) {
|
||||
// Write to temporary file
|
||||
fs.writeFileSync(this.tempFilePath, rulesetContent);
|
||||
|
||||
// Apply the ruleset
|
||||
await this.executeWithRetry(
|
||||
`${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
|
||||
this.settings.maxRetries,
|
||||
this.settings.retryDelayMs
|
||||
);
|
||||
// Apply the ruleset using the helper
|
||||
await this.executeWithTempFile(rulesetContent);
|
||||
|
||||
this.log('info', `Added QoS rules for ${family}`);
|
||||
|
||||
@ -1048,9 +1030,6 @@ export class NfTablesProxy {
|
||||
await this.verifyRuleApplication(rule);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove temporary file
|
||||
fs.unlinkSync(this.tempFilePath);
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -1615,25 +1594,27 @@ export class NfTablesProxy {
|
||||
// Apply the ruleset if we have any rules to delete
|
||||
if (rulesetContent) {
|
||||
// Write to temporary file
|
||||
fs.writeFileSync(this.tempFilePath, rulesetContent);
|
||||
await AsyncFileSystem.writeFile(this.tempFilePath, rulesetContent);
|
||||
|
||||
// Apply the ruleset
|
||||
await this.executeWithRetry(
|
||||
`${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
|
||||
this.settings.maxRetries,
|
||||
this.settings.retryDelayMs
|
||||
);
|
||||
|
||||
this.log('info', 'Removed all added rules');
|
||||
|
||||
// Mark all rules as removed
|
||||
this.rules.forEach(rule => {
|
||||
rule.added = false;
|
||||
rule.verified = false;
|
||||
});
|
||||
|
||||
// Remove temporary file
|
||||
fs.unlinkSync(this.tempFilePath);
|
||||
try {
|
||||
// Apply the ruleset
|
||||
await this.executeWithRetry(
|
||||
`${NfTablesProxy.NFT_CMD} -f ${this.tempFilePath}`,
|
||||
this.settings.maxRetries,
|
||||
this.settings.retryDelayMs
|
||||
);
|
||||
|
||||
this.log('info', 'Removed all added rules');
|
||||
|
||||
// Mark all rules as removed
|
||||
this.rules.forEach(rule => {
|
||||
rule.added = false;
|
||||
rule.verified = false;
|
||||
});
|
||||
} finally {
|
||||
// Remove temporary file
|
||||
await AsyncFileSystem.remove(this.tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up IP sets if we created any
|
||||
@ -1862,8 +1843,12 @@ export class NfTablesProxy {
|
||||
|
||||
/**
|
||||
* Synchronous version of cleanSlate
|
||||
* @deprecated This method blocks the event loop and should be avoided. Use cleanSlate() instead.
|
||||
* WARNING: This method uses execSync which blocks the entire Node.js event loop!
|
||||
*/
|
||||
public static cleanSlateSync(): void {
|
||||
console.warn('[DEPRECATION WARNING] cleanSlateSync blocks the event loop and should not be used. Consider using the async cleanSlate() method instead.');
|
||||
|
||||
try {
|
||||
// Check for rules with our comment pattern
|
||||
const stdout = execSync(`${NfTablesProxy.NFT_CMD} list ruleset`).toString();
|
||||
|
Reference in New Issue
Block a user