Skip to content

Commit c41b48a

Browse files
committed
refactor(workflow): enhance daily balance snapshot orchestration
- Updated endpoint to use parameters instead of body - Introduced a step to install script dependencies before running the snapshot orchestration. - Updated the orchestration command to use `npm start` instead of directly executing the script. - Improved failure notification logic to handle cases where the Discord webhook URL is not configured.
1 parent 9e06790 commit c41b48a

File tree

4 files changed

+120
-42
lines changed

4 files changed

+120
-42
lines changed

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ on:
1313
jobs:
1414
snapshot-balances:
1515
runs-on: ubuntu-latest
16+
timeout-minutes: 300 # 5 hours timeout (GitHub Actions max is 6 hours)
1617

1718
steps:
1819
- name: Checkout repository
@@ -27,8 +28,15 @@ jobs:
2728
- name: Install dependencies
2829
run: npm ci
2930

31+
- name: Install script dependencies
32+
run: |
33+
cd scripts
34+
npm ci
35+
3036
- name: Run batch snapshot orchestration
31-
run: node scripts/batch-snapshot-orchestrator.js
37+
run: |
38+
cd scripts
39+
npm start
3240
env:
3341
API_BASE_URL: "https://multisig.meshjs.dev"
3442
SNAPSHOT_AUTH_TOKEN: ${{ secrets.SNAPSHOT_AUTH_TOKEN }}
@@ -41,6 +49,10 @@ jobs:
4149
# Send failure notification
4250
run: |
4351
echo "❌ Daily balance snapshot job failed"
44-
curl -X POST -H 'Content-type: application/json' \
45-
--data "{\"text\":\"❌ Daily balance snapshots failed. Check the GitHub Actions logs.\"}" \
46-
${{ secrets.SNAPSHOT_ERROR_DISCORD_WEBHOOK_URL }}
52+
if [ -n "${{ secrets.SNAPSHOT_ERROR_DISCORD_WEBHOOK_URL }}" ]; then
53+
curl -X POST -H 'Content-type: application/json' \
54+
--data "{\"text\":\"❌ Daily balance snapshots failed. Check the GitHub Actions logs.\"}" \
55+
${{ secrets.SNAPSHOT_ERROR_DISCORD_WEBHOOK_URL }} || echo "Failed to send Discord notification"
56+
else
57+
echo "SNAPSHOT_ERROR_DISCORD_WEBHOOK_URL not configured, skipping notification"
58+
fi
Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
* It handles timeout issues by processing wallets in small batches.
99
*
1010
* Usage:
11-
* node scripts/batch-snapshot-orchestrator.js
12-
* SNAPSHOT_AUTH_TOKEN=your_token node scripts/batch-snapshot-orchestrator.js
11+
* npx tsx scripts/batch-snapshot-orchestrator.ts
12+
* SNAPSHOT_AUTH_TOKEN=your_token npx tsx scripts/batch-snapshot-orchestrator.ts
1313
*
1414
* Environment Variables:
1515
* - API_BASE_URL: Base URL for the API (default: http://localhost:3000)
@@ -19,7 +19,49 @@
1919
* - MAX_RETRIES: Maximum retries for failed batches (default: 3)
2020
*/
2121

22+
interface BatchProgress {
23+
processedInBatch: number;
24+
walletsInBatch: number;
25+
failedInBatch: number;
26+
snapshotsStored: number;
27+
totalAdaBalance: number;
28+
totalBatches: number;
29+
}
30+
31+
interface BatchResponse {
32+
success: boolean;
33+
message?: string;
34+
progress: BatchProgress;
35+
}
36+
37+
interface BatchResults {
38+
totalBatches: number;
39+
completedBatches: number;
40+
failedBatches: number;
41+
totalWalletsProcessed: number;
42+
totalWalletsFailed: number;
43+
totalAdaBalance: number;
44+
totalSnapshotsStored: number;
45+
executionTime: number;
46+
}
47+
48+
interface BatchConfig {
49+
apiBaseUrl: string;
50+
authToken: string;
51+
batchSize: number;
52+
delayBetweenBatches: number;
53+
maxRetries: number;
54+
}
55+
56+
interface ApiResponse<T> {
57+
data: T;
58+
status: number;
59+
}
60+
2261
class BatchSnapshotOrchestrator {
62+
private config: BatchConfig;
63+
private results: BatchResults;
64+
2365
constructor() {
2466
this.config = this.loadConfig();
2567
this.results = {
@@ -34,7 +76,7 @@ class BatchSnapshotOrchestrator {
3476
};
3577
}
3678

37-
loadConfig() {
79+
private loadConfig(): BatchConfig {
3880
const apiBaseUrl = process.env.API_BASE_URL || 'http://localhost:3000';
3981
const authToken = process.env.SNAPSHOT_AUTH_TOKEN;
4082

@@ -51,48 +93,55 @@ class BatchSnapshotOrchestrator {
5193
};
5294
}
5395

54-
async makeRequest(/** @type {string} */ url, /** @type {RequestInit} */ options = {}) {
96+
private async makeRequest<T>(url: string, options: RequestInit = {}): Promise<ApiResponse<T>> {
5597
try {
98+
// Add timeout to prevent hanging requests
99+
const controller = new AbortController();
100+
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
101+
56102
const response = await fetch(url, {
57103
...options,
104+
signal: controller.signal,
58105
headers: {
59106
'Authorization': `Bearer ${this.config.authToken}`,
60107
'Content-Type': 'application/json',
61108
...(options.headers || {}),
62109
},
63110
});
111+
112+
clearTimeout(timeoutId);
64113

65114
if (!response.ok) {
66115
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
67116
}
68117

69-
const data = await response.json();
118+
const data = await response.json() as T;
70119
return { data, status: response.status };
71120
} catch (error) {
121+
if (error instanceof Error && error.name === 'AbortError') {
122+
throw new Error('Request timeout after 30 seconds');
123+
}
72124
throw error;
73125
}
74126
}
75127

76-
async delay(/** @type {number} */ seconds) {
128+
private async delay(seconds: number): Promise<void> {
77129
return new Promise(resolve => setTimeout(resolve, seconds * 1000));
78130
}
79131

80-
async processBatch(/** @type {number} */ batchNumber, /** @type {string} */ batchId) {
132+
private async processBatch(batchNumber: number, batchId: string): Promise<BatchProgress | null> {
81133
console.log(`📦 Processing batch ${batchNumber}...`);
82134

83135
for (let attempt = 1; attempt <= this.config.maxRetries; attempt++) {
84136
try {
85-
const { data } = await this.makeRequest(
86-
`${this.config.apiBaseUrl}/api/v1/stats/run-snapshots-batch`,
87-
{
88-
method: 'POST',
89-
body: JSON.stringify({
90-
batchId,
91-
batchNumber,
92-
batchSize: this.config.batchSize,
93-
}),
94-
}
95-
);
137+
const url = new URL(`${this.config.apiBaseUrl}/api/v1/stats/run-snapshots-batch`);
138+
url.searchParams.set('batchId', batchId);
139+
url.searchParams.set('batchNumber', batchNumber.toString());
140+
url.searchParams.set('batchSize', this.config.batchSize.toString());
141+
142+
const { data } = await this.makeRequest<BatchResponse>(url.toString(), {
143+
method: 'POST',
144+
});
96145

97146
if (data.success) {
98147
console.log(`✅ Batch ${batchNumber} completed successfully`);
@@ -124,7 +173,7 @@ class BatchSnapshotOrchestrator {
124173
return null;
125174
}
126175

127-
async run() {
176+
public async run(): Promise<BatchResults> {
128177
const startTime = Date.now();
129178
const batchId = `snapshot-${Date.now()}`;
130179

@@ -152,10 +201,8 @@ class BatchSnapshotOrchestrator {
152201
// Process remaining batches
153202
for (let batchNumber = 2; batchNumber <= this.results.totalBatches; batchNumber++) {
154203
// Delay between batches to prevent overwhelming the server
155-
if (batchNumber > 2) {
156-
console.log(`⏳ Waiting ${this.config.delayBetweenBatches}s before next batch...`);
157-
await this.delay(this.config.delayBetweenBatches);
158-
}
204+
console.log(`⏳ Waiting ${this.config.delayBetweenBatches}s before next batch...`);
205+
await this.delay(this.config.delayBetweenBatches);
159206

160207
const batchProgress = await this.processBatch(batchNumber, batchId);
161208

@@ -204,7 +251,7 @@ class BatchSnapshotOrchestrator {
204251
}
205252

206253
// Main execution
207-
async function main() {
254+
async function main(): Promise<void> {
208255
try {
209256
const orchestrator = new BatchSnapshotOrchestrator();
210257
await orchestrator.run();
@@ -217,7 +264,7 @@ async function main() {
217264
}
218265

219266
// Export for use in other modules
220-
export { BatchSnapshotOrchestrator };
267+
export { BatchSnapshotOrchestrator, type BatchResults, type BatchProgress, type BatchConfig };
221268

222269
// Run if this file is executed directly
223270
if (import.meta.url === `file://${process.argv[1]}`) {

scripts/package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "batch-snapshot-orchestrator",
3+
"version": "1.0.0",
4+
"description": "Batch snapshot orchestrator for wallet balance snapshots",
5+
"type": "module",
6+
"scripts": {
7+
"start": "tsx batch-snapshot-orchestrator.ts"
8+
},
9+
"dependencies": {
10+
"tsx": "^4.7.0"
11+
},
12+
"engines": {
13+
"node": ">=18.0.0"
14+
}
15+
}

src/pages/api/v1/stats/run-snapshots-batch.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,24 +75,28 @@ export default async function handler(
7575
return res.status(401).json({ error: "Unauthorized" });
7676
}
7777

78-
const { batchId, batchNumber, batchSize = 10 } = req.body;
78+
const { batchId, batchNumber, batchSize } = req.query;
7979
const startTime = new Date().toISOString();
8080

81+
// Convert string parameters to numbers
82+
const parsedBatchNumber = batchNumber ? parseInt(batchNumber as string, 10) : 1;
83+
const parsedBatchSize = batchSize ? parseInt(batchSize as string, 10) : 10;
84+
8185
try {
82-
console.log(`🔄 Starting batch ${batchNumber || 1} of balance snapshots...`);
86+
console.log(`🔄 Starting batch ${parsedBatchNumber} of balance snapshots...`);
8387

8488
// Step 1: Get total wallet count and calculate batches
8589
const totalWallets = await db.wallet.count();
86-
const totalBatches = Math.ceil(totalWallets / batchSize);
87-
const currentBatch = batchNumber || 1;
88-
const offset = (currentBatch - 1) * batchSize;
90+
const totalBatches = Math.ceil(totalWallets / parsedBatchSize);
91+
const currentBatch = parsedBatchNumber;
92+
const offset = (currentBatch - 1) * parsedBatchSize;
8993

90-
console.log(`📊 Processing batch ${currentBatch}/${totalBatches} (${batchSize} wallets per batch)`);
94+
console.log(`📊 Processing batch ${currentBatch}/${totalBatches} (${parsedBatchSize} wallets per batch)`);
9195

9296
// Step 2: Fetch wallets for this batch
9397
const wallets: DbWallet[] = await db.wallet.findMany({
9498
skip: offset,
95-
take: batchSize,
99+
take: parsedBatchSize,
96100
orderBy: { id: 'asc' }, // Consistent ordering
97101
});
98102

@@ -101,7 +105,7 @@ export default async function handler(
101105
success: true,
102106
message: "No wallets found in this batch",
103107
progress: {
104-
batchId: batchId || `batch-${currentBatch}`,
108+
batchId: (batchId as string) || `batch-${currentBatch}`,
105109
totalBatches,
106110
currentBatch,
107111
walletsInBatch: 0,
@@ -281,8 +285,8 @@ export default async function handler(
281285

282286
// Step 5: Calculate progress
283287
const isComplete = currentBatch >= totalBatches;
284-
const totalProcessed = (currentBatch - 1) * batchSize + processedInBatch;
285-
const totalFailed = (currentBatch - 1) * batchSize + failedInBatch;
288+
const totalProcessed = (currentBatch - 1) * parsedBatchSize + processedInBatch;
289+
const totalFailed = (currentBatch - 1) * parsedBatchSize + failedInBatch;
286290

287291
console.log(`📊 Batch ${currentBatch}/${totalBatches} completed:`);
288292
console.log(` • Processed: ${processedInBatch}/${wallets.length}`);
@@ -292,7 +296,7 @@ export default async function handler(
292296
console.log(` • Overall progress: ${totalProcessed}/${totalWallets} wallets`);
293297

294298
const progress: BatchProgress = {
295-
batchId: batchId || `batch-${currentBatch}`,
299+
batchId: (batchId as string) || `batch-${currentBatch}`,
296300
totalBatches,
297301
currentBatch,
298302
walletsInBatch: wallets.length,
@@ -326,9 +330,9 @@ export default async function handler(
326330
success: false,
327331
message: `Batch snapshot process failed: ${errorMessage}`,
328332
progress: {
329-
batchId: batchId || `batch-${batchNumber || 1}`,
333+
batchId: (batchId as string) || `batch-${parsedBatchNumber}`,
330334
totalBatches: 0,
331-
currentBatch: batchNumber || 1,
335+
currentBatch: parsedBatchNumber,
332336
walletsInBatch: 0,
333337
processedInBatch: 0,
334338
failedInBatch: 0,

0 commit comments

Comments
 (0)