huge refactor with a million of things to make the code much more managable and easier to create new services #3
19 changed files with 309 additions and 549 deletions
97
DATA-INGESTION-REFACTOR-SUMMARY.md
Normal file
97
DATA-INGESTION-REFACTOR-SUMMARY.md
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
# Data-Ingestion Service Refactor Summary
|
||||||
|
|
||||||
|
## What Was Done
|
||||||
|
|
||||||
|
Successfully refactored the `data-ingestion` service to use the new connection pool pattern, completely removing dependencies on the singleton anti-pattern.
|
||||||
|
|
||||||
|
### Key Changes
|
||||||
|
|
||||||
|
1. **Service Container Setup**
|
||||||
|
- Created `database-setup.ts` with proper connection factory configuration
|
||||||
|
- Configured appropriate pool sizes for data ingestion workloads
|
||||||
|
- Added optional dynamic pool sizing for production environments
|
||||||
|
|
||||||
|
2. **Main Service Refactor** (`index.ts`)
|
||||||
|
- Removed `connectMongoDB()` and `connectPostgreSQL()` singleton calls
|
||||||
|
- Replaced with `setupServiceContainer()` initialization
|
||||||
|
- Updated shutdown handlers to dispose container properly
|
||||||
|
- Routes now have access to the service container
|
||||||
|
|
||||||
|
3. **Handler Updates**
|
||||||
|
- All handlers now accept `ServiceContainer` parameter
|
||||||
|
- QM handler operations use container-based OperationContext
|
||||||
|
- IB, Proxy, and WebShare handlers updated to accept container
|
||||||
|
- Added proper resource disposal with `ctx.dispose()`
|
||||||
|
|
||||||
|
4. **Route Refactoring**
|
||||||
|
- Created `create-routes.ts` factory function
|
||||||
|
- Routes can access container through Hono context
|
||||||
|
- Maintains backward compatibility for simple routes
|
||||||
|
|
||||||
|
5. **Migration Helper**
|
||||||
|
- Created temporary migration helper for legacy operations
|
||||||
|
- Provides `getMongoDBClient()` for IB operations still being migrated
|
||||||
|
- Includes cleanup in shutdown sequence
|
||||||
|
|
||||||
|
### Configuration Changes
|
||||||
|
|
||||||
|
- Added `@stock-bot/connection-factory` dependency
|
||||||
|
- Updated `tsconfig.json` with proper references
|
||||||
|
- Pool sizes optimized for data ingestion:
|
||||||
|
- MongoDB: 50 connections (batch imports)
|
||||||
|
- PostgreSQL: 30 connections
|
||||||
|
- Cache: 20 connections
|
||||||
|
|
||||||
|
### Benefits Achieved
|
||||||
|
|
||||||
|
1. **No More Global State**: Each service manages its own connections
|
||||||
|
2. **Better Resource Management**: Proper cleanup on shutdown
|
||||||
|
3. **Scalability**: Dynamic pool sizing for production workloads
|
||||||
|
4. **Monitoring**: Pool metrics available for observability
|
||||||
|
5. **Testing**: Easier to test with mock containers
|
||||||
|
6. **Gradual Migration**: Legacy operations still work during transition
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
1. **Complete Operation Migration**: Update IB operations to use container
|
||||||
|
2. **Remove Migration Helper**: Once all operations are migrated
|
||||||
|
3. **Add Monitoring**: Set up dashboards for pool metrics
|
||||||
|
4. **Performance Testing**: Validate pool sizes under load
|
||||||
|
5. **Replicate Pattern**: Apply same refactor to other services
|
||||||
|
|
||||||
|
### Example Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Handler with container
|
||||||
|
export function initializeHandler(container: ServiceContainer) {
|
||||||
|
const config = {
|
||||||
|
operations: {
|
||||||
|
'my-operation': createJobHandler(async (payload) => {
|
||||||
|
// Operation uses container
|
||||||
|
const ctx = OperationContext.create('handler', 'operation', { container });
|
||||||
|
try {
|
||||||
|
// Use databases through context
|
||||||
|
await ctx.mongodb.insertOne(data);
|
||||||
|
await ctx.postgres.query('...');
|
||||||
|
return { success: true };
|
||||||
|
} finally {
|
||||||
|
await ctx.dispose(); // Clean up resources
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Checklist
|
||||||
|
|
||||||
|
- [x] Remove singleton imports from index.ts
|
||||||
|
- [x] Create service container setup
|
||||||
|
- [x] Update all handlers to accept container
|
||||||
|
- [x] Create route factory with container access
|
||||||
|
- [x] Add migration helper for legacy code
|
||||||
|
- [x] Update shutdown handlers
|
||||||
|
- [x] Test build successfully
|
||||||
|
- [ ] Migrate remaining operations
|
||||||
|
- [ ] Remove migration helper
|
||||||
|
- [ ] Deploy and monitor
|
||||||
|
|
@ -14,12 +14,14 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@stock-bot/cache": "*",
|
"@stock-bot/cache": "*",
|
||||||
"@stock-bot/config": "*",
|
"@stock-bot/config": "*",
|
||||||
|
"@stock-bot/connection-factory": "*",
|
||||||
"@stock-bot/logger": "*",
|
"@stock-bot/logger": "*",
|
||||||
"@stock-bot/mongodb-client": "*",
|
"@stock-bot/mongodb-client": "*",
|
||||||
"@stock-bot/postgres-client": "*",
|
"@stock-bot/postgres-client": "*",
|
||||||
"@stock-bot/questdb-client": "*",
|
"@stock-bot/questdb-client": "*",
|
||||||
"@stock-bot/queue": "*",
|
"@stock-bot/queue": "*",
|
||||||
"@stock-bot/shutdown": "*",
|
"@stock-bot/shutdown": "*",
|
||||||
|
"@stock-bot/utils": "*",
|
||||||
"hono": "^4.0.0"
|
"hono": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,12 @@ import {
|
||||||
handlerRegistry,
|
handlerRegistry,
|
||||||
type HandlerConfigWithSchedule,
|
type HandlerConfigWithSchedule,
|
||||||
} from '@stock-bot/queue';
|
} from '@stock-bot/queue';
|
||||||
|
import type { ServiceContainer } from '@stock-bot/connection-factory';
|
||||||
|
|
||||||
const logger = getLogger('ib-provider');
|
const logger = getLogger('ib-provider');
|
||||||
|
|
||||||
// Initialize and register the IB provider
|
// Initialize and register the IB provider
|
||||||
export function initializeIBProvider() {
|
export function initializeIBProvider(container: ServiceContainer) {
|
||||||
logger.debug('Registering IB provider with scheduled jobs...');
|
logger.debug('Registering IB provider with scheduled jobs...');
|
||||||
|
|
||||||
const ibProviderConfig: HandlerConfigWithSchedule = {
|
const ibProviderConfig: HandlerConfigWithSchedule = {
|
||||||
|
|
@ -21,7 +22,7 @@ export function initializeIBProvider() {
|
||||||
// payload contains session configuration (not used in current implementation)
|
// payload contains session configuration (not used in current implementation)
|
||||||
logger.debug('Processing session fetch request');
|
logger.debug('Processing session fetch request');
|
||||||
const { fetchSession } = await import('./operations/session.operations');
|
const { fetchSession } = await import('./operations/session.operations');
|
||||||
return fetchSession();
|
return fetchSession(container);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
'fetch-exchanges': createJobHandler(async () => {
|
'fetch-exchanges': createJobHandler(async () => {
|
||||||
|
|
@ -29,9 +30,9 @@ export function initializeIBProvider() {
|
||||||
logger.debug('Processing exchanges fetch request');
|
logger.debug('Processing exchanges fetch request');
|
||||||
const { fetchSession } = await import('./operations/session.operations');
|
const { fetchSession } = await import('./operations/session.operations');
|
||||||
const { fetchExchanges } = await import('./operations/exchanges.operations');
|
const { fetchExchanges } = await import('./operations/exchanges.operations');
|
||||||
const sessionHeaders = await fetchSession();
|
const sessionHeaders = await fetchSession(container);
|
||||||
if (sessionHeaders) {
|
if (sessionHeaders) {
|
||||||
return fetchExchanges(sessionHeaders);
|
return fetchExchanges(sessionHeaders, container);
|
||||||
}
|
}
|
||||||
throw new Error('Failed to get session headers');
|
throw new Error('Failed to get session headers');
|
||||||
}),
|
}),
|
||||||
|
|
@ -41,9 +42,9 @@ export function initializeIBProvider() {
|
||||||
logger.debug('Processing symbols fetch request');
|
logger.debug('Processing symbols fetch request');
|
||||||
const { fetchSession } = await import('./operations/session.operations');
|
const { fetchSession } = await import('./operations/session.operations');
|
||||||
const { fetchSymbols } = await import('./operations/symbols.operations');
|
const { fetchSymbols } = await import('./operations/symbols.operations');
|
||||||
const sessionHeaders = await fetchSession();
|
const sessionHeaders = await fetchSession(container);
|
||||||
if (sessionHeaders) {
|
if (sessionHeaders) {
|
||||||
return fetchSymbols(sessionHeaders);
|
return fetchSymbols(sessionHeaders, container);
|
||||||
}
|
}
|
||||||
throw new Error('Failed to get session headers');
|
throw new Error('Failed to get session headers');
|
||||||
}),
|
}),
|
||||||
|
|
@ -55,16 +56,16 @@ export function initializeIBProvider() {
|
||||||
const { fetchExchanges } = await import('./operations/exchanges.operations');
|
const { fetchExchanges } = await import('./operations/exchanges.operations');
|
||||||
const { fetchSymbols } = await import('./operations/symbols.operations');
|
const { fetchSymbols } = await import('./operations/symbols.operations');
|
||||||
|
|
||||||
const sessionHeaders = await fetchSession();
|
const sessionHeaders = await fetchSession(container);
|
||||||
logger.info('Fetched symbol summary from IB');
|
logger.info('Fetched symbol summary from IB');
|
||||||
|
|
||||||
if (sessionHeaders) {
|
if (sessionHeaders) {
|
||||||
logger.debug('Fetching exchanges from IB');
|
logger.debug('Fetching exchanges from IB');
|
||||||
const exchanges = await fetchExchanges(sessionHeaders);
|
const exchanges = await fetchExchanges(sessionHeaders, container);
|
||||||
logger.info('Fetched exchanges from IB', { count: exchanges?.length });
|
logger.info('Fetched exchanges from IB', { count: exchanges?.length });
|
||||||
|
|
||||||
logger.debug('Fetching symbols from IB');
|
logger.debug('Fetching symbols from IB');
|
||||||
const symbols = await fetchSymbols(sessionHeaders);
|
const symbols = await fetchSymbols(sessionHeaders, container);
|
||||||
logger.info('Fetched symbols from IB', { symbols });
|
logger.info('Fetched symbols from IB', { symbols });
|
||||||
|
|
||||||
return { exchangesCount: exchanges?.length, symbolsCount: symbols?.length };
|
return { exchangesCount: exchanges?.length, symbolsCount: symbols?.length };
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
/**
|
/**
|
||||||
* IB Exchanges Operations - Fetching exchange data from IB API
|
* IB Exchanges Operations - Fetching exchange data from IB API
|
||||||
*/
|
*/
|
||||||
import { getMongoDBClient } from '@stock-bot/mongodb-client';
|
|
||||||
import { OperationContext } from '@stock-bot/utils';
|
import { OperationContext } from '@stock-bot/utils';
|
||||||
|
import type { ServiceContainer } from '@stock-bot/connection-factory';
|
||||||
|
|
||||||
import { IB_CONFIG } from '../shared/config';
|
import { IB_CONFIG } from '../shared/config';
|
||||||
|
|
||||||
export async function fetchExchanges(sessionHeaders: Record<string, string>): Promise<unknown[] | null> {
|
export async function fetchExchanges(sessionHeaders: Record<string, string>, container: ServiceContainer): Promise<unknown[] | null> {
|
||||||
const ctx = OperationContext.create('ib', 'exchanges');
|
const ctx = OperationContext.create('ib', 'exchanges', { container });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ctx.logger.info('🔍 Fetching exchanges with session headers...');
|
ctx.logger.info('🔍 Fetching exchanges with session headers...');
|
||||||
|
|
@ -53,8 +53,7 @@ export async function fetchExchanges(sessionHeaders: Record<string, string>): Pr
|
||||||
ctx.logger.info('✅ Exchange data fetched successfully');
|
ctx.logger.info('✅ Exchange data fetched successfully');
|
||||||
|
|
||||||
ctx.logger.info('Saving IB exchanges to MongoDB...');
|
ctx.logger.info('Saving IB exchanges to MongoDB...');
|
||||||
const client = getMongoDBClient();
|
await ctx.mongodb.batchUpsert('ibExchanges', exchanges, ['id', 'country_code']);
|
||||||
await client.batchUpsert('ibExchanges', exchanges, ['id', 'country_code']);
|
|
||||||
ctx.logger.info('✅ Exchange IB data saved to MongoDB:', {
|
ctx.logger.info('✅ Exchange IB data saved to MongoDB:', {
|
||||||
count: exchanges.length,
|
count: exchanges.length,
|
||||||
});
|
});
|
||||||
|
|
@ -63,5 +62,7 @@ export async function fetchExchanges(sessionHeaders: Record<string, string>): Pr
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.logger.error('❌ Failed to fetch exchanges', { error });
|
ctx.logger.error('❌ Failed to fetch exchanges', { error });
|
||||||
return null;
|
return null;
|
||||||
|
} finally {
|
||||||
|
await ctx.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,11 +3,12 @@
|
||||||
*/
|
*/
|
||||||
import { Browser } from '@stock-bot/browser';
|
import { Browser } from '@stock-bot/browser';
|
||||||
import { OperationContext } from '@stock-bot/utils';
|
import { OperationContext } from '@stock-bot/utils';
|
||||||
|
import type { ServiceContainer } from '@stock-bot/connection-factory';
|
||||||
|
|
||||||
import { IB_CONFIG } from '../shared/config';
|
import { IB_CONFIG } from '../shared/config';
|
||||||
|
|
||||||
export async function fetchSession(): Promise<Record<string, string> | undefined> {
|
export async function fetchSession(container: ServiceContainer): Promise<Record<string, string> | undefined> {
|
||||||
const ctx = OperationContext.create('ib', 'session');
|
const ctx = OperationContext.create('ib', 'session', { container });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Browser.initialize({
|
await Browser.initialize({
|
||||||
|
|
@ -84,5 +85,7 @@ export async function fetchSession(): Promise<Record<string, string> | undefined
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.logger.error('Failed to fetch IB symbol summary', { error });
|
ctx.logger.error('Failed to fetch IB symbol summary', { error });
|
||||||
return;
|
return;
|
||||||
|
} finally {
|
||||||
|
await ctx.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
/**
|
/**
|
||||||
* IB Symbols Operations - Fetching symbol data from IB API
|
* IB Symbols Operations - Fetching symbol data from IB API
|
||||||
*/
|
*/
|
||||||
import { getMongoDBClient } from '@stock-bot/mongodb-client';
|
|
||||||
import { OperationContext } from '@stock-bot/utils';
|
import { OperationContext } from '@stock-bot/utils';
|
||||||
|
import type { ServiceContainer } from '@stock-bot/connection-factory';
|
||||||
|
|
||||||
import { IB_CONFIG } from '../shared/config';
|
import { IB_CONFIG } from '../shared/config';
|
||||||
|
|
||||||
// Fetch symbols from IB using the session headers
|
// Fetch symbols from IB using the session headers
|
||||||
export async function fetchSymbols(sessionHeaders: Record<string, string>): Promise<unknown[] | null> {
|
export async function fetchSymbols(sessionHeaders: Record<string, string>, container: ServiceContainer): Promise<unknown[] | null> {
|
||||||
const ctx = OperationContext.create('ib', 'symbols');
|
const ctx = OperationContext.create('ib', 'symbols', { container });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ctx.logger.info('🔍 Fetching symbols with session headers...');
|
ctx.logger.info('🔍 Fetching symbols with session headers...');
|
||||||
|
|
@ -111,8 +111,7 @@ export async function fetchSymbols(sessionHeaders: Record<string, string>): Prom
|
||||||
ctx.logger.info('✅ IB symbols fetched successfully, saving to DB...', {
|
ctx.logger.info('✅ IB symbols fetched successfully, saving to DB...', {
|
||||||
totalSymbols: symbols.length,
|
totalSymbols: symbols.length,
|
||||||
});
|
});
|
||||||
const client = getMongoDBClient();
|
await ctx.mongodb.batchUpsert('ib_symbols', symbols, ['symbol', 'exchangeId']);
|
||||||
await client.batchUpsert('ib_symbols', symbols, ['symbol', 'exchangeId']);
|
|
||||||
ctx.logger.info('Saved IB symbols to DB', {
|
ctx.logger.info('Saved IB symbols to DB', {
|
||||||
totalSymbols: symbols.length,
|
totalSymbols: symbols.length,
|
||||||
});
|
});
|
||||||
|
|
@ -121,5 +120,7 @@ export async function fetchSymbols(sessionHeaders: Record<string, string>): Prom
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.logger.error('❌ Failed to fetch symbols', { error });
|
ctx.logger.error('❌ Failed to fetch symbols', { error });
|
||||||
return null;
|
return null;
|
||||||
|
} finally {
|
||||||
|
await ctx.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,11 +4,12 @@
|
||||||
import { ProxyInfo } from '@stock-bot/http';
|
import { ProxyInfo } from '@stock-bot/http';
|
||||||
import { getLogger } from '@stock-bot/logger';
|
import { getLogger } from '@stock-bot/logger';
|
||||||
import { handlerRegistry, createJobHandler, type HandlerConfigWithSchedule } from '@stock-bot/queue';
|
import { handlerRegistry, createJobHandler, type HandlerConfigWithSchedule } from '@stock-bot/queue';
|
||||||
|
import type { ServiceContainer } from '@stock-bot/connection-factory';
|
||||||
|
|
||||||
const handlerLogger = getLogger('proxy-handler');
|
const handlerLogger = getLogger('proxy-handler');
|
||||||
|
|
||||||
// Initialize and register the Proxy provider
|
// Initialize and register the Proxy provider
|
||||||
export function initializeProxyProvider() {
|
export function initializeProxyProvider(container: ServiceContainer) {
|
||||||
handlerLogger.debug('Registering proxy provider with scheduled jobs...');
|
handlerLogger.debug('Registering proxy provider with scheduled jobs...');
|
||||||
|
|
||||||
const proxyProviderConfig: HandlerConfigWithSchedule = {
|
const proxyProviderConfig: HandlerConfigWithSchedule = {
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { OperationContext } from '@stock-bot/utils';
|
import { OperationContext } from '@stock-bot/utils';
|
||||||
|
import type { ServiceContainer } from '@stock-bot/connection-factory';
|
||||||
|
|
||||||
import { initializeQMResources } from './session.operations';
|
import { initializeQMResources } from './session.operations';
|
||||||
|
|
||||||
export async function fetchExchanges(): Promise<unknown[] | null> {
|
export async function fetchExchanges(container: ServiceContainer): Promise<unknown[] | null> {
|
||||||
const ctx = OperationContext.create('qm', 'exchanges');
|
const ctx = OperationContext.create('qm', 'exchanges', { container });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure resources are initialized
|
// Ensure resources are initialized
|
||||||
|
|
@ -15,7 +16,7 @@ export async function fetchExchanges(): Promise<unknown[] | null> {
|
||||||
const sessionManager = QMSessionManager.getInstance();
|
const sessionManager = QMSessionManager.getInstance();
|
||||||
|
|
||||||
if (!sessionManager.getInitialized()) {
|
if (!sessionManager.getInitialized()) {
|
||||||
await initializeQMResources();
|
await initializeQMResources(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.logger.info('QM exchanges fetch - not implemented yet');
|
ctx.logger.info('QM exchanges fetch - not implemented yet');
|
||||||
|
|
@ -37,5 +38,7 @@ export async function fetchExchanges(): Promise<unknown[] | null> {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.logger.error('Failed to fetch QM exchanges', { error });
|
ctx.logger.error('Failed to fetch QM exchanges', { error });
|
||||||
return null;
|
return null;
|
||||||
|
} finally {
|
||||||
|
await ctx.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,13 +5,14 @@
|
||||||
import { OperationContext } from '@stock-bot/utils';
|
import { OperationContext } from '@stock-bot/utils';
|
||||||
import { isShutdownSignalReceived } from '@stock-bot/shutdown';
|
import { isShutdownSignalReceived } from '@stock-bot/shutdown';
|
||||||
import { getRandomProxy } from '@stock-bot/utils';
|
import { getRandomProxy } from '@stock-bot/utils';
|
||||||
|
import type { ServiceContainer } from '@stock-bot/connection-factory';
|
||||||
|
|
||||||
import { QMSessionManager } from '../shared/session-manager';
|
import { QMSessionManager } from '../shared/session-manager';
|
||||||
import { QM_SESSION_IDS, QM_CONFIG, SESSION_CONFIG, getQmHeaders } from '../shared/config';
|
import { QM_SESSION_IDS, QM_CONFIG, SESSION_CONFIG, getQmHeaders } from '../shared/config';
|
||||||
import type { QMSession } from '../shared/types';
|
import type { QMSession } from '../shared/types';
|
||||||
|
|
||||||
export async function createSessions(): Promise<void> {
|
export async function createSessions(container: ServiceContainer): Promise<void> {
|
||||||
const ctx = OperationContext.create('qm', 'session');
|
const ctx = OperationContext.create('qm', 'session', { container });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ctx.logger.info('Creating QM sessions...');
|
ctx.logger.info('Creating QM sessions...');
|
||||||
|
|
@ -21,7 +22,7 @@ export async function createSessions(): Promise<void> {
|
||||||
|
|
||||||
// Check if already initialized
|
// Check if already initialized
|
||||||
if (!sessionManager.getInitialized()) {
|
if (!sessionManager.getInitialized()) {
|
||||||
await initializeQMResources();
|
await initializeQMResources(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up failed sessions first
|
// Clean up failed sessions first
|
||||||
|
|
@ -67,6 +68,8 @@ export async function createSessions(): Promise<void> {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.logger.error('Failed to create QM sessions', { error });
|
ctx.logger.error('Failed to create QM sessions', { error });
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await ctx.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,8 +164,8 @@ async function createSingleSession(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initializeQMResources(): Promise<void> {
|
export async function initializeQMResources(container?: ServiceContainer): Promise<void> {
|
||||||
const ctx = OperationContext.create('qm', 'init');
|
const ctx = OperationContext.create('qm', 'init', container ? { container } : undefined);
|
||||||
|
|
||||||
// Check if already initialized
|
// Check if already initialized
|
||||||
const alreadyInitialized = await ctx.cache.get('initialized');
|
const alreadyInitialized = await ctx.cache.get('initialized');
|
||||||
|
|
@ -181,4 +184,6 @@ export async function initializeQMResources(): Promise<void> {
|
||||||
sessionManager.setInitialized(true);
|
sessionManager.setInitialized(true);
|
||||||
|
|
||||||
ctx.logger.info('QM resources initialized successfully');
|
ctx.logger.info('QM resources initialized successfully');
|
||||||
|
|
||||||
|
await ctx.dispose();
|
||||||
}
|
}
|
||||||
|
|
@ -7,14 +7,16 @@ import { QueueManager } from '@stock-bot/queue';
|
||||||
|
|
||||||
import { QMSessionManager } from '../shared/session-manager';
|
import { QMSessionManager } from '../shared/session-manager';
|
||||||
import { QM_SESSION_IDS } from '../shared/config';
|
import { QM_SESSION_IDS } from '../shared/config';
|
||||||
|
import type { ServiceContainer } from '@stock-bot/connection-factory';
|
||||||
import type { SymbolSpiderJob, SpiderResult } from '../shared/types';
|
import type { SymbolSpiderJob, SpiderResult } from '../shared/types';
|
||||||
import { initializeQMResources } from './session.operations';
|
import { initializeQMResources } from './session.operations';
|
||||||
import { searchQMSymbolsAPI } from './symbols.operations';
|
import { searchQMSymbolsAPI } from './symbols.operations';
|
||||||
|
|
||||||
export async function spiderSymbolSearch(
|
export async function spiderSymbolSearch(
|
||||||
payload: SymbolSpiderJob
|
payload: SymbolSpiderJob,
|
||||||
|
container: ServiceContainer
|
||||||
): Promise<SpiderResult> {
|
): Promise<SpiderResult> {
|
||||||
const ctx = OperationContext.create('qm', 'spider');
|
const ctx = OperationContext.create('qm', 'spider', { container });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { prefix, depth, source = 'qm', maxDepth = 4 } = payload;
|
const { prefix, depth, source = 'qm', maxDepth = 4 } = payload;
|
||||||
|
|
@ -37,7 +39,7 @@ export async function spiderSymbolSearch(
|
||||||
// Ensure resources are initialized
|
// Ensure resources are initialized
|
||||||
const sessionManager = QMSessionManager.getInstance();
|
const sessionManager = QMSessionManager.getInstance();
|
||||||
if (!sessionManager.getInitialized()) {
|
if (!sessionManager.getInitialized()) {
|
||||||
await initializeQMResources();
|
await initializeQMResources(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: SpiderResult;
|
let result: SpiderResult;
|
||||||
|
|
@ -47,7 +49,7 @@ export async function spiderSymbolSearch(
|
||||||
result = await createAlphabetJobs(source, maxDepth, ctx);
|
result = await createAlphabetJobs(source, maxDepth, ctx);
|
||||||
} else {
|
} else {
|
||||||
// Leaf job: Search for symbols with this prefix
|
// Leaf job: Search for symbols with this prefix
|
||||||
result = await searchAndSpawnJobs(prefix, depth, source, maxDepth, ctx);
|
result = await searchAndSpawnJobs(prefix, depth, source, maxDepth, ctx, container);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
|
|
@ -88,6 +90,8 @@ export async function spiderSymbolSearch(
|
||||||
await ctx.cache.set(cacheKey, failedResult, { ttl: 300 });
|
await ctx.cache.set(cacheKey, failedResult, { ttl: 300 });
|
||||||
|
|
||||||
return failedResult;
|
return failedResult;
|
||||||
|
} finally {
|
||||||
|
await ctx.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,7 +158,8 @@ async function searchAndSpawnJobs(
|
||||||
depth: number,
|
depth: number,
|
||||||
source: string,
|
source: string,
|
||||||
maxDepth: number,
|
maxDepth: number,
|
||||||
ctx: OperationContext
|
ctx: OperationContext,
|
||||||
|
container: ServiceContainer
|
||||||
): Promise<SpiderResult> {
|
): Promise<SpiderResult> {
|
||||||
try {
|
try {
|
||||||
// Ensure sessions exist for symbol search
|
// Ensure sessions exist for symbol search
|
||||||
|
|
@ -164,14 +169,14 @@ async function searchAndSpawnJobs(
|
||||||
if (!lookupSession) {
|
if (!lookupSession) {
|
||||||
ctx.logger.info('No lookup sessions available, creating sessions first...');
|
ctx.logger.info('No lookup sessions available, creating sessions first...');
|
||||||
const { createSessions } = await import('./session.operations');
|
const { createSessions } = await import('./session.operations');
|
||||||
await createSessions();
|
await createSessions(container);
|
||||||
|
|
||||||
// Wait a bit for session creation
|
// Wait a bit for session creation
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search for symbols with this prefix
|
// Search for symbols with this prefix
|
||||||
const symbols = await searchQMSymbolsAPI(prefix);
|
const symbols = await searchQMSymbolsAPI(prefix, container);
|
||||||
const symbolCount = symbols.length;
|
const symbolCount = symbols.length;
|
||||||
|
|
||||||
ctx.logger.info(`Prefix "${prefix}" returned ${symbolCount} symbols`);
|
ctx.logger.info(`Prefix "${prefix}" returned ${symbolCount} symbols`);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
import { OperationContext } from '@stock-bot/utils';
|
import { OperationContext } from '@stock-bot/utils';
|
||||||
import { getRandomProxy } from '@stock-bot/utils';
|
import { getRandomProxy } from '@stock-bot/utils';
|
||||||
|
import type { ServiceContainer } from '@stock-bot/connection-factory';
|
||||||
|
|
||||||
import { QMSessionManager } from '../shared/session-manager';
|
import { QMSessionManager } from '../shared/session-manager';
|
||||||
import { QM_SESSION_IDS, QM_CONFIG, SESSION_CONFIG } from '../shared/config';
|
import { QM_SESSION_IDS, QM_CONFIG, SESSION_CONFIG } from '../shared/config';
|
||||||
|
|
@ -11,13 +12,13 @@ import type { SymbolSpiderJob, Exchange } from '../shared/types';
|
||||||
import { initializeQMResources } from './session.operations';
|
import { initializeQMResources } from './session.operations';
|
||||||
import { spiderSymbolSearch } from './spider.operations';
|
import { spiderSymbolSearch } from './spider.operations';
|
||||||
|
|
||||||
export async function fetchSymbols(): Promise<unknown[] | null> {
|
export async function fetchSymbols(container: ServiceContainer): Promise<unknown[] | null> {
|
||||||
const ctx = OperationContext.create('qm', 'symbols');
|
const ctx = OperationContext.create('qm', 'symbols', { container });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sessionManager = QMSessionManager.getInstance();
|
const sessionManager = QMSessionManager.getInstance();
|
||||||
if (!sessionManager.getInitialized()) {
|
if (!sessionManager.getInitialized()) {
|
||||||
await initializeQMResources();
|
await initializeQMResources(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.logger.info('Starting QM spider-based symbol search...');
|
ctx.logger.info('Starting QM spider-based symbol search...');
|
||||||
|
|
@ -57,11 +58,13 @@ export async function fetchSymbols(): Promise<unknown[] | null> {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.logger.error('Failed to start QM spider symbol search', { error });
|
ctx.logger.error('Failed to start QM spider symbol search', { error });
|
||||||
return null;
|
return null;
|
||||||
|
} finally {
|
||||||
|
await ctx.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchQMSymbolsAPI(query: string): Promise<any[]> {
|
export async function searchQMSymbolsAPI(query: string, container: ServiceContainer): Promise<any[]> {
|
||||||
const ctx = OperationContext.create('qm', 'api-search');
|
const ctx = OperationContext.create('qm', 'api-search', { container });
|
||||||
|
|
||||||
const proxyInfo = await getRandomProxy();
|
const proxyInfo = await getRandomProxy();
|
||||||
if (!proxyInfo) {
|
if (!proxyInfo) {
|
||||||
|
|
@ -191,5 +194,7 @@ export async function searchQMSymbolsAPI(query: string): Promise<any[]> {
|
||||||
});
|
});
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await ctx.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,12 +4,13 @@ import {
|
||||||
handlerRegistry,
|
handlerRegistry,
|
||||||
type HandlerConfigWithSchedule
|
type HandlerConfigWithSchedule
|
||||||
} from '@stock-bot/queue';
|
} from '@stock-bot/queue';
|
||||||
|
import type { ServiceContainer } from '@stock-bot/connection-factory';
|
||||||
import type { SymbolSpiderJob } from './shared/types';
|
import type { SymbolSpiderJob } from './shared/types';
|
||||||
|
|
||||||
const handlerLogger = getLogger('qm-handler');
|
const handlerLogger = getLogger('qm-handler');
|
||||||
|
|
||||||
// Initialize and register the QM provider
|
// Initialize and register the QM provider
|
||||||
export function initializeQMProvider() {
|
export function initializeQMProvider(container: ServiceContainer) {
|
||||||
handlerLogger.debug('Registering QM provider with scheduled jobs...');
|
handlerLogger.debug('Registering QM provider with scheduled jobs...');
|
||||||
|
|
||||||
const qmProviderConfig: HandlerConfigWithSchedule = {
|
const qmProviderConfig: HandlerConfigWithSchedule = {
|
||||||
|
|
@ -17,12 +18,12 @@ export function initializeQMProvider() {
|
||||||
operations: {
|
operations: {
|
||||||
'create-sessions': createJobHandler(async () => {
|
'create-sessions': createJobHandler(async () => {
|
||||||
const { createSessions } = await import('./operations/session.operations');
|
const { createSessions } = await import('./operations/session.operations');
|
||||||
await createSessions();
|
await createSessions(container);
|
||||||
return { success: true, message: 'QM sessions created successfully' };
|
return { success: true, message: 'QM sessions created successfully' };
|
||||||
}),
|
}),
|
||||||
'search-symbols': createJobHandler(async () => {
|
'search-symbols': createJobHandler(async () => {
|
||||||
const { fetchSymbols } = await import('./operations/symbols.operations');
|
const { fetchSymbols } = await import('./operations/symbols.operations');
|
||||||
const symbols = await fetchSymbols();
|
const symbols = await fetchSymbols(container);
|
||||||
|
|
||||||
if (symbols && symbols.length > 0) {
|
if (symbols && symbols.length > 0) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -41,9 +42,7 @@ export function initializeQMProvider() {
|
||||||
}),
|
}),
|
||||||
'spider-symbol-search': createJobHandler(async (payload: SymbolSpiderJob) => {
|
'spider-symbol-search': createJobHandler(async (payload: SymbolSpiderJob) => {
|
||||||
const { spiderSymbolSearch } = await import('./operations/spider.operations');
|
const { spiderSymbolSearch } = await import('./operations/spider.operations');
|
||||||
const result = await spiderSymbolSearch(payload);
|
return await spiderSymbolSearch(payload, container);
|
||||||
|
|
||||||
return result;
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,420 +0,0 @@
|
||||||
import { getRandomUserAgent } from '@stock-bot/http';
|
|
||||||
import { getLogger } from '@stock-bot/logger';
|
|
||||||
import { getMongoDBClient } from '@stock-bot/mongodb-client';
|
|
||||||
import { QueueManager } from '@stock-bot/queue';
|
|
||||||
import { isShutdownSignalReceived } from '@stock-bot/shutdown';
|
|
||||||
import { getRandomProxy } from '@stock-bot/utils';
|
|
||||||
|
|
||||||
// Shared instances (module-scoped, not global)
|
|
||||||
let isInitialized = false; // Track if resources are initialized
|
|
||||||
let logger: ReturnType<typeof getLogger>;
|
|
||||||
// let cache: CacheProvider;
|
|
||||||
|
|
||||||
export interface QMSession {
|
|
||||||
proxy: string;
|
|
||||||
headers: Record<string, string>;
|
|
||||||
successfulCalls: number;
|
|
||||||
failedCalls: number;
|
|
||||||
lastUsed: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SymbolSpiderJob {
|
|
||||||
prefix: string | null; // null = root job (A-Z)
|
|
||||||
depth: number; // 1=A, 2=AA, 3=AAA, etc.
|
|
||||||
source: string; // 'qm'
|
|
||||||
maxDepth?: number; // optional max depth limit
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Exchange {
|
|
||||||
exchange: string;
|
|
||||||
exchangeCode: string;
|
|
||||||
exchangeShortName: string;
|
|
||||||
countryCode: string;
|
|
||||||
source: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getQmHeaders(): Record<string, string> {
|
|
||||||
return {
|
|
||||||
'User-Agent': getRandomUserAgent(),
|
|
||||||
Accept: '*/*',
|
|
||||||
'Accept-Language': 'en',
|
|
||||||
'Sec-Fetch-Mode': 'cors',
|
|
||||||
Origin: 'https://www.quotemedia.com',
|
|
||||||
Referer: 'https://www.quotemedia.com/',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionCache: Record<string, QMSession[]> = {
|
|
||||||
// '5ad521e05faf5778d567f6d0012ec34d6cdbaeb2462f41568f66558bc7b4ced9': [], //4488d072b
|
|
||||||
// cc1cbdaf040f76db8f4c94f7d156b9b9b716e1a7509ec9c74a48a47f6b6b9f87: [], //97ff00cf3 // getQuotes
|
|
||||||
// '74963ff42f1db2320d051762b5d3950ff9eab23f9d5c5b592551b4ca0441d086': [], //32ca24e394b // getSplitsBySymbol getBrokerRatingsBySymbol getDividendsBySymbol getEarningsSurprisesBySymbol getEarningsEventsBySymbol
|
|
||||||
// '1e1d7cb1de1fd2fe52684abdea41a446919a5fe12776dfab88615ac1ce1ec2f6': [], //fb5721812d2c // getEnhancedQuotes getProfiles
|
|
||||||
// a900a06cc6b3e8036afb9eeb1bbf9783f0007698ed8f5cb1e373dc790e7be2e5: [], //cc882cd95f9 // getEnhancedQuotes
|
|
||||||
// a863d519e38f80e45d10e280fb1afc729816e23f0218db2f3e8b23005a9ad8dd: [], //05a09a41225 // getCompanyFilings getEnhancedQuotes
|
|
||||||
// b3cdb1873f3682c5aeeac097be6181529bfb755945e5a412a24f4b9316291427: [], //6a63f56a6 // getHeadlinesTickerStory
|
|
||||||
dc8c9930437f65d30f6597768800957017bac203a0a50342932757c8dfa158d6: [], //fceb3c4bdd // lookup
|
|
||||||
// '97b24911d7b034620aafad9441afdb2bc906ee5c992d86933c5903254ca29709': [], //c56424868d // detailed-quotes
|
|
||||||
// '8a394f09cb8540c8be8988780660a7ae5b583c331a1f6cb12834f051a0169a8f': [], //2a86d214e50e5 // getGlobalIndustrySectorPeers getKeyRatiosBySymbol getGlobalIndustrySectorCodeList
|
|
||||||
// '2f059f75e2a839437095c9e7e4991d2365bafa7bbb086672a87ae0cf8d92eb01': [], // 48fa36d // getNethouseBySymbol
|
|
||||||
// d7ae7e0091dd1d7011948c3dc4af09b5ec552285d92bb188be2618968bc78e3f: [], // 63548ee //getRecentTradesBySymbol getQuotes getLevel2Quote getRecentTradesBySymbol
|
|
||||||
// d22d1db8f67fe6e420b4028e5129b289ca64862aa6cee8459193747b68c01de3: [], // 84e9e
|
|
||||||
// '6e0b22a7cbc02ac3fa07d45e2880b7696aaebeb29574dce81789e570570c9002': [], //
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function initializeQMResources(): Promise<void> {
|
|
||||||
// Skip if already initialized
|
|
||||||
if (isInitialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger = getLogger('qm-tasks');
|
|
||||||
isInitialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createSessions(): Promise<void> {
|
|
||||||
try {
|
|
||||||
//for each session, check array length, if less than 5, create new session
|
|
||||||
if (!isInitialized) {
|
|
||||||
await initializeQMResources();
|
|
||||||
}
|
|
||||||
logger.info('Creating QM sessions...');
|
|
||||||
for (const [sessionId, sessionArray] of Object.entries(sessionCache)) {
|
|
||||||
const initialCount = sessionArray.length;
|
|
||||||
const filteredArray = sessionArray.filter(session => session.failedCalls <= 10);
|
|
||||||
sessionCache[sessionId] = filteredArray;
|
|
||||||
|
|
||||||
const removedCount = initialCount - filteredArray.length;
|
|
||||||
if (removedCount > 0) {
|
|
||||||
logger.info(
|
|
||||||
`Removed ${removedCount} sessions with excessive failures for ${sessionId}. Remaining: ${filteredArray.length}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (sessionCache[sessionId].length < 10) {
|
|
||||||
if(isShutdownSignalReceived()) {
|
|
||||||
logger.info('Shutting down, skipping session creation');
|
|
||||||
break; // Exit if shutting down
|
|
||||||
}
|
|
||||||
logger.info(`Creating new session for ${sessionId}`);
|
|
||||||
const proxyInfo = await getRandomProxy();
|
|
||||||
if (!proxyInfo) {
|
|
||||||
logger.error('No proxy available for QM session creation');
|
|
||||||
break; // Skip session creation if no proxy is available
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert ProxyInfo to string format
|
|
||||||
const auth = proxyInfo.username && proxyInfo.password ? `${proxyInfo.username}:${proxyInfo.password}@` : '';
|
|
||||||
const proxy = `${proxyInfo.protocol}://${auth}${proxyInfo.host}:${proxyInfo.port}`;
|
|
||||||
const newSession: QMSession = {
|
|
||||||
proxy: proxy, // Placeholder, should be set to a valid proxy
|
|
||||||
headers: getQmHeaders(),
|
|
||||||
successfulCalls: 0,
|
|
||||||
failedCalls: 0,
|
|
||||||
lastUsed: new Date(),
|
|
||||||
};
|
|
||||||
const sessionResponse = await fetch(
|
|
||||||
`https://app.quotemedia.com/auth/g/authenticate/dataTool/v0/500/${sessionId}`,
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
proxy: newSession.proxy,
|
|
||||||
headers: newSession.headers,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.debug('Session response received', {
|
|
||||||
status: sessionResponse.status,
|
|
||||||
sessionId,
|
|
||||||
});
|
|
||||||
if (!sessionResponse.ok) {
|
|
||||||
logger.error('Failed to create QM session', {
|
|
||||||
sessionId,
|
|
||||||
status: sessionResponse.status,
|
|
||||||
statusText: sessionResponse.statusText,
|
|
||||||
});
|
|
||||||
continue; // Skip this session if creation failed
|
|
||||||
}
|
|
||||||
const sessionData = await sessionResponse.json();
|
|
||||||
logger.info('QM session created successfully', {
|
|
||||||
sessionId,
|
|
||||||
sessionData,
|
|
||||||
proxy: newSession.proxy,
|
|
||||||
sessionCount: sessionCache[sessionId].length + 1,
|
|
||||||
});
|
|
||||||
newSession.headers['Datatool-Token'] = sessionData.token;
|
|
||||||
sessionCache[sessionId].push(newSession);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('❌ Failed to fetch QM session', { error });
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spider-based symbol search functions
|
|
||||||
export async function spiderSymbolSearch(
|
|
||||||
payload: SymbolSpiderJob
|
|
||||||
): Promise<{ success: boolean; symbolsFound: number; jobsCreated: number }> {
|
|
||||||
try {
|
|
||||||
if (!isInitialized) {
|
|
||||||
await initializeQMResources();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { prefix, depth, source = 'qm', maxDepth = 4 } = payload;
|
|
||||||
|
|
||||||
logger.info(`Starting spider search`, { prefix: prefix || 'ROOT', depth, source });
|
|
||||||
|
|
||||||
// Root job: Create A-Z jobs
|
|
||||||
if (prefix === null || prefix === undefined || prefix === '') {
|
|
||||||
return await createAlphabetJobs(source, maxDepth);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Leaf job: Search for symbols with this prefix
|
|
||||||
return await searchAndSpawnJobs(prefix, depth, source, maxDepth);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Spider symbol search failed', { error, payload });
|
|
||||||
return { success: false, symbolsFound: 0, jobsCreated: 0 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createAlphabetJobs(
|
|
||||||
source: string,
|
|
||||||
maxDepth: number
|
|
||||||
): Promise<{ success: boolean; symbolsFound: number; jobsCreated: number }> {
|
|
||||||
try {
|
|
||||||
const queueManager = QueueManager.getInstance();
|
|
||||||
const queue = queueManager.getQueue('qm');
|
|
||||||
let jobsCreated = 0;
|
|
||||||
|
|
||||||
// Create jobs for A-Z
|
|
||||||
for (let i = 0; i < 26; i++) {
|
|
||||||
const letter = String.fromCharCode(65 + i); // A=65, B=66, etc.
|
|
||||||
|
|
||||||
const job: SymbolSpiderJob = {
|
|
||||||
prefix: letter,
|
|
||||||
depth: 1,
|
|
||||||
source,
|
|
||||||
maxDepth,
|
|
||||||
};
|
|
||||||
|
|
||||||
await queue.add(
|
|
||||||
'spider-symbol-search',
|
|
||||||
{
|
|
||||||
handler: 'qm',
|
|
||||||
operation: 'spider-symbol-search',
|
|
||||||
payload: job,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
priority: 5,
|
|
||||||
delay: i * 100, // Stagger jobs by 100ms
|
|
||||||
attempts: 3,
|
|
||||||
backoff: { type: 'exponential', delay: 2000 },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
jobsCreated++;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Created ${jobsCreated} alphabet jobs (A-Z)`);
|
|
||||||
return { success: true, symbolsFound: 0, jobsCreated };
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to create alphabet jobs', { error });
|
|
||||||
return { success: false, symbolsFound: 0, jobsCreated: 0 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function searchAndSpawnJobs(
|
|
||||||
prefix: string,
|
|
||||||
depth: number,
|
|
||||||
source: string,
|
|
||||||
maxDepth: number
|
|
||||||
): Promise<{ success: boolean; symbolsFound: number; jobsCreated: number }> {
|
|
||||||
try {
|
|
||||||
// Ensure sessions exist
|
|
||||||
const sessionId = 'dc8c9930437f65d30f6597768800957017bac203a0a50342932757c8dfa158d6';
|
|
||||||
const currentSessions = sessionCache[sessionId] || [];
|
|
||||||
|
|
||||||
if (currentSessions.length === 0) {
|
|
||||||
logger.info('No sessions found, creating sessions first...');
|
|
||||||
await createSessions();
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search for symbols with this prefix
|
|
||||||
const symbols = await searchQMSymbolsAPI(prefix);
|
|
||||||
const symbolCount = symbols.length;
|
|
||||||
|
|
||||||
logger.info(`Prefix "${prefix}" returned ${symbolCount} symbols`);
|
|
||||||
|
|
||||||
let jobsCreated = 0;
|
|
||||||
|
|
||||||
// If we have 50+ symbols and haven't reached max depth, spawn sub-jobs
|
|
||||||
if (symbolCount >= 50 && depth < maxDepth) {
|
|
||||||
const queueManager = QueueManager.getInstance();
|
|
||||||
const queue = queueManager.getQueue('qm');
|
|
||||||
|
|
||||||
logger.info(`Spawning sub-jobs for prefix "${prefix}" (${symbolCount} >= 50 symbols)`);
|
|
||||||
|
|
||||||
// Create jobs for prefixA, prefixB, prefixC... prefixZ
|
|
||||||
for (let i = 0; i < 26; i++) {
|
|
||||||
const letter = String.fromCharCode(65 + i);
|
|
||||||
const newPrefix = prefix + letter;
|
|
||||||
|
|
||||||
const job: SymbolSpiderJob = {
|
|
||||||
prefix: newPrefix,
|
|
||||||
depth: depth + 1,
|
|
||||||
source,
|
|
||||||
maxDepth,
|
|
||||||
};
|
|
||||||
|
|
||||||
await queue.add(
|
|
||||||
'spider-symbol-search',
|
|
||||||
{
|
|
||||||
handler: 'qm',
|
|
||||||
operation: 'spider-symbol-search',
|
|
||||||
payload: job,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
priority: Math.max(1, 6 - depth), // Higher priority for deeper jobs
|
|
||||||
delay: i * 50, // Stagger sub-jobs by 50ms
|
|
||||||
attempts: 3,
|
|
||||||
backoff: { type: 'exponential', delay: 2000 },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
jobsCreated++;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Created ${jobsCreated} sub-jobs for prefix "${prefix}"`);
|
|
||||||
} else {
|
|
||||||
// Terminal case: save symbols and exchanges (already done in searchQMSymbolsAPI)
|
|
||||||
logger.info(`Terminal case for prefix "${prefix}": ${symbolCount} symbols saved`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, symbolsFound: symbolCount, jobsCreated };
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Failed to search and spawn jobs for prefix "${prefix}"`, { error, depth });
|
|
||||||
return { success: false, symbolsFound: 0, jobsCreated: 0 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// API call function to search symbols via QM
|
|
||||||
async function searchQMSymbolsAPI(query: string): Promise<string[]> {
|
|
||||||
const proxyInfo = await getRandomProxy();
|
|
||||||
|
|
||||||
if (!proxyInfo) {
|
|
||||||
throw new Error('No proxy available for QM API call');
|
|
||||||
}
|
|
||||||
const sessionId = 'dc8c9930437f65d30f6597768800957017bac203a0a50342932757c8dfa158d6'; // Use the session ID for symbol lookup
|
|
||||||
const session =
|
|
||||||
sessionCache[sessionId][Math.floor(Math.random() * sessionCache[sessionId].length)]; // lookup session
|
|
||||||
if (!session) {
|
|
||||||
throw new Error(`No active session found for QM API with ID: ${sessionId}`);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// QM lookup endpoint for symbol search
|
|
||||||
const apiUrl = `https://app.quotemedia.com/datatool/lookup.json?marketType=equity&pathName=%2Fdemo%2Fportal%2Fcompany-summary.php&q=${encodeURIComponent(query)}&qmodTool=SmartSymbolLookup&searchType=symbol&showFree=false&showHisa=false&webmasterId=500`;
|
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: session.headers,
|
|
||||||
proxy: session.proxy,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`QM API request failed: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const symbols = await response.json();
|
|
||||||
const mongoClient = getMongoDBClient();
|
|
||||||
const updatedSymbols = symbols.map((symbol: Record<string, unknown>) => {
|
|
||||||
return {
|
|
||||||
...symbol,
|
|
||||||
qmSearchCode: symbol.symbol, // Store original symbol for reference
|
|
||||||
symbol: symbol.symbol.split(':')[0], // Extract symbol from "symbol:exchange"
|
|
||||||
};
|
|
||||||
});
|
|
||||||
await mongoClient.batchUpsert('qmSymbols', updatedSymbols, ['qmSearchCode']);
|
|
||||||
const exchanges: Exchange[] = [];
|
|
||||||
for (const symbol of symbols) {
|
|
||||||
if (!exchanges.some(ex => ex.exchange === symbol.exchange)) {
|
|
||||||
exchanges.push({
|
|
||||||
exchange: symbol.exchange,
|
|
||||||
exchangeCode: symbol.exchangeCode,
|
|
||||||
exchangeShortName: symbol.exchangeShortName,
|
|
||||||
countryCode: symbol.countryCode,
|
|
||||||
source: 'qm',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await mongoClient.batchUpsert('qmExchanges', exchanges, ['exchange']);
|
|
||||||
session.successfulCalls++;
|
|
||||||
session.lastUsed = new Date();
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`QM API returned ${symbols.length} symbols for query: ${query} with proxy ${session.proxy}`
|
|
||||||
);
|
|
||||||
return symbols;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error searching QM symbols for query "${query}":`, error);
|
|
||||||
if (session) {
|
|
||||||
session.failedCalls++;
|
|
||||||
session.lastUsed = new Date();
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchSymbols(): Promise<unknown[] | null> {
|
|
||||||
try {
|
|
||||||
if (!isInitialized) {
|
|
||||||
await initializeQMResources();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('🔄 Starting QM spider-based symbol search...');
|
|
||||||
|
|
||||||
// Start the spider process with root job
|
|
||||||
const rootJob: SymbolSpiderJob = {
|
|
||||||
prefix: null, // Root job creates A-Z jobs
|
|
||||||
depth: 0,
|
|
||||||
source: 'qm',
|
|
||||||
maxDepth: 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await spiderSymbolSearch(rootJob);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
logger.info(
|
|
||||||
`QM spider search initiated successfully. Created ${result.jobsCreated} initial jobs`
|
|
||||||
);
|
|
||||||
return [`Spider search initiated with ${result.jobsCreated} jobs`];
|
|
||||||
} else {
|
|
||||||
logger.error('Failed to initiate QM spider search');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('❌ Failed to start QM spider symbol search', { error });
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchExchanges(): Promise<unknown[] | null> {
|
|
||||||
try {
|
|
||||||
if (!isInitialized) {
|
|
||||||
await initializeQMResources();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('🔄 QM exchanges fetch - not implemented yet');
|
|
||||||
// TODO: Implement QM exchanges fetching logic
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('❌ Failed to fetch QM exchanges', { error });
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const qmTasks = {
|
|
||||||
createSessions,
|
|
||||||
fetchSymbols,
|
|
||||||
fetchExchanges,
|
|
||||||
spiderSymbolSearch,
|
|
||||||
};
|
|
||||||
|
|
@ -8,11 +8,12 @@ import {
|
||||||
type HandlerConfigWithSchedule,
|
type HandlerConfigWithSchedule,
|
||||||
} from '@stock-bot/queue';
|
} from '@stock-bot/queue';
|
||||||
import { updateProxies } from '@stock-bot/utils';
|
import { updateProxies } from '@stock-bot/utils';
|
||||||
|
import type { ServiceContainer } from '@stock-bot/connection-factory';
|
||||||
|
|
||||||
const logger = getLogger('webshare-provider');
|
const logger = getLogger('webshare-provider');
|
||||||
|
|
||||||
// Initialize and register the WebShare provider
|
// Initialize and register the WebShare provider
|
||||||
export function initializeWebShareProvider() {
|
export function initializeWebShareProvider(container: ServiceContainer) {
|
||||||
logger.debug('Registering WebShare provider with scheduled jobs...');
|
logger.debug('Registering WebShare provider with scheduled jobs...');
|
||||||
|
|
||||||
const webShareProviderConfig: HandlerConfigWithSchedule = {
|
const webShareProviderConfig: HandlerConfigWithSchedule = {
|
||||||
|
|
@ -76,6 +77,6 @@ export function initializeWebShareProvider() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const webShareProvider = {
|
export const webShareProvider = {
|
||||||
initialize: initializeWebShareProvider,
|
initialize: (container: ServiceContainer) => initializeWebShareProvider(container),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ import { Hono } from 'hono';
|
||||||
import { cors } from 'hono/cors';
|
import { cors } from 'hono/cors';
|
||||||
// Library imports
|
// Library imports
|
||||||
import { getLogger, setLoggerConfig, shutdownLoggers } from '@stock-bot/logger';
|
import { getLogger, setLoggerConfig, shutdownLoggers } from '@stock-bot/logger';
|
||||||
import { connectMongoDB } from '@stock-bot/mongodb-client';
|
|
||||||
import { connectPostgreSQL } from '@stock-bot/postgres-client';
|
|
||||||
import { QueueManager, type QueueManagerConfig } from '@stock-bot/queue';
|
import { QueueManager, type QueueManagerConfig } from '@stock-bot/queue';
|
||||||
import { Shutdown } from '@stock-bot/shutdown';
|
import { Shutdown } from '@stock-bot/shutdown';
|
||||||
import { ProxyManager } from '@stock-bot/utils';
|
import { ProxyManager } from '@stock-bot/utils';
|
||||||
|
import type { ServiceContainer } from '@stock-bot/connection-factory';
|
||||||
// Local imports
|
// Local imports
|
||||||
import { exchangeRoutes, healthRoutes, queueRoutes } from './routes';
|
import { setupServiceContainer } from './setup/database-setup';
|
||||||
|
import { createRoutes } from './routes/create-routes';
|
||||||
|
|
||||||
const config = initializeServiceConfig();
|
const config = initializeServiceConfig();
|
||||||
console.log('Data Service Configuration:', JSON.stringify(config, null, 2));
|
console.log('Data Service Configuration:', JSON.stringify(config, null, 2));
|
||||||
|
|
@ -31,7 +31,27 @@ if (config.log) {
|
||||||
// Create logger AFTER config is set
|
// Create logger AFTER config is set
|
||||||
const logger = getLogger('data-ingestion');
|
const logger = getLogger('data-ingestion');
|
||||||
|
|
||||||
const app = new Hono();
|
const PORT = serviceConfig.port;
|
||||||
|
let server: ReturnType<typeof Bun.serve> | null = null;
|
||||||
|
let serviceContainer: ServiceContainer | null = null;
|
||||||
|
let queueManager: QueueManager | null = null;
|
||||||
|
let app: Hono | null = null;
|
||||||
|
|
||||||
|
// Initialize shutdown manager
|
||||||
|
const shutdown = Shutdown.getInstance({ timeout: 15000 });
|
||||||
|
|
||||||
|
// Initialize services
|
||||||
|
async function initializeServices() {
|
||||||
|
logger.info('Initializing data-ingestion service...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initialize service container with connection pools
|
||||||
|
logger.debug('Setting up service container with connection pools...');
|
||||||
|
serviceContainer = await setupServiceContainer();
|
||||||
|
logger.info('Service container initialized with connection pools');
|
||||||
|
|
||||||
|
// Create app with routes that have access to the container
|
||||||
|
app = new Hono();
|
||||||
|
|
||||||
// Add CORS middleware
|
// Add CORS middleware
|
||||||
app.use(
|
app.use(
|
||||||
|
|
@ -43,56 +63,10 @@ app.use(
|
||||||
credentials: false,
|
credentials: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const PORT = serviceConfig.port;
|
|
||||||
let server: ReturnType<typeof Bun.serve> | null = null;
|
|
||||||
// Singleton clients are managed in libraries
|
|
||||||
let queueManager: QueueManager | null = null;
|
|
||||||
|
|
||||||
// Initialize shutdown manager
|
// Create and mount routes with container
|
||||||
const shutdown = Shutdown.getInstance({ timeout: 15000 });
|
const routes = createRoutes(serviceContainer);
|
||||||
|
app.route('/', routes);
|
||||||
// Mount routes
|
|
||||||
app.route('/health', healthRoutes);
|
|
||||||
app.route('/api/exchanges', exchangeRoutes);
|
|
||||||
app.route('/api/queue', queueRoutes);
|
|
||||||
|
|
||||||
// Initialize services
|
|
||||||
async function initializeServices() {
|
|
||||||
logger.info('Initializing data service...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Initialize MongoDB client singleton
|
|
||||||
logger.debug('Connecting to MongoDB...');
|
|
||||||
const mongoConfig = databaseConfig.mongodb;
|
|
||||||
await connectMongoDB({
|
|
||||||
uri: mongoConfig.uri,
|
|
||||||
database: mongoConfig.database,
|
|
||||||
host: mongoConfig.host || 'localhost',
|
|
||||||
port: mongoConfig.port || 27017,
|
|
||||||
timeouts: {
|
|
||||||
connectTimeout: 30000,
|
|
||||||
socketTimeout: 30000,
|
|
||||||
serverSelectionTimeout: 5000,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
logger.info('MongoDB connected');
|
|
||||||
|
|
||||||
// Initialize PostgreSQL client singleton
|
|
||||||
logger.debug('Connecting to PostgreSQL...');
|
|
||||||
const pgConfig = databaseConfig.postgres;
|
|
||||||
await connectPostgreSQL({
|
|
||||||
host: pgConfig.host,
|
|
||||||
port: pgConfig.port,
|
|
||||||
database: pgConfig.database,
|
|
||||||
username: pgConfig.user,
|
|
||||||
password: pgConfig.password,
|
|
||||||
poolSettings: {
|
|
||||||
min: 2,
|
|
||||||
max: pgConfig.poolSize || 10,
|
|
||||||
idleTimeoutMillis: pgConfig.idleTimeout || 30000,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
logger.info('PostgreSQL connected');
|
|
||||||
|
|
||||||
// Initialize queue system (with delayed worker start)
|
// Initialize queue system (with delayed worker start)
|
||||||
logger.debug('Initializing queue system...');
|
logger.debug('Initializing queue system...');
|
||||||
|
|
@ -136,12 +110,13 @@ async function initializeServices() {
|
||||||
const { initializeProxyProvider } = await import('./handlers/proxy/proxy.handler');
|
const { initializeProxyProvider } = await import('./handlers/proxy/proxy.handler');
|
||||||
const { initializeQMProvider } = await import('./handlers/qm/qm.handler');
|
const { initializeQMProvider } = await import('./handlers/qm/qm.handler');
|
||||||
|
|
||||||
initializeWebShareProvider();
|
// Pass service container to handlers
|
||||||
initializeIBProvider();
|
initializeWebShareProvider(serviceContainer);
|
||||||
initializeProxyProvider();
|
initializeIBProvider(serviceContainer);
|
||||||
initializeQMProvider();
|
initializeProxyProvider(serviceContainer);
|
||||||
|
initializeQMProvider(serviceContainer);
|
||||||
|
|
||||||
logger.info('Data handlers initialized');
|
logger.info('Data handlers initialized with service container');
|
||||||
|
|
||||||
// Create scheduled jobs from registered handlers
|
// Create scheduled jobs from registered handlers
|
||||||
logger.debug('Creating scheduled jobs from registered handlers...');
|
logger.debug('Creating scheduled jobs from registered handlers...');
|
||||||
|
|
@ -205,13 +180,17 @@ async function initializeServices() {
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
await initializeServices();
|
await initializeServices();
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
throw new Error('App not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
server = Bun.serve({
|
server = Bun.serve({
|
||||||
port: PORT,
|
port: PORT,
|
||||||
fetch: app.fetch,
|
fetch: app.fetch,
|
||||||
development: config.environment === 'development',
|
development: config.environment === 'development',
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`Data Service started on port ${PORT}`);
|
logger.info(`Data-ingestion service started on port ${PORT}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register shutdown handlers with priorities
|
// Register shutdown handlers with priorities
|
||||||
|
|
@ -241,20 +220,18 @@ shutdown.onShutdownHigh(async () => {
|
||||||
}
|
}
|
||||||
}, 'HTTP Server');
|
}, 'HTTP Server');
|
||||||
|
|
||||||
// Priority 2: Database connections (medium priority)
|
// Priority 2: Service container and connections (medium priority)
|
||||||
shutdown.onShutdownMedium(async () => {
|
shutdown.onShutdownMedium(async () => {
|
||||||
logger.info('Disconnecting from databases...');
|
logger.info('Disposing service container and connections...');
|
||||||
try {
|
try {
|
||||||
const { disconnectMongoDB } = await import('@stock-bot/mongodb-client');
|
if (serviceContainer) {
|
||||||
const { disconnectPostgreSQL } = await import('@stock-bot/postgres-client');
|
await serviceContainer.dispose();
|
||||||
|
logger.info('Service container disposed, all connections closed');
|
||||||
await disconnectMongoDB();
|
|
||||||
await disconnectPostgreSQL();
|
|
||||||
logger.info('Database connections closed');
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error closing database connections', { error });
|
|
||||||
}
|
}
|
||||||
}, 'Databases');
|
} catch (error) {
|
||||||
|
logger.error('Error disposing service container', { error });
|
||||||
|
}
|
||||||
|
}, 'Service Container');
|
||||||
|
|
||||||
// Priority 3: Logger shutdown (lowest priority - runs last)
|
// Priority 3: Logger shutdown (lowest priority - runs last)
|
||||||
shutdown.onShutdownLow(async () => {
|
shutdown.onShutdownLow(async () => {
|
||||||
|
|
|
||||||
27
apps/data-ingestion/src/routes/create-routes.ts
Normal file
27
apps/data-ingestion/src/routes/create-routes.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
import type { ServiceContainer } from '@stock-bot/connection-factory';
|
||||||
|
import { exchangeRoutes } from './exchange.routes';
|
||||||
|
import { healthRoutes } from './health.routes';
|
||||||
|
import { queueRoutes } from './queue.routes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates all routes with access to the service container
|
||||||
|
*/
|
||||||
|
export function createRoutes(container: ServiceContainer): Hono {
|
||||||
|
const app = new Hono();
|
||||||
|
|
||||||
|
// Mount routes that don't need container
|
||||||
|
app.route('/health', healthRoutes);
|
||||||
|
|
||||||
|
// TODO: Update these routes to use container when needed
|
||||||
|
app.route('/api/exchanges', exchangeRoutes);
|
||||||
|
app.route('/api/queue', queueRoutes);
|
||||||
|
|
||||||
|
// Store container in app context for handlers that need it
|
||||||
|
app.use('*', async (c, next) => {
|
||||||
|
c.set('container', container);
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
@ -3,10 +3,9 @@ import { getLogger } from '@stock-bot/logger';
|
||||||
import {
|
import {
|
||||||
ConnectionFactory,
|
ConnectionFactory,
|
||||||
ServiceContainer,
|
ServiceContainer,
|
||||||
createServiceContainer,
|
|
||||||
PoolSizeCalculator
|
PoolSizeCalculator
|
||||||
} from '@stock-bot/connection-factory';
|
} from '@stock-bot/connection-factory';
|
||||||
import type { ConnectionFactoryConfig } from '@stock-bot/connection-factory';
|
import type { ConnectionFactoryConfig, DynamicPoolConfig } from '@stock-bot/mongodb-client';
|
||||||
|
|
||||||
const logger = getLogger('database-setup');
|
const logger = getLogger('database-setup');
|
||||||
|
|
||||||
|
|
@ -55,10 +54,18 @@ export async function setupServiceContainer(): Promise<ServiceContainer> {
|
||||||
const pool = await connectionFactory.createMongoDB({
|
const pool = await connectionFactory.createMongoDB({
|
||||||
name: 'default',
|
name: 'default',
|
||||||
config: {
|
config: {
|
||||||
connectionString: dbConfig.mongodb.uri,
|
uri: dbConfig.mongodb.uri,
|
||||||
database: dbConfig.mongodb.database,
|
database: dbConfig.mongodb.database,
|
||||||
|
host: dbConfig.mongodb.host,
|
||||||
|
port: dbConfig.mongodb.port,
|
||||||
|
username: dbConfig.mongodb.username,
|
||||||
|
password: dbConfig.mongodb.password,
|
||||||
|
authSource: dbConfig.mongodb.authSource,
|
||||||
|
poolSettings: {
|
||||||
maxPoolSize: poolSize.max,
|
maxPoolSize: poolSize.max,
|
||||||
minPoolSize: poolSize.min,
|
minPoolSize: poolSize.min,
|
||||||
|
maxIdleTime: 30000,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
maxConnections: poolSize.max,
|
maxConnections: poolSize.max,
|
||||||
minConnections: poolSize.min,
|
minConnections: poolSize.min,
|
||||||
|
|
@ -82,11 +89,12 @@ export async function setupServiceContainer(): Promise<ServiceContainer> {
|
||||||
host: dbConfig.postgresql.host,
|
host: dbConfig.postgresql.host,
|
||||||
port: dbConfig.postgresql.port,
|
port: dbConfig.postgresql.port,
|
||||||
database: dbConfig.postgresql.database,
|
database: dbConfig.postgresql.database,
|
||||||
user: dbConfig.postgresql.user,
|
username: dbConfig.postgresql.user,
|
||||||
password: dbConfig.postgresql.password,
|
password: dbConfig.postgresql.password,
|
||||||
pool: {
|
poolSettings: {
|
||||||
max: poolSize.max,
|
max: poolSize.max,
|
||||||
min: poolSize.min,
|
min: poolSize.min,
|
||||||
|
idleTimeoutMillis: 30000,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
maxConnections: poolSize.max,
|
maxConnections: poolSize.max,
|
||||||
|
|
@ -133,5 +141,45 @@ export async function setupServiceContainer(): Promise<ServiceContainer> {
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info('Service container setup complete');
|
logger.info('Service container setup complete');
|
||||||
|
|
||||||
|
// Optional: Enable dynamic pool sizing for production
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
await enableDynamicPoolSizing(container);
|
||||||
|
}
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable dynamic pool sizing for production workloads
|
||||||
|
*/
|
||||||
|
async function enableDynamicPoolSizing(container: ServiceContainer): Promise<void> {
|
||||||
|
const dynamicConfig: DynamicPoolConfig = {
|
||||||
|
enabled: true,
|
||||||
|
minSize: 5,
|
||||||
|
maxSize: 100,
|
||||||
|
scaleUpThreshold: 70,
|
||||||
|
scaleDownThreshold: 30,
|
||||||
|
scaleUpIncrement: 10,
|
||||||
|
scaleDownIncrement: 5,
|
||||||
|
evaluationInterval: 30000, // Check every 30 seconds
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Set dynamic config for MongoDB
|
||||||
|
const mongoClient = await container.resolveAsync('mongodb');
|
||||||
|
if (mongoClient && typeof mongoClient.setDynamicPoolConfig === 'function') {
|
||||||
|
mongoClient.setDynamicPoolConfig(dynamicConfig);
|
||||||
|
logger.info('Dynamic pool sizing enabled for MongoDB');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set dynamic config for PostgreSQL
|
||||||
|
const pgClient = await container.resolveAsync('postgres');
|
||||||
|
if (pgClient && typeof pgClient.setDynamicPoolConfig === 'function') {
|
||||||
|
pgClient.setDynamicPoolConfig(dynamicConfig);
|
||||||
|
logger.info('Dynamic pool sizing enabled for PostgreSQL');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to enable dynamic pool sizing', { error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,8 @@
|
||||||
{ "path": "../../libs/mongodb-client" },
|
{ "path": "../../libs/mongodb-client" },
|
||||||
{ "path": "../../libs/postgres-client" },
|
{ "path": "../../libs/postgres-client" },
|
||||||
{ "path": "../../libs/questdb-client" },
|
{ "path": "../../libs/questdb-client" },
|
||||||
{ "path": "../../libs/shutdown" }
|
{ "path": "../../libs/shutdown" },
|
||||||
|
{ "path": "../../libs/connection-factory" },
|
||||||
|
{ "path": "../../libs/utils" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
bun.lock
2
bun.lock
|
|
@ -47,12 +47,14 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@stock-bot/cache": "*",
|
"@stock-bot/cache": "*",
|
||||||
"@stock-bot/config": "*",
|
"@stock-bot/config": "*",
|
||||||
|
"@stock-bot/connection-factory": "*",
|
||||||
"@stock-bot/logger": "*",
|
"@stock-bot/logger": "*",
|
||||||
"@stock-bot/mongodb-client": "*",
|
"@stock-bot/mongodb-client": "*",
|
||||||
"@stock-bot/postgres-client": "*",
|
"@stock-bot/postgres-client": "*",
|
||||||
"@stock-bot/questdb-client": "*",
|
"@stock-bot/questdb-client": "*",
|
||||||
"@stock-bot/queue": "*",
|
"@stock-bot/queue": "*",
|
||||||
"@stock-bot/shutdown": "*",
|
"@stock-bot/shutdown": "*",
|
||||||
|
"@stock-bot/utils": "*",
|
||||||
"hono": "^4.0.0",
|
"hono": "^4.0.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue