Skip to content

Commit 186dd3e

Browse files
committed
refactor(batch-snapshot): optimize batch processing and enhance error handling
- Reduced batch size from 10 to 5 for improved performance and reliability. - Introduced a new configurable request timeout to prevent hanging requests. - Enhanced input validation for batch parameters to ensure correct usage. - Updated error handling to include detailed wallet structure information in failure reports. - Improved documentation to reflect changes in batch processing and configuration options.
1 parent 03fac37 commit 186dd3e

File tree

4 files changed

+343
-51
lines changed

4 files changed

+343
-51
lines changed

.github/workflows/daily-balance-snapshots.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ jobs:
4040
env:
4141
API_BASE_URL: "https://multisig.meshjs.dev"
4242
SNAPSHOT_AUTH_TOKEN: ${{ secrets.SNAPSHOT_AUTH_TOKEN }}
43-
BATCH_SIZE: 10
43+
BATCH_SIZE: 5
4444
DELAY_BETWEEN_BATCHES: 10
4545
MAX_RETRIES: 3
46+
REQUEST_TIMEOUT: 45
4647

4748
- name: Notify on failure
4849
if: failure()

scripts/batch-snapshot-orchestrator.ts

Lines changed: 120 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
* Environment Variables:
1515
* - API_BASE_URL: Base URL for the API (default: http://localhost:3000)
1616
* - SNAPSHOT_AUTH_TOKEN: Authentication token for API requests
17-
* - BATCH_SIZE: Number of wallets per batch (default: 10)
17+
* - BATCH_SIZE: Number of wallets per batch (default: 5)
1818
* - DELAY_BETWEEN_BATCHES: Delay between batches in seconds (default: 10)
1919
* - MAX_RETRIES: Maximum retries for failed batches (default: 3)
20+
* - REQUEST_TIMEOUT: Request timeout in seconds (default: 45)
2021
*/
2122

2223
interface BatchProgress {
@@ -35,6 +36,26 @@ interface BatchProgress {
3536
walletId: string;
3637
errorType: string;
3738
errorMessage: string;
39+
walletStructure?: {
40+
name: string;
41+
type: string;
42+
numRequiredSigners: number;
43+
signersCount: number;
44+
hasStakeCredential: boolean;
45+
hasScriptCbor: boolean;
46+
isArchived: boolean;
47+
verified: number;
48+
hasDRepKeys: boolean;
49+
hasClarityApiKey: boolean;
50+
// Character counts for key fields
51+
scriptCborLength: number;
52+
stakeCredentialLength: number;
53+
signersAddressesLength: number;
54+
signersStakeKeysLength: number;
55+
signersDRepKeysLength: number;
56+
signersDescriptionsLength: number;
57+
clarityApiKeyLength: number;
58+
};
3859
}>;
3960
}
4061

@@ -63,6 +84,26 @@ interface BatchResults {
6384
errorType: string;
6485
errorMessage: string;
6586
batchNumber: number;
87+
walletStructure?: {
88+
name: string;
89+
type: string;
90+
numRequiredSigners: number;
91+
signersCount: number;
92+
hasStakeCredential: boolean;
93+
hasScriptCbor: boolean;
94+
isArchived: boolean;
95+
verified: number;
96+
hasDRepKeys: boolean;
97+
hasClarityApiKey: boolean;
98+
// Character counts for key fields
99+
scriptCborLength: number;
100+
stakeCredentialLength: number;
101+
signersAddressesLength: number;
102+
signersStakeKeysLength: number;
103+
signersDRepKeysLength: number;
104+
signersDescriptionsLength: number;
105+
clarityApiKeyLength: number;
106+
};
66107
}>;
67108
failureSummary: Record<string, number>;
68109
}
@@ -73,6 +114,7 @@ interface BatchConfig {
73114
batchSize: number;
74115
delayBetweenBatches: number;
75116
maxRetries: number;
117+
requestTimeout: number; // in seconds
76118
}
77119

78120
interface ApiResponse<T> {
@@ -113,20 +155,52 @@ class BatchSnapshotOrchestrator {
113155
throw new Error('SNAPSHOT_AUTH_TOKEN environment variable is required');
114156
}
115157

158+
if (authToken.trim().length === 0) {
159+
throw new Error('SNAPSHOT_AUTH_TOKEN environment variable cannot be empty');
160+
}
161+
162+
// Validate API base URL format
163+
try {
164+
new URL(apiBaseUrl);
165+
} catch (error) {
166+
throw new Error(`Invalid API_BASE_URL format: ${apiBaseUrl}`);
167+
}
168+
169+
// Parse and validate numeric environment variables
170+
const batchSize = this.parseAndValidateNumber(process.env.BATCH_SIZE || '5', 'BATCH_SIZE', 1, 10);
171+
const delayBetweenBatches = this.parseAndValidateNumber(process.env.DELAY_BETWEEN_BATCHES || '10', 'DELAY_BETWEEN_BATCHES', 1, 300);
172+
const maxRetries = this.parseAndValidateNumber(process.env.MAX_RETRIES || '3', 'MAX_RETRIES', 1, 10);
173+
const requestTimeout = this.parseAndValidateNumber(process.env.REQUEST_TIMEOUT || '45', 'REQUEST_TIMEOUT', 10, 300);
174+
116175
return {
117176
apiBaseUrl,
118177
authToken,
119-
batchSize: parseInt(process.env.BATCH_SIZE || '10'),
120-
delayBetweenBatches: parseInt(process.env.DELAY_BETWEEN_BATCHES || '10'),
121-
maxRetries: parseInt(process.env.MAX_RETRIES || '3'),
178+
batchSize,
179+
delayBetweenBatches,
180+
maxRetries,
181+
requestTimeout,
122182
};
123183
}
124184

185+
private parseAndValidateNumber(value: string, name: string, min: number, max: number): number {
186+
const parsed = parseInt(value, 10);
187+
188+
if (isNaN(parsed)) {
189+
throw new Error(`${name} must be a valid integer, got: ${value}`);
190+
}
191+
192+
if (parsed < min || parsed > max) {
193+
throw new Error(`${name} must be between ${min} and ${max}, got: ${parsed}`);
194+
}
195+
196+
return parsed;
197+
}
198+
125199
private async makeRequest<T>(url: string, options: RequestInit = {}): Promise<ApiResponse<T>> {
126200
try {
127-
// Add timeout to prevent hanging requests
201+
// Add configurable timeout to prevent hanging requests
128202
const controller = new AbortController();
129-
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
203+
const timeoutId = setTimeout(() => controller.abort(), this.config.requestTimeout * 1000);
130204

131205
const response = await fetch(url, {
132206
...options,
@@ -148,7 +222,7 @@ class BatchSnapshotOrchestrator {
148222
return { data, status: response.status };
149223
} catch (error) {
150224
if (error instanceof Error && error.name === 'AbortError') {
151-
throw new Error('Request timeout after 30 seconds');
225+
throw new Error(`Request timeout after ${this.config.requestTimeout} seconds`);
152226
}
153227
throw error;
154228
}
@@ -172,6 +246,15 @@ class BatchSnapshotOrchestrator {
172246
private async processBatch(batchNumber: number, batchId: string): Promise<BatchProgress | null> {
173247
console.log(`📦 Processing batch ${batchNumber}...`);
174248

249+
// Validate inputs
250+
if (!Number.isInteger(batchNumber) || batchNumber < 1) {
251+
throw new Error(`Invalid batchNumber: ${batchNumber}. Must be a positive integer.`);
252+
}
253+
254+
if (!batchId || typeof batchId !== 'string' || batchId.trim().length === 0) {
255+
throw new Error(`Invalid batchId: ${batchId}. Must be a non-empty string.`);
256+
}
257+
175258
for (let attempt = 1; attempt <= this.config.maxRetries; attempt++) {
176259
try {
177260
const url = new URL(`${this.config.apiBaseUrl}/api/v1/stats/run-snapshots-batch`);
@@ -196,6 +279,22 @@ class BatchSnapshotOrchestrator {
196279
console.log(` ❌ Failures in this batch:`);
197280
data.progress.failures.forEach((failure, index) => {
198281
console.log(` ${index + 1}. ${failure.walletId}... - ${failure.errorMessage}`);
282+
if (failure.walletStructure) {
283+
const structure = failure.walletStructure;
284+
console.log(` 📋 Wallet Structure:`);
285+
console.log(` • Name: ${structure.name} (${structure.name.length} chars)`);
286+
console.log(` • Type: ${structure.type} (${structure.type.length} chars)`);
287+
console.log(` • Required Signers: ${structure.numRequiredSigners}/${structure.signersCount}`);
288+
console.log(` • Has Stake Credential: ${structure.hasStakeCredential} (${structure.stakeCredentialLength} chars)`);
289+
console.log(` • Has Script CBOR: ${structure.hasScriptCbor} (${structure.scriptCborLength} chars)`);
290+
console.log(` • Is Archived: ${structure.isArchived}`);
291+
console.log(` • Verified Count: ${structure.verified}`);
292+
console.log(` • Has DRep Keys: ${structure.hasDRepKeys} (${structure.signersDRepKeysLength} items)`);
293+
console.log(` • Has Clarity API Key: ${structure.hasClarityApiKey} (${structure.clarityApiKeyLength} chars)`);
294+
console.log(` • Signers Addresses: ${structure.signersAddressesLength} items`);
295+
console.log(` • Signers Stake Keys: ${structure.signersStakeKeysLength} items`);
296+
console.log(` • Signers Descriptions: ${structure.signersDescriptionsLength} items`);
297+
}
199298
});
200299
}
201300

@@ -214,8 +313,10 @@ class BatchSnapshotOrchestrator {
214313
return null;
215314
}
216315

217-
// Wait before retry
218-
await this.delay(this.config.delayBetweenBatches);
316+
// For 405 errors (Method Not Allowed), wait longer as it might be a server-side issue
317+
const waitTime = errorMessage.includes('405') ? this.config.delayBetweenBatches * 2 : this.config.delayBetweenBatches;
318+
console.log(` ⏳ Waiting ${waitTime}s before retry...`);
319+
await this.delay(waitTime);
219320
}
220321
}
221322

@@ -253,8 +354,11 @@ class BatchSnapshotOrchestrator {
253354
// Accumulate failures
254355
firstBatch.failures.forEach(failure => {
255356
this.results.allFailures.push({
256-
...failure,
257-
batchNumber: 1
357+
walletId: failure.walletId,
358+
errorType: failure.errorType,
359+
errorMessage: failure.errorMessage,
360+
batchNumber: 1,
361+
walletStructure: failure.walletStructure
258362
});
259363
this.results.failureSummary[failure.errorType] = (this.results.failureSummary[failure.errorType] || 0) + 1;
260364
});
@@ -284,8 +388,11 @@ class BatchSnapshotOrchestrator {
284388
// Accumulate failures
285389
batchProgress.failures.forEach(failure => {
286390
this.results.allFailures.push({
287-
...failure,
288-
batchNumber
391+
walletId: failure.walletId,
392+
errorType: failure.errorType,
393+
errorMessage: failure.errorMessage,
394+
batchNumber,
395+
walletStructure: failure.walletStructure
289396
});
290397
this.results.failureSummary[failure.errorType] = (this.results.failureSummary[failure.errorType] || 0) + 1;
291398
});

0 commit comments

Comments
 (0)