Skip to content

Commit

Permalink
Improve cache update reliability and monitoring
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
openhands-agent committed Dec 18, 2024
1 parent 65ee8f2 commit e0456a4
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 27 deletions.
94 changes: 81 additions & 13 deletions api/cron.js
Original file line number Diff line number Diff line change
@@ -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
});
}
}
21 changes: 21 additions & 0 deletions api/status.js
Original file line number Diff line number Diff line change
@@ -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
});
}
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
61 changes: 61 additions & 0 deletions scripts/monitor-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
interface CacheStatus {
lastSuccessfulUpdate?: string;
lastAttempt?: string;
status: 'success' | 'error';
activitiesCount?: number;
error?: string;
}

async function checkCacheStatus(): Promise<void> {
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();
}
54 changes: 41 additions & 13 deletions scripts/src/github-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,50 @@ const REPO_NAME = 'OpenHands';

import fs from 'fs';

async function fetchWithAuth<T = unknown>(url: string): Promise<ApiResponse<T>> {
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<T = unknown>(url: string, retries = MAX_RETRIES): Promise<ApiResponse<T>> {
// 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') ?? '';
Expand Down

0 comments on commit e0456a4

Please sign in to comment.