fix: throttle Tiny API usage across backfills
All checks were successful
Build and Deploy / build-and-push (push) Successful in 59s
All checks were successful
Build and Deploy / build-and-push (push) Successful in 59s
This commit is contained in:
127
src/services/tiny-api.service.ts
Normal file
127
src/services/tiny-api.service.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import axios from 'axios';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
const DEFAULT_LOCK_DIR = path.join(os.tmpdir(), 'api-tiny-n8n-rate-limit.lock');
|
||||
const DEFAULT_STATE_FILE = path.join(os.tmpdir(), 'api-tiny-n8n-rate-limit.json');
|
||||
const LOCK_STALE_MS = 30000;
|
||||
const DEFAULT_BLOCK_RETRY_DELAY_MS = 120000;
|
||||
const DEFAULT_BLOCK_MAX_RETRIES = 30;
|
||||
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
const getPositiveNumberEnv = (name: string, fallback: number) => {
|
||||
const configured = Number(process.env[name] || fallback);
|
||||
return Number.isFinite(configured) && configured > 0 ? configured : 0;
|
||||
};
|
||||
|
||||
const getMinIntervalMs = () => getPositiveNumberEnv('TINY_MIN_REQUEST_INTERVAL_MS', 0);
|
||||
const getBlockRetryDelayMs = () => getPositiveNumberEnv('TINY_BLOCK_RETRY_DELAY_MS', DEFAULT_BLOCK_RETRY_DELAY_MS);
|
||||
const getBlockMaxRetries = () => getPositiveNumberEnv('TINY_BLOCK_MAX_RETRIES', DEFAULT_BLOCK_MAX_RETRIES);
|
||||
const getLockDir = () => process.env.TINY_RATE_LIMIT_LOCK_DIR || DEFAULT_LOCK_DIR;
|
||||
const getStateFile = () => process.env.TINY_RATE_LIMIT_STATE_FILE || DEFAULT_STATE_FILE;
|
||||
|
||||
const readLastRequestAt = () => {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(getStateFile(), 'utf-8'));
|
||||
return Number(parsed.lastRequestAt || 0);
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const writeLastRequestAt = (timestamp: number) => {
|
||||
fs.writeFileSync(getStateFile(), JSON.stringify({ lastRequestAt: timestamp }, null, 2));
|
||||
};
|
||||
|
||||
const removeStaleLock = (lockDir: string) => {
|
||||
try {
|
||||
const stats = fs.statSync(lockDir);
|
||||
if (Date.now() - stats.mtimeMs > LOCK_STALE_MS) {
|
||||
fs.rmSync(lockDir, { recursive: true, force: true });
|
||||
}
|
||||
} catch {
|
||||
// Lock does not exist or cannot be inspected. The acquire loop will retry.
|
||||
}
|
||||
};
|
||||
|
||||
const acquireLock = async () => {
|
||||
const lockDir = getLockDir();
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
fs.mkdirSync(lockDir);
|
||||
return lockDir;
|
||||
} catch (error: any) {
|
||||
if (error?.code !== 'EEXIST') throw error;
|
||||
removeStaleLock(lockDir);
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const releaseLock = (lockDir: string) => {
|
||||
fs.rmSync(lockDir, { recursive: true, force: true });
|
||||
};
|
||||
|
||||
const waitForTinySlot = async () => {
|
||||
const minIntervalMs = getMinIntervalMs();
|
||||
if (!minIntervalMs) return;
|
||||
|
||||
const lockDir = await acquireLock();
|
||||
|
||||
try {
|
||||
const lastRequestAt = readLastRequestAt();
|
||||
const elapsed = Date.now() - lastRequestAt;
|
||||
const waitMs = Math.max(0, minIntervalMs - elapsed);
|
||||
|
||||
if (waitMs > 0) {
|
||||
console.log(`[Tiny Rate Limit] Waiting ${waitMs}ms before next Tiny API request.`);
|
||||
await sleep(waitMs);
|
||||
}
|
||||
|
||||
writeLastRequestAt(Date.now());
|
||||
} finally {
|
||||
releaseLock(lockDir);
|
||||
}
|
||||
};
|
||||
|
||||
const getTinyErrors = (data: any) => {
|
||||
const erros = data?.retorno?.erros;
|
||||
if (!erros) return [];
|
||||
return Array.isArray(erros) ? erros : [erros];
|
||||
};
|
||||
|
||||
const isTinyApiBlocked = (data: any) => {
|
||||
return getTinyErrors(data).some((entry: any) => {
|
||||
const message = String(entry?.erro || entry || '').toLowerCase();
|
||||
return message.includes('api bloqueada') || message.includes('excedido o número de acessos');
|
||||
});
|
||||
};
|
||||
|
||||
export const tinyPost = async (servicePhp: string, params: URLSearchParams) => {
|
||||
let attempt = 0;
|
||||
|
||||
while (true) {
|
||||
await waitForTinySlot();
|
||||
|
||||
const response = await axios.post(`https://api.tiny.com.br/api2/${servicePhp}`, params, {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
});
|
||||
|
||||
if (!isTinyApiBlocked(response.data)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
attempt += 1;
|
||||
const maxRetries = getBlockMaxRetries();
|
||||
if (attempt > maxRetries) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const delayMs = getBlockRetryDelayMs();
|
||||
console.warn(`[Tiny Rate Limit] Tiny blocked ${servicePhp}. Waiting ${delayMs}ms before retry ${attempt}/${maxRetries}.`);
|
||||
await sleep(delayMs);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user