removed singletop pattern from queue manager

This commit is contained in:
Boki 2025-06-22 19:16:25 -04:00
parent eeb5d1aca2
commit db3aa9c330
12 changed files with 504 additions and 380 deletions

View file

@ -3,12 +3,16 @@
*/ */
import { OperationContext } from '@stock-bot/di'; import { OperationContext } from '@stock-bot/di';
import type { ProxyInfo } from '@stock-bot/proxy'; import type { ProxyInfo } from '@stock-bot/proxy';
import { QueueManager } from '@stock-bot/queue'; import type { IServiceContainer } from '@stock-bot/handlers';
export async function queueProxyFetch(): Promise<string> { export async function queueProxyFetch(container: IServiceContainer): Promise<string> {
const ctx = OperationContext.create('proxy', 'queue-fetch'); const ctx = OperationContext.create('proxy', 'queue-fetch');
const queueManager = QueueManager.getInstance(); const queueManager = container.queue;
if (!queueManager) {
throw new Error('Queue manager not available');
}
const queue = queueManager.getQueue('proxy'); const queue = queueManager.getQueue('proxy');
const job = await queue.add('proxy-fetch', { const job = await queue.add('proxy-fetch', {
handler: 'proxy', handler: 'proxy',
@ -22,10 +26,14 @@ export async function queueProxyFetch(): Promise<string> {
return jobId; return jobId;
} }
export async function queueProxyCheck(proxies: ProxyInfo[]): Promise<string> { export async function queueProxyCheck(proxies: ProxyInfo[], container: IServiceContainer): Promise<string> {
const ctx = OperationContext.create('proxy', 'queue-check'); const ctx = OperationContext.create('proxy', 'queue-check');
const queueManager = QueueManager.getInstance(); const queueManager = container.queue;
if (!queueManager) {
throw new Error('Queue manager not available');
}
const queue = queueManager.getQueue('proxy'); const queue = queueManager.getQueue('proxy');
const job = await queue.add('proxy-check', { const job = await queue.add('proxy-check', {
handler: 'proxy', handler: 'proxy',

View file

@ -35,6 +35,10 @@ export function initializeProxyProvider(_container: ServiceContainer) {
return { processed: 0, successful: 0 }; return { processed: 0, successful: 0 };
} }
// Get QueueManager instance - we have to use getInstance for now until handlers get container access
const { QueueManager } = await import('@stock-bot/queue');
const queueManager = QueueManager.getInstance();
// Batch process the proxies through check-proxy operation // Batch process the proxies through check-proxy operation
const batchResult = await processItems(proxies, 'proxy', { const batchResult = await processItems(proxies, 'proxy', {
handler: 'proxy', handler: 'proxy',
@ -47,7 +51,7 @@ export function initializeProxyProvider(_container: ServiceContainer) {
ttl: 30000, // 30 second timeout per proxy check ttl: 30000, // 30 second timeout per proxy check
removeOnComplete: 5, removeOnComplete: 5,
removeOnFail: 3, removeOnFail: 3,
}); }, queueManager);
handlerLogger.info('Batch proxy validation completed', { handlerLogger.info('Batch proxy validation completed', {
totalProxies: proxies.length, totalProxies: proxies.length,

View file

@ -6,7 +6,7 @@ import { Hono } from 'hono';
import type { IServiceContainer } from '@stock-bot/handlers'; import type { IServiceContainer } from '@stock-bot/handlers';
import { exchangeRoutes } from './exchange.routes'; import { exchangeRoutes } from './exchange.routes';
import { healthRoutes } from './health.routes'; import { healthRoutes } from './health.routes';
import { queueRoutes } from './queue.routes'; import { createQueueRoutes } from './queue.routes';
/** /**
* Creates all routes with access to type-safe services * Creates all routes with access to type-safe services
@ -17,9 +17,9 @@ export function createRoutes(services: IServiceContainer): Hono {
// Mount routes that don't need services // Mount routes that don't need services
app.route('/health', healthRoutes); app.route('/health', healthRoutes);
// Mount routes that need services (will be updated to use services) // Mount routes that need services
app.route('/api/exchanges', exchangeRoutes); app.route('/api/exchanges', exchangeRoutes);
app.route('/api/queue', queueRoutes); app.route('/api/queue', createQueueRoutes(services));
// Store services in app context for handlers that need it // Store services in app context for handlers that need it
app.use('*', async (c, next) => { app.use('*', async (c, next) => {

View file

@ -3,119 +3,140 @@
*/ */
import { Hono } from 'hono'; import { Hono } from 'hono';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { processItems, QueueManager } from '@stock-bot/queue'; import { processItems } from '@stock-bot/queue';
import type { IServiceContainer } from '@stock-bot/handlers';
const logger = getLogger('market-data-routes'); const logger = getLogger('market-data-routes');
export const marketDataRoutes = new Hono(); export function createMarketDataRoutes(container: IServiceContainer) {
const marketDataRoutes = new Hono();
// Market data endpoints // Market data endpoints
marketDataRoutes.get('/api/live/:symbol', async c => { marketDataRoutes.get('/api/live/:symbol', async c => {
const symbol = c.req.param('symbol'); const symbol = c.req.param('symbol');
logger.info('Live data request', { symbol }); logger.info('Live data request', { symbol });
try { try {
// Queue job for live data using Yahoo provider // Queue job for live data using Yahoo provider
const queueManager = QueueManager.getInstance(); const queueManager = container.queue;
const queue = queueManager.getQueue('yahoo-finance'); if (!queueManager) {
const job = await queue.add('live-data', { return c.json({ status: 'error', message: 'Queue manager not available' }, 503);
handler: 'yahoo-finance', }
operation: 'live-data',
payload: { symbol },
});
return c.json({
status: 'success',
message: 'Live data job queued',
jobId: job.id,
symbol,
});
} catch (error) {
logger.error('Failed to queue live data job', { symbol, error });
return c.json({ status: 'error', message: 'Failed to queue live data job' }, 500);
}
});
marketDataRoutes.get('/api/historical/:symbol', async c => { const queue = queueManager.getQueue('yahoo-finance');
const symbol = c.req.param('symbol'); const job = await queue.add('live-data', {
const from = c.req.query('from'); handler: 'yahoo-finance',
const to = c.req.query('to'); operation: 'live-data',
payload: { symbol },
logger.info('Historical data request', { symbol, from, to }); });
return c.json({
try { status: 'success',
const fromDate = from ? new Date(from) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 days ago message: 'Live data job queued',
const toDate = to ? new Date(to) : new Date(); // Now jobId: job.id,
// Queue job for historical data using Yahoo provider
const queueManager = QueueManager.getInstance();
const queue = queueManager.getQueue('yahoo-finance');
const job = await queue.add('historical-data', {
handler: 'yahoo-finance',
operation: 'historical-data',
payload: {
symbol, symbol,
from: fromDate.toISOString(), });
to: toDate.toISOString(), } catch (error) {
}, logger.error('Failed to queue live data job', { symbol, error });
}); return c.json({ status: 'error', message: 'Failed to queue live data job' }, 500);
return c.json({
status: 'success',
message: 'Historical data job queued',
jobId: job.id,
symbol,
from: fromDate,
to: toDate,
});
} catch (error) {
logger.error('Failed to queue historical data job', { symbol, from, to, error });
return c.json({ status: 'error', message: 'Failed to queue historical data job' }, 500);
}
});
// Batch processing endpoint using new queue system
marketDataRoutes.post('/api/process-symbols', async c => {
try {
const {
symbols,
provider = 'ib',
operation = 'fetch-session',
useBatching = true,
totalDelayHours = 0.0083, // ~30 seconds (30/3600 hours)
batchSize = 10,
} = await c.req.json();
if (!symbols || !Array.isArray(symbols) || symbols.length === 0) {
return c.json({ status: 'error', message: 'Invalid symbols array' }, 400);
} }
});
logger.info('Batch processing symbols', { marketDataRoutes.get('/api/historical/:symbol', async c => {
count: symbols.length, const symbol = c.req.param('symbol');
provider, const from = c.req.query('from');
operation, const to = c.req.query('to');
useBatching,
});
const result = await processItems(symbols, provider, { logger.info('Historical data request', { symbol, from, to });
handler: provider,
operation,
totalDelayHours,
useBatching,
batchSize,
priority: 2,
retries: 2,
removeOnComplete: 5,
removeOnFail: 10,
});
return c.json({ try {
status: 'success', const fromDate = from ? new Date(from) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 days ago
message: 'Batch processing initiated', const toDate = to ? new Date(to) : new Date(); // Now
result,
symbols: symbols.length, // Queue job for historical data using Yahoo provider
}); const queueManager = container.queue;
} catch (error) { if (!queueManager) {
logger.error('Failed to process symbols batch', { error }); return c.json({ status: 'error', message: 'Queue manager not available' }, 503);
return c.json({ status: 'error', message: 'Failed to process symbols batch' }, 500); }
}
}); const queue = queueManager.getQueue('yahoo-finance');
const job = await queue.add('historical-data', {
handler: 'yahoo-finance',
operation: 'historical-data',
payload: {
symbol,
from: fromDate.toISOString(),
to: toDate.toISOString(),
},
});
return c.json({
status: 'success',
message: 'Historical data job queued',
jobId: job.id,
symbol,
from: fromDate,
to: toDate,
});
} catch (error) {
logger.error('Failed to queue historical data job', { symbol, from, to, error });
return c.json({ status: 'error', message: 'Failed to queue historical data job' }, 500);
}
});
// Batch processing endpoint using new queue system
marketDataRoutes.post('/api/process-symbols', async c => {
try {
const {
symbols,
provider = 'ib',
operation = 'fetch-session',
useBatching = true,
totalDelayHours = 0.0083, // ~30 seconds (30/3600 hours)
batchSize = 10,
} = await c.req.json();
if (!symbols || !Array.isArray(symbols) || symbols.length === 0) {
return c.json({ status: 'error', message: 'Invalid symbols array' }, 400);
}
logger.info('Batch processing symbols', {
count: symbols.length,
provider,
operation,
useBatching,
});
const queueManager = container.queue;
if (!queueManager) {
return c.json({ status: 'error', message: 'Queue manager not available' }, 503);
}
const result = await processItems(symbols, provider, {
handler: provider,
operation,
totalDelayHours,
useBatching,
batchSize,
priority: 2,
retries: 2,
removeOnComplete: 5,
removeOnFail: 10,
}, queueManager);
return c.json({
status: 'success',
message: 'Batch processing initiated',
result,
symbols: symbols.length,
});
} catch (error) {
logger.error('Failed to process symbols batch', { error });
return c.json({ status: 'error', message: 'Failed to process symbols batch' }, 500);
}
});
return marketDataRoutes;
}
// Legacy export for backward compatibility
export const marketDataRoutes = createMarketDataRoutes({} as IServiceContainer);

View file

@ -1,25 +1,35 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { QueueManager } from '@stock-bot/queue'; import type { IServiceContainer } from '@stock-bot/handlers';
const logger = getLogger('queue-routes'); const logger = getLogger('queue-routes');
const queue = new Hono();
// Queue status endpoint export function createQueueRoutes(container: IServiceContainer) {
queue.get('/status', async c => { const queue = new Hono();
try {
const queueManager = QueueManager.getInstance();
const globalStats = await queueManager.getGlobalStats();
return c.json({ // Queue status endpoint
status: 'success', queue.get('/status', async c => {
data: globalStats, try {
message: 'Queue status retrieved successfully', const queueManager = container.queue;
}); if (!queueManager) {
} catch (error) { return c.json({ status: 'error', message: 'Queue manager not available' }, 503);
logger.error('Failed to get queue status', { error }); }
return c.json({ status: 'error', message: 'Failed to get queue status' }, 500);
}
});
export { queue as queueRoutes }; const globalStats = await queueManager.getGlobalStats();
return c.json({
status: 'success',
data: globalStats,
message: 'Queue status retrieved successfully',
});
} catch (error) {
logger.error('Failed to get queue status', { error });
return c.json({ status: 'error', message: 'Failed to get queue status' }, 500);
}
});
return queue;
}
// Legacy export for backward compatibility
export const queueRoutes = createQueueRoutes({} as IServiceContainer);

View file

@ -4,10 +4,13 @@
*/ */
import { Hono } from 'hono'; import { Hono } from 'hono';
import type { ServiceContainer } from '@stock-bot/di'; import type { IServiceContainer } from '@stock-bot/handlers';
import { healthRoutes, syncRoutes, enhancedSyncRoutes, statsRoutes } from './index'; import { healthRoutes } from './health.routes';
import { createSyncRoutes } from './sync.routes';
import { createEnhancedSyncRoutes } from './enhanced-sync.routes';
import { createStatsRoutes } from './stats.routes';
export function createRoutes(container: ServiceContainer): Hono { export function createRoutes(container: IServiceContainer): Hono {
const app = new Hono(); const app = new Hono();
// Add container to context for all routes // Add container to context for all routes
@ -18,9 +21,9 @@ export function createRoutes(container: ServiceContainer): Hono {
// Mount routes // Mount routes
app.route('/health', healthRoutes); app.route('/health', healthRoutes);
app.route('/sync', syncRoutes); app.route('/sync', createSyncRoutes(container));
app.route('/sync', enhancedSyncRoutes); app.route('/sync', createEnhancedSyncRoutes(container));
app.route('/sync/stats', statsRoutes); app.route('/sync/stats', createStatsRoutes(container));
return app; return app;
} }

View file

@ -1,100 +1,154 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { QueueManager } from '@stock-bot/queue'; import type { IServiceContainer } from '@stock-bot/handlers';
const logger = getLogger('enhanced-sync-routes'); const logger = getLogger('enhanced-sync-routes');
const enhancedSync = new Hono();
// Enhanced sync endpoints export function createEnhancedSyncRoutes(container: IServiceContainer) {
enhancedSync.post('/exchanges/all', async c => { const enhancedSync = new Hono();
try {
const clearFirst = c.req.query('clear') === 'true';
const queueManager = QueueManager.getInstance();
const exchangesQueue = queueManager.getQueue('exchanges');
const job = await exchangesQueue.addJob('sync-all-exchanges', { // Enhanced sync endpoints
handler: 'exchanges', enhancedSync.post('/exchanges/all', async c => {
operation: 'sync-all-exchanges', try {
payload: { clearFirst }, const clearFirst = c.req.query('clear') === 'true';
}); const queueManager = container.queue;
if (!queueManager) {
return c.json({ success: false, error: 'Queue manager not available' }, 503);
}
return c.json({ success: true, jobId: job.id, message: 'Enhanced exchange sync job queued' }); const exchangesQueue = queueManager.getQueue('exchanges');
} catch (error) {
logger.error('Failed to queue enhanced exchange sync job', { error });
return c.json(
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
500
);
}
});
enhancedSync.post('/provider-mappings/qm', async c => { const job = await exchangesQueue.addJob('sync-all-exchanges', {
try { handler: 'exchanges',
const queueManager = QueueManager.getInstance(); operation: 'sync-all-exchanges',
const exchangesQueue = queueManager.getQueue('exchanges'); payload: { clearFirst },
});
const job = await exchangesQueue.addJob('sync-qm-provider-mappings', { return c.json({ success: true, jobId: job.id, message: 'Enhanced exchange sync job queued' });
handler: 'exchanges', } catch (error) {
operation: 'sync-qm-provider-mappings', logger.error('Failed to queue enhanced exchange sync job', { error });
payload: {}, return c.json(
}); { success: false, error: error instanceof Error ? error.message : 'Unknown error' },
500
);
}
});
return c.json({ enhancedSync.post('/provider-mappings/qm', async c => {
success: true, try {
jobId: job.id, const queueManager = container.queue;
message: 'QM provider mappings sync job queued', if (!queueManager) {
}); return c.json({ success: false, error: 'Queue manager not available' }, 503);
} catch (error) { }
logger.error('Failed to queue QM provider mappings sync job', { error });
return c.json(
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
500
);
}
});
enhancedSync.post('/symbols/:provider', async c => { const exchangesQueue = queueManager.getQueue('exchanges');
try {
const provider = c.req.param('provider');
const clearFirst = c.req.query('clear') === 'true';
const queueManager = QueueManager.getInstance();
const symbolsQueue = queueManager.getQueue('symbols');
const job = await symbolsQueue.addJob(`sync-symbols-${provider}`, { const job = await exchangesQueue.addJob('sync-qm-provider-mappings', {
handler: 'symbols', handler: 'exchanges',
operation: `sync-symbols-${provider}`, operation: 'sync-qm-provider-mappings',
payload: { provider, clearFirst }, payload: {},
}); });
return c.json({ success: true, jobId: job.id, message: `${provider} symbols sync job queued` }); return c.json({
} catch (error) { success: true,
logger.error('Failed to queue enhanced symbol sync job', { error }); jobId: job.id,
return c.json( message: 'QM provider mappings sync job queued',
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' }, });
500 } catch (error) {
); logger.error('Failed to queue QM provider mappings sync job', { error });
} return c.json(
}); { success: false, error: error instanceof Error ? error.message : 'Unknown error' },
500
);
}
});
// Enhanced status endpoints enhancedSync.post('/provider-mappings/ib', async c => {
enhancedSync.get('/status/enhanced', async c => { try {
try { const queueManager = container.queue;
const queueManager = QueueManager.getInstance(); if (!queueManager) {
const exchangesQueue = queueManager.getQueue('exchanges'); return c.json({ success: false, error: 'Queue manager not available' }, 503);
}
const job = await exchangesQueue.addJob('enhanced-sync-status', { const exchangesQueue = queueManager.getQueue('exchanges');
handler: 'exchanges',
operation: 'enhanced-sync-status',
payload: {},
});
// Wait for job to complete and return result const job = await exchangesQueue.addJob('sync-ib-exchanges', {
const result = await job.waitUntilFinished(); handler: 'exchanges',
return c.json(result); operation: 'sync-ib-exchanges',
} catch (error) { payload: {},
logger.error('Failed to get enhanced sync status', { error }); });
return c.json({ error: error instanceof Error ? error.message : 'Unknown error' }, 500);
}
});
export { enhancedSync as enhancedSyncRoutes }; return c.json({
success: true,
jobId: job.id,
message: 'IB exchanges sync job queued',
});
} catch (error) {
logger.error('Failed to queue IB exchanges sync job', { error });
return c.json(
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
500
);
}
});
enhancedSync.get('/status', async c => {
try {
const queueManager = container.queue;
if (!queueManager) {
return c.json({ success: false, error: 'Queue manager not available' }, 503);
}
const symbolsQueue = queueManager.getQueue('symbols');
const job = await symbolsQueue.addJob('sync-status', {
handler: 'symbols',
operation: 'sync-status',
payload: {},
});
return c.json({ success: true, jobId: job.id, message: 'Sync status job queued' });
} catch (error) {
logger.error('Failed to queue sync status job', { error });
return c.json(
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
500
);
}
});
enhancedSync.post('/clear/postgresql', async c => {
try {
const dataType = c.req.query('type') as 'exchanges' | 'provider_mappings' | 'all';
const queueManager = container.queue;
if (!queueManager) {
return c.json({ success: false, error: 'Queue manager not available' }, 503);
}
const exchangesQueue = queueManager.getQueue('exchanges');
const job = await exchangesQueue.addJob('clear-postgresql-data', {
handler: 'exchanges',
operation: 'clear-postgresql-data',
payload: { dataType: dataType || 'all' },
});
return c.json({
success: true,
jobId: job.id,
message: 'PostgreSQL data clear job queued',
});
} catch (error) {
logger.error('Failed to queue PostgreSQL clear job', { error });
return c.json(
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
500
);
}
});
return enhancedSync;
}
// Legacy export for backward compatibility
export const enhancedSyncRoutes = createEnhancedSyncRoutes({} as IServiceContainer);

View file

@ -1,49 +1,63 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { QueueManager } from '@stock-bot/queue'; import type { IServiceContainer } from '@stock-bot/handlers';
const logger = getLogger('stats-routes'); const logger = getLogger('stats-routes');
const stats = new Hono();
// Statistics endpoints export function createStatsRoutes(container: IServiceContainer) {
stats.get('/exchanges', async c => { const stats = new Hono();
try {
const queueManager = QueueManager.getInstance();
const exchangesQueue = queueManager.getQueue('exchanges');
const job = await exchangesQueue.addJob('get-exchange-stats', { // Statistics endpoints
handler: 'exchanges', stats.get('/exchanges', async c => {
operation: 'get-exchange-stats', try {
payload: {}, const queueManager = container.queue;
}); if (!queueManager) {
return c.json({ error: 'Queue manager not available' }, 503);
}
// Wait for job to complete and return result const exchangesQueue = queueManager.getQueue('exchanges');
const result = await job.waitUntilFinished();
return c.json(result);
} catch (error) {
logger.error('Failed to get exchange stats', { error });
return c.json({ error: error instanceof Error ? error.message : 'Unknown error' }, 500);
}
});
stats.get('/provider-mappings', async c => { const job = await exchangesQueue.addJob('get-exchange-stats', {
try { handler: 'exchanges',
const queueManager = QueueManager.getInstance(); operation: 'get-exchange-stats',
const exchangesQueue = queueManager.getQueue('exchanges'); payload: {},
});
const job = await exchangesQueue.addJob('get-provider-mapping-stats', { // Wait for job to complete and return result
handler: 'exchanges', const result = await job.waitUntilFinished();
operation: 'get-provider-mapping-stats', return c.json(result);
payload: {}, } catch (error) {
}); logger.error('Failed to get exchange stats', { error });
return c.json({ error: error instanceof Error ? error.message : 'Unknown error' }, 500);
}
});
// Wait for job to complete and return result stats.get('/provider-mappings', async c => {
const result = await job.waitUntilFinished(); try {
return c.json(result); const queueManager = container.queue;
} catch (error) { if (!queueManager) {
logger.error('Failed to get provider mapping stats', { error }); return c.json({ error: 'Queue manager not available' }, 503);
return c.json({ error: error instanceof Error ? error.message : 'Unknown error' }, 500); }
}
});
export { stats as statsRoutes }; const exchangesQueue = queueManager.getQueue('exchanges');
const job = await exchangesQueue.addJob('get-provider-mapping-stats', {
handler: 'exchanges',
operation: 'get-provider-mapping-stats',
payload: {},
});
// Wait for job to complete and return result
const result = await job.waitUntilFinished();
return c.json(result);
} catch (error) {
logger.error('Failed to get provider mapping stats', { error });
return c.json({ error: error instanceof Error ? error.message : 'Unknown error' }, 500);
}
});
return stats;
}
// Legacy export for backward compatibility
export const statsRoutes = createStatsRoutes({} as IServiceContainer);

View file

@ -1,96 +1,95 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import { getLogger } from '@stock-bot/logger'; import { getLogger } from '@stock-bot/logger';
import { QueueManager } from '@stock-bot/queue'; import type { IServiceContainer } from '@stock-bot/handlers';
const logger = getLogger('sync-routes'); const logger = getLogger('sync-routes');
const sync = new Hono();
// Manual sync trigger endpoints export function createSyncRoutes(container: IServiceContainer) {
sync.post('/symbols', async c => { const sync = new Hono();
try {
const queueManager = QueueManager.getInstance();
const symbolsQueue = queueManager.getQueue('symbols');
const job = await symbolsQueue.addJob('sync-qm-symbols', { // Manual sync trigger endpoints
handler: 'symbols', sync.post('/symbols', async c => {
operation: 'sync-qm-symbols', try {
payload: {}, const queueManager = container.queue;
}); if (!queueManager) {
return c.json({ success: false, error: 'Queue manager not available' }, 503);
}
return c.json({ success: true, jobId: job.id, message: 'QM symbols sync job queued' }); const symbolsQueue = queueManager.getQueue('symbols');
} catch (error) {
logger.error('Failed to queue symbol sync job', { error });
return c.json(
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
500
);
}
});
sync.post('/exchanges', async c => { const job = await symbolsQueue.addJob('sync-qm-symbols', {
try { handler: 'symbols',
const queueManager = QueueManager.getInstance(); operation: 'sync-qm-symbols',
const exchangesQueue = queueManager.getQueue('exchanges'); payload: {},
});
const job = await exchangesQueue.addJob('sync-qm-exchanges', { return c.json({ success: true, jobId: job.id, message: 'QM symbols sync job queued' });
handler: 'exchanges', } catch (error) {
operation: 'sync-qm-exchanges', logger.error('Failed to queue symbol sync job', { error });
payload: {}, return c.json(
}); { success: false, error: error instanceof Error ? error.message : 'Unknown error' },
500
);
}
});
return c.json({ success: true, jobId: job.id, message: 'QM exchanges sync job queued' }); sync.post('/exchanges', async c => {
} catch (error) { try {
logger.error('Failed to queue exchange sync job', { error }); const queueManager = container.queue;
return c.json( if (!queueManager) {
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' }, return c.json({ success: false, error: 'Queue manager not available' }, 503);
500 }
);
}
});
// Get sync status const exchangesQueue = queueManager.getQueue('exchanges');
sync.get('/status', async c => {
try {
const queueManager = QueueManager.getInstance();
const symbolsQueue = queueManager.getQueue('symbols');
const job = await symbolsQueue.addJob('sync-status', { const job = await exchangesQueue.addJob('sync-qm-exchanges', {
handler: 'symbols', handler: 'exchanges',
operation: 'sync-status', operation: 'sync-qm-exchanges',
payload: {}, payload: {},
}); });
// Wait for job to complete and return result return c.json({ success: true, jobId: job.id, message: 'QM exchanges sync job queued' });
const result = await job.waitUntilFinished(); } catch (error) {
return c.json(result); logger.error('Failed to queue exchange sync job', { error });
} catch (error) { return c.json(
logger.error('Failed to get sync status', { error }); { success: false, error: error instanceof Error ? error.message : 'Unknown error' },
return c.json({ error: error instanceof Error ? error.message : 'Unknown error' }, 500); 500
} );
}); }
});
// Clear data endpoint sync.post('/symbols/:provider', async c => {
sync.post('/clear', async c => { try {
try { const provider = c.req.param('provider');
const queueManager = QueueManager.getInstance(); const queueManager = container.queue;
const exchangesQueue = queueManager.getQueue('exchanges'); if (!queueManager) {
return c.json({ success: false, error: 'Queue manager not available' }, 503);
}
const job = await exchangesQueue.addJob('clear-postgresql-data', { const symbolsQueue = queueManager.getQueue('symbols');
handler: 'exchanges',
operation: 'clear-postgresql-data',
payload: {},
});
// Wait for job to complete and return result const job = await symbolsQueue.addJob('sync-symbols-from-provider', {
const result = await job.waitUntilFinished(); handler: 'symbols',
return c.json({ success: true, result }); operation: 'sync-symbols-from-provider',
} catch (error) { payload: { provider },
logger.error('Failed to clear PostgreSQL data', { error }); });
return c.json(
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
500
);
}
});
export { sync as syncRoutes }; return c.json({
success: true,
jobId: job.id,
message: `${provider} symbols sync job queued`,
});
} catch (error) {
logger.error('Failed to queue provider symbol sync job', { error });
return c.json(
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
500
);
}
});
return sync;
}
// Legacy export for backward compatibility
export const syncRoutes = createSyncRoutes({} as IServiceContainer);

View file

@ -188,19 +188,18 @@ export function createServiceContainer(rawConfig: unknown): AwilixContainer<Serv
registrations.questdbClient = asValue(null); registrations.questdbClient = asValue(null);
} }
// Queue manager - placeholder until decoupled from singleton // Queue manager - properly instantiated with DI
registrations.queueManager = asFunction(({ redisConfig, cache, logger }) => { registrations.queueManager = asFunction(({ redisConfig, logger }) => {
// Import dynamically to avoid circular dependency
const { QueueManager } = require('@stock-bot/queue'); const { QueueManager } = require('@stock-bot/queue');
// Check if already initialized (singleton pattern) return new QueueManager({
if (QueueManager.isInitialized()) { redis: {
return QueueManager.getInstance(); host: redisConfig.host,
} port: redisConfig.port,
db: redisConfig.db,
// Initialize if not already done password: redisConfig.password,
return QueueManager.initialize({ username: redisConfig.username,
redis: { host: redisConfig.host, port: redisConfig.port, db: redisConfig.db }, },
enableScheduledJobs: true, enableScheduledJobs: true,
delayWorkerStart: true, // We'll start workers manually delayWorkerStart: true, // We'll start workers manually
}); });

View file

@ -11,9 +11,9 @@ const logger = getLogger('batch-processor');
export async function processItems<T>( export async function processItems<T>(
items: T[], items: T[],
queueName: string, queueName: string,
options: ProcessOptions options: ProcessOptions,
queueManager: QueueManager
): Promise<BatchResult> { ): Promise<BatchResult> {
const queueManager = QueueManager.getInstance();
queueManager.getQueue(queueName); queueManager.getQueue(queueName);
const startTime = Date.now(); const startTime = Date.now();
@ -35,8 +35,8 @@ export async function processItems<T>(
try { try {
const result = options.useBatching const result = options.useBatching
? await processBatched(items, queueName, options) ? await processBatched(items, queueName, options, queueManager)
: await processDirect(items, queueName, options); : await processDirect(items, queueName, options, queueManager);
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
@ -58,9 +58,9 @@ export async function processItems<T>(
async function processDirect<T>( async function processDirect<T>(
items: T[], items: T[],
queueName: string, queueName: string,
options: ProcessOptions options: ProcessOptions,
queueManager: QueueManager
): Promise<Omit<BatchResult, 'duration'>> { ): Promise<Omit<BatchResult, 'duration'>> {
const queueManager = QueueManager.getInstance();
queueManager.getQueue(queueName); queueManager.getQueue(queueName);
const totalDelayMs = options.totalDelayHours * 60 * 60 * 1000; // Convert hours to milliseconds const totalDelayMs = options.totalDelayHours * 60 * 60 * 1000; // Convert hours to milliseconds
const delayPerItem = totalDelayMs / items.length; const delayPerItem = totalDelayMs / items.length;
@ -87,7 +87,7 @@ async function processDirect<T>(
}, },
})); }));
const createdJobs = await addJobsInChunks(queueName, jobs); const createdJobs = await addJobsInChunks(queueName, jobs, queueManager);
return { return {
totalItems: items.length, totalItems: items.length,
@ -102,9 +102,9 @@ async function processDirect<T>(
async function processBatched<T>( async function processBatched<T>(
items: T[], items: T[],
queueName: string, queueName: string,
options: ProcessOptions options: ProcessOptions,
queueManager: QueueManager
): Promise<Omit<BatchResult, 'duration'>> { ): Promise<Omit<BatchResult, 'duration'>> {
const queueManager = QueueManager.getInstance();
queueManager.getQueue(queueName); queueManager.getQueue(queueName);
const batchSize = options.batchSize || 100; const batchSize = options.batchSize || 100;
const batches = createBatches(items, batchSize); const batches = createBatches(items, batchSize);
@ -121,7 +121,7 @@ async function processBatched<T>(
const batchJobs = await Promise.all( const batchJobs = await Promise.all(
batches.map(async (batch, batchIndex) => { batches.map(async (batch, batchIndex) => {
// Just store the items directly - no processing needed // Just store the items directly - no processing needed
const payloadKey = await storeItems(batch, queueName, options); const payloadKey = await storeItems(batch, queueName, options, queueManager);
return { return {
name: 'process-batch', name: 'process-batch',
@ -148,7 +148,7 @@ async function processBatched<T>(
}) })
); );
const createdJobs = await addJobsInChunks(queueName, batchJobs); const createdJobs = await addJobsInChunks(queueName, batchJobs, queueManager);
return { return {
totalItems: items.length, totalItems: items.length,
@ -161,8 +161,7 @@ async function processBatched<T>(
/** /**
* Process a batch job - loads items and creates individual jobs * Process a batch job - loads items and creates individual jobs
*/ */
export async function processBatchJob(jobData: BatchJobData, queueName: string): Promise<unknown> { export async function processBatchJob(jobData: BatchJobData, queueName: string, queueManager: QueueManager): Promise<unknown> {
const queueManager = QueueManager.getInstance();
queueManager.getQueue(queueName); queueManager.getQueue(queueName);
const { payloadKey, batchIndex, totalBatches, itemCount, totalDelayHours } = jobData; const { payloadKey, batchIndex, totalBatches, itemCount, totalDelayHours } = jobData;
@ -174,7 +173,7 @@ export async function processBatchJob(jobData: BatchJobData, queueName: string):
}); });
try { try {
const payload = await loadPayload(payloadKey, queueName); const payload = await loadPayload(payloadKey, queueName, queueManager);
if (!payload || !payload.items || !payload.options) { if (!payload || !payload.items || !payload.options) {
logger.error('Invalid payload data', { payloadKey, payload }); logger.error('Invalid payload data', { payloadKey, payload });
throw new Error(`Invalid payload data for key: ${payloadKey}`); throw new Error(`Invalid payload data for key: ${payloadKey}`);
@ -210,10 +209,10 @@ export async function processBatchJob(jobData: BatchJobData, queueName: string):
}, },
})); }));
const createdJobs = await addJobsInChunks(queueName, jobs); const createdJobs = await addJobsInChunks(queueName, jobs, queueManager);
// Cleanup payload after successful processing // Cleanup payload after successful processing
await cleanupPayload(payloadKey, queueName); await cleanupPayload(payloadKey, queueName, queueManager);
return { return {
batchIndex, batchIndex,
@ -239,9 +238,9 @@ function createBatches<T>(items: T[], batchSize: number): T[][] {
async function storeItems<T>( async function storeItems<T>(
items: T[], items: T[],
queueName: string, queueName: string,
options: ProcessOptions options: ProcessOptions,
queueManager: QueueManager
): Promise<string> { ): Promise<string> {
const queueManager = QueueManager.getInstance();
const cache = queueManager.getCache(queueName); const cache = queueManager.getCache(queueName);
const payloadKey = `payload:${Date.now()}:${Math.random().toString(36).substr(2, 9)}`; const payloadKey = `payload:${Date.now()}:${Math.random().toString(36).substr(2, 9)}`;
@ -265,7 +264,8 @@ async function storeItems<T>(
async function loadPayload<T>( async function loadPayload<T>(
key: string, key: string,
queueName: string queueName: string,
queueManager: QueueManager
): Promise<{ ): Promise<{
items: T[]; items: T[];
options: { options: {
@ -276,7 +276,6 @@ async function loadPayload<T>(
operation: string; operation: string;
}; };
} | null> { } | null> {
const queueManager = QueueManager.getInstance();
const cache = queueManager.getCache(queueName); const cache = queueManager.getCache(queueName);
return (await cache.get(key)) as { return (await cache.get(key)) as {
items: T[]; items: T[];
@ -290,8 +289,7 @@ async function loadPayload<T>(
} | null; } | null;
} }
async function cleanupPayload(key: string, queueName: string): Promise<void> { async function cleanupPayload(key: string, queueName: string, queueManager: QueueManager): Promise<void> {
const queueManager = QueueManager.getInstance();
const cache = queueManager.getCache(queueName); const cache = queueManager.getCache(queueName);
await cache.del(key); await cache.del(key);
} }
@ -299,9 +297,9 @@ async function cleanupPayload(key: string, queueName: string): Promise<void> {
async function addJobsInChunks( async function addJobsInChunks(
queueName: string, queueName: string,
jobs: Array<{ name: string; data: JobData; opts?: Record<string, unknown> }>, jobs: Array<{ name: string; data: JobData; opts?: Record<string, unknown> }>,
queueManager: QueueManager,
chunkSize = 100 chunkSize = 100
): Promise<unknown[]> { ): Promise<unknown[]> {
const queueManager = QueueManager.getInstance();
const queue = queueManager.getQueue(queueName); const queue = queueManager.getQueue(queueName);
const allCreatedJobs = []; const allCreatedJobs = [];

View file

@ -15,7 +15,7 @@ import { getRedisConnection } from './utils';
const logger = getLogger('queue-manager'); const logger = getLogger('queue-manager');
/** /**
* Singleton QueueManager that provides unified queue and cache management * QueueManager provides unified queue and cache management
* Main entry point for all queue operations with getQueue() method * Main entry point for all queue operations with getQueue() method
*/ */
export class QueueManager { export class QueueManager {
@ -28,7 +28,7 @@ export class QueueManager {
private shutdownPromise: Promise<void> | null = null; private shutdownPromise: Promise<void> | null = null;
private config: QueueManagerConfig; private config: QueueManagerConfig;
private constructor(config: QueueManagerConfig) { constructor(config: QueueManagerConfig) {
this.config = config; this.config = config;
this.redisConnection = getRedisConnection(config.redis); this.redisConnection = getRedisConnection(config.redis);
@ -42,16 +42,20 @@ export class QueueManager {
}); });
} }
logger.info('QueueManager singleton initialized', { logger.info('QueueManager initialized', {
redis: `${config.redis.host}:${config.redis.port}`, redis: `${config.redis.host}:${config.redis.port}`,
}); });
} }
/** /**
* @deprecated Use dependency injection instead. This method will be removed in a future version.
* Get the singleton instance * Get the singleton instance
* @throws Error if not initialized - use initialize() first * @throws Error if not initialized - use initialize() first
*/ */
static getInstance(): QueueManager { static getInstance(): QueueManager {
logger.warn(
'QueueManager.getInstance() is deprecated. Please use dependency injection instead.'
);
if (!QueueManager.instance) { if (!QueueManager.instance) {
throw new Error('QueueManager not initialized. Call QueueManager.initialize(config) first.'); throw new Error('QueueManager not initialized. Call QueueManager.initialize(config) first.');
} }
@ -59,10 +63,14 @@ export class QueueManager {
} }
/** /**
* @deprecated Use dependency injection instead. This method will be removed in a future version.
* Initialize the singleton with config * Initialize the singleton with config
* Must be called before getInstance() * Must be called before getInstance()
*/ */
static initialize(config: QueueManagerConfig): QueueManager { static initialize(config: QueueManagerConfig): QueueManager {
logger.warn(
'QueueManager.initialize() is deprecated. Please use dependency injection instead.'
);
if (QueueManager.instance) { if (QueueManager.instance) {
logger.warn('QueueManager already initialized, returning existing instance'); logger.warn('QueueManager already initialized, returning existing instance');
return QueueManager.instance; return QueueManager.instance;
@ -72,10 +80,14 @@ export class QueueManager {
} }
/** /**
* @deprecated Use dependency injection instead. This method will be removed in a future version.
* Get or initialize the singleton * Get or initialize the singleton
* Convenience method that combines initialize and getInstance * Convenience method that combines initialize and getInstance
*/ */
static getOrInitialize(config?: QueueManagerConfig): QueueManager { static getOrInitialize(config?: QueueManagerConfig): QueueManager {
logger.warn(
'QueueManager.getOrInitialize() is deprecated. Please use dependency injection instead.'
);
if (QueueManager.instance) { if (QueueManager.instance) {
return QueueManager.instance; return QueueManager.instance;
} }
@ -91,6 +103,7 @@ export class QueueManager {
} }
/** /**
* @deprecated Use dependency injection instead. This method will be removed in a future version.
* Check if the QueueManager is initialized * Check if the QueueManager is initialized
*/ */
static isInitialized(): boolean { static isInitialized(): boolean {
@ -98,6 +111,7 @@ export class QueueManager {
} }
/** /**
* @deprecated Use dependency injection instead. This method will be removed in a future version.
* Reset the singleton (mainly for testing) * Reset the singleton (mainly for testing)
*/ */
static async reset(): Promise<void> { static async reset(): Promise<void> {