From e0456a48243a9e5edd085d8f2dfcd06334de5629 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 18 Dec 2024 19:09:41 +0000 Subject: [PATCH] Improve cache update reliability and monitoring - Add retry mechanism and rate limit handling to GitHub API calls - Create monitoring system for cache health - Add detailed logging and status tracking - Implement safer cache building process - Add status endpoint for monitoring --- api/cron.js | 94 +++++++++++++++++++++++++++++++++------ api/status.js | 21 +++++++++ package.json | 4 +- scripts/monitor-cache.ts | 61 +++++++++++++++++++++++++ scripts/src/github-api.ts | 54 ++++++++++++++++------ 5 files changed, 207 insertions(+), 27 deletions(-) create mode 100644 api/status.js create mode 100644 scripts/monitor-cache.ts diff --git a/api/cron.js b/api/cron.js index 5c7e6a7..129c5a1 100644 --- a/api/cron.js +++ b/api/cron.js @@ -1,23 +1,91 @@ -import { exec } from 'child_process'; -import { promisify } from 'util'; - -const execAsync = promisify(exec); +import { fetchBotActivities } from '../scripts/src/github-api'; export default async function handler(req, res) { + console.log('Cron job started:', new Date().toISOString()); + try { - if (req.method === 'POST') { - const { authorization } = req.headers; + if (req.method !== 'POST') { + console.log('Invalid method:', req.method); + return res.status(405).json({ error: 'Method not allowed' }); + } + + const { authorization } = req.headers; + if (authorization !== `Bearer ${process.env.CRON_SECRET}`) { + console.warn('Unauthorized access attempt'); + return res.status(401).json({ error: 'Unauthorized' }); + } - if (authorization !== `Bearer ${process.env.CRON_SECRET}`) { - return res.status(401).json({ error: 'Unauthorized' }); - } + console.log('Fetching bot activities...'); + const activities = await fetchBotActivities(); + + // Store the cache in public/cache directory + const cacheData = { + activities, + lastUpdated: new Date().toISOString() + }; - await execAsync('npm run build:cache'); - return res.status(200).json({ success: true }); + // Write to a status file to track last successful update + const statusData = { + lastSuccessfulUpdate: new Date().toISOString(), + activitiesCount: activities.length, + status: 'success' + }; + + try { + const { writeFile } = await import('fs/promises'); + const { join } = await import('path'); + + // Ensure directories exist + const { mkdir } = await import('fs/promises'); + await mkdir('public/cache', { recursive: true }); + + // Write cache and status files + await writeFile( + join('public/cache/bot-activities.json'), + JSON.stringify(cacheData, null, 2) + ); + + await writeFile( + join('public/cache/status.json'), + JSON.stringify(statusData, null, 2) + ); + + console.log('Cache files written successfully'); + } catch (fsError) { + console.error('Failed to write cache files:', fsError); + throw new Error('Failed to write cache files: ' + fsError.message); } - return res.status(405).json({ error: 'Method not allowed' }); + console.log('Cron job completed successfully'); + return res.status(200).json({ + success: true, + timestamp: new Date().toISOString(), + activitiesCount: activities.length + }); } catch (error) { - return res.status(500).json({ error: error.message }); + console.error('Cron job failed:', error); + + // Write error status + try { + const { writeFile } = await import('fs/promises'); + const { join } = await import('path'); + + await writeFile( + join('public/cache/status.json'), + JSON.stringify({ + lastAttempt: new Date().toISOString(), + status: 'error', + error: error.message + }, null, 2) + ); + } catch (fsError) { + console.error('Failed to write error status:', fsError); + } + + return res.status(500).json({ + error: error.message, + timestamp: new Date().toISOString(), + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined + }); } } \ No newline at end of file diff --git a/api/status.js b/api/status.js new file mode 100644 index 0000000..4ecb448 --- /dev/null +++ b/api/status.js @@ -0,0 +1,21 @@ +export default async function handler(req, res) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { readFile } = await import('fs/promises'); + const { join } = await import('path'); + + const statusPath = join('public/cache/status.json'); + const status = JSON.parse(await readFile(statusPath, 'utf8')); + + return res.status(200).json(status); + } catch (error) { + console.error('Failed to read status:', error); + return res.status(500).json({ + error: 'Failed to read cache status', + details: error.message + }); + } +} \ No newline at end of file diff --git a/package.json b/package.json index 984a8e3..5e3213d 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "test:coverage": "vitest run --coverage --exclude '**/__integration_tests__/**'", "test:integration": "vitest run '**/__integration_tests__/**'", "typecheck": "tsc --noEmit", - "cache-data": "node scripts/cache-github-data.js" + "cache-data": "node scripts/cache-github-data.js", + "monitor:cache": "cd scripts && npx tsc && node dist/monitor-cache.js", + "build:cache:safe": "npm run build:cache || (echo 'Cache build failed, checking status...' && npm run monitor:cache)" }, "dependencies": { "@nextui-org/react": "^2.4.8", diff --git a/scripts/monitor-cache.ts b/scripts/monitor-cache.ts new file mode 100644 index 0000000..9332fe9 --- /dev/null +++ b/scripts/monitor-cache.ts @@ -0,0 +1,61 @@ +interface CacheStatus { + lastSuccessfulUpdate?: string; + lastAttempt?: string; + status: 'success' | 'error'; + activitiesCount?: number; + error?: string; +} + +async function checkCacheStatus(): Promise { + try { + const fs = await import('fs/promises'); + const path = await import('path'); + + const statusPath = path.join(process.cwd(), 'public/cache/status.json'); + const cacheDataPath = path.join(process.cwd(), 'public/cache/bot-activities.json'); + + // Read status file + const statusData = JSON.parse(await fs.readFile(statusPath, 'utf8')) as CacheStatus; + + // Read cache file + const cacheData = JSON.parse(await fs.readFile(cacheDataPath, 'utf8')); + + // Check cache freshness + const lastUpdate = new Date(statusData.lastSuccessfulUpdate || 0); + const now = new Date(); + const hoursSinceUpdate = (now.getTime() - lastUpdate.getTime()) / (1000 * 60 * 60); + + console.log('Cache Status Report:'); + console.log('-------------------'); + console.log(`Status: ${statusData.status}`); + console.log(`Last Successful Update: ${statusData.lastSuccessfulUpdate || 'Never'}`); + console.log(`Hours Since Last Update: ${hoursSinceUpdate.toFixed(2)}`); + console.log(`Activities in Cache: ${cacheData.activities?.length || 0}`); + + if (statusData.error) { + console.error('Last Error:', statusData.error); + } + + // Alert if cache is stale + if (hoursSinceUpdate > 12) { + throw new Error(`Cache is stale! Last update was ${hoursSinceUpdate.toFixed(2)} hours ago`); + } + + // Alert if no activities + if (!cacheData.activities?.length) { + throw new Error('Cache contains no activities!'); + } + + console.log('\nCache status is healthy ✓'); + + } catch (error) { + console.error('\nCache Health Check Failed!'); + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} + +// Run the check if this file is being run directly +if (require.main === module) { + checkCacheStatus(); +} \ No newline at end of file diff --git a/scripts/src/github-api.ts b/scripts/src/github-api.ts index b025db4..5107ba7 100644 --- a/scripts/src/github-api.ts +++ b/scripts/src/github-api.ts @@ -8,22 +8,50 @@ const REPO_NAME = 'OpenHands'; import fs from 'fs'; -async function fetchWithAuth(url: string): Promise> { +const MAX_RETRIES = 3; +const RETRY_DELAY = 1000; // 1 second +const RATE_LIMIT_DELAY = 60000; // 1 minute + +async function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function fetchWithAuth(url: string, retries = MAX_RETRIES): Promise> { // Log the request fs.appendFileSync('github-api.log', `\n[${new Date().toISOString()}] REQUEST: ${url}\n`); - const response = await fetch(url, { - headers: { - 'Authorization': `Bearer ${GITHUB_TOKEN}`, - 'Accept': 'application/vnd.github.v3+json', - }, - }); - - if (!response.ok) { - const errorBody = await response.text(); - fs.appendFileSync('github-api.log', `[${new Date().toISOString()}] ERROR: ${String(response.status)} ${String(response.statusText)}\n${String(errorBody)}\n`); - throw new Error(`GitHub API error: ${String(response.status)} ${String(response.statusText)}\n${String(errorBody)}`); - } + try { + const response = await fetch(url, { + headers: { + 'Authorization': `Bearer ${GITHUB_TOKEN}`, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'OpenHands-Agent-Monitor' + }, + }); + + // Check for rate limiting + if (response.status === 403 && response.headers.get('x-ratelimit-remaining') === '0') { + const resetTime = parseInt(response.headers.get('x-ratelimit-reset') || '0') * 1000; + const waitTime = Math.max(0, resetTime - Date.now()); + console.log(`Rate limited. Waiting ${waitTime}ms before retrying...`); + fs.appendFileSync('github-api.log', `[${new Date().toISOString()}] RATE LIMIT: Waiting ${waitTime}ms before retry\n`); + await sleep(waitTime); + return fetchWithAuth(url, retries); + } + + if (!response.ok) { + const errorBody = await response.text(); + fs.appendFileSync('github-api.log', `[${new Date().toISOString()}] ERROR: ${String(response.status)} ${String(response.statusText)}\n${String(errorBody)}\n`); + + if (retries > 0) { + console.log(`Request failed. Retrying in ${RETRY_DELAY}ms... (${retries} retries left)`); + fs.appendFileSync('github-api.log', `[${new Date().toISOString()}] RETRY: ${retries} attempts remaining\n`); + await sleep(RETRY_DELAY); + return fetchWithAuth(url, retries - 1); + } + + throw new Error(`GitHub API error: ${String(response.status)} ${String(response.statusText)}\n${String(errorBody)}`); + } // Parse Link header for pagination const linkHeader = response.headers.get('Link') ?? '';