feat(tasks): Add unified task-based worker architecture
Replace fragmented job systems (job_schedules, dispensary_crawl_jobs, SyncOrchestrator) with a single unified task queue: - Add worker_tasks table with atomic task claiming via SELECT FOR UPDATE SKIP LOCKED - Add TaskService for CRUD, claiming, and capacity metrics - Add TaskWorker with role-based handlers (resync, discovery, analytics) - Add /api/tasks endpoints for management and migration from legacy systems - Add TasksDashboard UI and integrate task counts into main dashboard - Add comprehensive documentation Task roles: store_discovery, entry_point_discovery, product_discovery, product_resync, analytics_refresh Run workers with: WORKER_ROLE=product_resync npx tsx src/tasks/task-worker.ts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
92
backend/src/tasks/handlers/analytics-refresh.ts
Normal file
92
backend/src/tasks/handlers/analytics-refresh.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Analytics Refresh Handler
|
||||
*
|
||||
* Refreshes materialized views and pre-computed analytics tables.
|
||||
* Should run daily or on-demand after major data changes.
|
||||
*/
|
||||
|
||||
import { TaskContext, TaskResult } from '../task-worker';
|
||||
|
||||
export async function handleAnalyticsRefresh(ctx: TaskContext): Promise<TaskResult> {
|
||||
const { pool } = ctx;
|
||||
|
||||
console.log(`[AnalyticsRefresh] Starting analytics refresh...`);
|
||||
|
||||
const refreshed: string[] = [];
|
||||
const failed: string[] = [];
|
||||
|
||||
// List of materialized views to refresh
|
||||
const materializedViews = [
|
||||
'mv_state_metrics',
|
||||
'mv_brand_metrics',
|
||||
'mv_category_metrics',
|
||||
'v_brand_summary',
|
||||
'v_dashboard_stats',
|
||||
];
|
||||
|
||||
for (const viewName of materializedViews) {
|
||||
try {
|
||||
// Heartbeat before each refresh
|
||||
await ctx.heartbeat();
|
||||
|
||||
// Check if view exists
|
||||
const existsResult = await pool.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM pg_matviews WHERE matviewname = $1
|
||||
UNION
|
||||
SELECT 1 FROM pg_views WHERE viewname = $1
|
||||
) as exists
|
||||
`, [viewName]);
|
||||
|
||||
if (!existsResult.rows[0].exists) {
|
||||
console.log(`[AnalyticsRefresh] View ${viewName} does not exist, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to refresh (only works for materialized views)
|
||||
try {
|
||||
await pool.query(`REFRESH MATERIALIZED VIEW CONCURRENTLY ${viewName}`);
|
||||
refreshed.push(viewName);
|
||||
console.log(`[AnalyticsRefresh] Refreshed ${viewName}`);
|
||||
} catch (refreshError: any) {
|
||||
// Try non-concurrent refresh
|
||||
try {
|
||||
await pool.query(`REFRESH MATERIALIZED VIEW ${viewName}`);
|
||||
refreshed.push(viewName);
|
||||
console.log(`[AnalyticsRefresh] Refreshed ${viewName} (non-concurrent)`);
|
||||
} catch (nonConcurrentError: any) {
|
||||
// Not a materialized view or other error
|
||||
console.log(`[AnalyticsRefresh] ${viewName} is not a materialized view or refresh failed`);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`[AnalyticsRefresh] Error refreshing ${viewName}:`, error.message);
|
||||
failed.push(viewName);
|
||||
}
|
||||
}
|
||||
|
||||
// Run analytics capture functions if they exist
|
||||
const captureFunctions = [
|
||||
'capture_brand_snapshots',
|
||||
'capture_category_snapshots',
|
||||
];
|
||||
|
||||
for (const funcName of captureFunctions) {
|
||||
try {
|
||||
await pool.query(`SELECT ${funcName}()`);
|
||||
console.log(`[AnalyticsRefresh] Executed ${funcName}()`);
|
||||
} catch (error: any) {
|
||||
// Function might not exist
|
||||
console.log(`[AnalyticsRefresh] ${funcName}() not available`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[AnalyticsRefresh] Complete: ${refreshed.length} refreshed, ${failed.length} failed`);
|
||||
|
||||
return {
|
||||
success: failed.length === 0,
|
||||
refreshed,
|
||||
failed,
|
||||
error: failed.length > 0 ? `Failed to refresh: ${failed.join(', ')}` : undefined,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user