@aaswe/codebase-ai
Version:
AI-Assisted Software Engineering (AASWE) - Rich codebase context for IDE LLMs
407 lines • 16 kB
JavaScript
"use strict";
/**
* Query Router
*
* Implements intelligent query routing for the Hybrid Storage Manager
* to optimize query performance across different storage layers.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueryRouter = void 0;
const events_1 = require("events");
const logger_1 = __importDefault(require("../../../utils/logger"));
const types_1 = require("./types");
class QueryRouter extends events_1.EventEmitter {
routingRules = new Map();
performanceMetrics = new Map();
metricsCollectionTimer;
constructor(_config) {
super();
this.initializeRoutingRules();
this.initializeMetrics();
this.setupMetricsCollection();
}
/**
* Initialize the query router
*/
async initialize() {
try {
logger_1.default.info('Initializing Query Router');
// Initialize performance metrics for all storage layers
for (const layer of Object.values(types_1.StorageLayer)) {
this.performanceMetrics.set(layer, this.createEmptyMetrics());
}
logger_1.default.info('Query Router initialized successfully');
this.emit('initialized');
}
catch (error) {
logger_1.default.error('Failed to initialize Query Router:', error);
throw error;
}
}
/**
* Route a query to the optimal storage layer
*/
async routeQuery(request) {
try {
const startTime = Date.now();
// Analyze query characteristics
const queryAnalysis = this.analyzeQuery(request);
// Determine optimal storage layer
const optimalLayer = this.selectOptimalLayer(queryAnalysis, request);
// Create routing decision
const decision = {
primaryLayer: optimalLayer,
fallbackLayers: this.getFallbackLayers(optimalLayer, queryAnalysis),
reasoning: this.generateRoutingReasoning(optimalLayer, queryAnalysis),
estimatedLatency: this.estimateLatency(optimalLayer, queryAnalysis),
cacheStrategy: this.determineCacheStrategy(queryAnalysis),
queryId: this.generateQueryId(request)
};
// Update routing metrics
this.updateRoutingMetrics(decision, Date.now() - startTime);
this.emit('query_routed', { request, decision });
return decision;
}
catch (error) {
logger_1.default.error('Failed to route query:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new types_1.QueryError(`Query routing failed: ${errorMessage}`, request.query, error instanceof Error ? error : undefined);
}
}
/**
* Update performance metrics for a storage layer
*/
updatePerformanceMetrics(layer, latency, success, resultSize) {
const metrics = this.performanceMetrics.get(layer);
if (!metrics)
return;
metrics.totalQueries++;
metrics.totalLatency += latency;
metrics.averageLatency = metrics.totalLatency / metrics.totalQueries;
if (success) {
metrics.successfulQueries++;
}
else {
metrics.failedQueries++;
}
metrics.successRate = metrics.successfulQueries / metrics.totalQueries;
if (resultSize !== undefined) {
metrics.totalResultSize += resultSize;
metrics.averageResultSize = metrics.totalResultSize / metrics.successfulQueries;
}
// Update min/max latency
if (latency < metrics.minLatency || metrics.minLatency === 0) {
metrics.minLatency = latency;
}
if (latency > metrics.maxLatency) {
metrics.maxLatency = latency;
}
this.emit('metrics_updated', { layer, metrics });
}
/**
* Get performance metrics for all storage layers
*/
getPerformanceMetrics() {
return new Map(this.performanceMetrics);
}
/**
* Get routing statistics
*/
getRoutingStats() {
const stats = {};
for (const [layer, metrics] of this.performanceMetrics) {
stats[layer] = {
totalQueries: metrics.totalQueries,
successRate: metrics.successRate,
averageLatency: metrics.averageLatency,
minLatency: metrics.minLatency,
maxLatency: metrics.maxLatency,
averageResultSize: metrics.averageResultSize
};
}
return stats;
}
/**
* Add custom routing rule
*/
addRoutingRule(ruleId, route) {
this.routingRules.set(ruleId, route);
logger_1.default.debug(`Added routing rule: ${ruleId}`);
this.emit('routing_rule_added', { ruleId, route });
}
/**
* Remove routing rule
*/
removeRoutingRule(ruleId) {
const removed = this.routingRules.delete(ruleId);
if (removed) {
logger_1.default.debug(`Removed routing rule: ${ruleId}`);
this.emit('routing_rule_removed', { ruleId });
}
return removed;
}
/**
* Shutdown the query router
*/
async shutdown() {
try {
logger_1.default.info('Shutting down Query Router');
if (this.metricsCollectionTimer) {
clearInterval(this.metricsCollectionTimer);
this.metricsCollectionTimer = undefined;
}
this.routingRules.clear();
this.performanceMetrics.clear();
logger_1.default.info('Query Router shutdown completed');
this.emit('shutdown');
}
catch (error) {
logger_1.default.error('Query Router shutdown failed:', error);
throw error;
}
}
// Private helper methods
initializeRoutingRules() {
// Default routing rules based on query types
// Simple lookups go to cache first, then in-memory
this.routingRules.set('simple_lookup', {
condition: (req) => req.type === types_1.QueryType.SIMPLE_LOOKUP,
primaryLayer: types_1.StorageLayer.CACHE,
fallbackLayers: [types_1.StorageLayer.IN_MEMORY, types_1.StorageLayer.NEO4J],
priority: 1
});
// Complex graph queries go to Neo4j
this.routingRules.set('graph_traversal', {
condition: (req) => req.type === types_1.QueryType.GRAPH_TRAVERSAL,
primaryLayer: types_1.StorageLayer.NEO4J,
fallbackLayers: [types_1.StorageLayer.IN_MEMORY],
priority: 1
});
// Full-text search goes to appropriate layer
this.routingRules.set('full_text_search', {
condition: (req) => req.type === types_1.QueryType.FULL_TEXT_SEARCH,
primaryLayer: types_1.StorageLayer.NEO4J,
fallbackLayers: [types_1.StorageLayer.RDF_FILES],
priority: 1
});
// Aggregation queries go to Neo4j
this.routingRules.set('aggregation', {
condition: (req) => req.type === types_1.QueryType.AGGREGATION,
primaryLayer: types_1.StorageLayer.NEO4J,
fallbackLayers: [types_1.StorageLayer.IN_MEMORY],
priority: 1
});
// Pattern matching goes to in-memory for speed
this.routingRules.set('pattern_matching', {
condition: (req) => req.type === types_1.QueryType.PATTERN_MATCHING,
primaryLayer: types_1.StorageLayer.IN_MEMORY,
fallbackLayers: [types_1.StorageLayer.NEO4J],
priority: 1
});
}
initializeMetrics() {
for (const layer of Object.values(types_1.StorageLayer)) {
this.performanceMetrics.set(layer, this.createEmptyMetrics());
}
}
createEmptyMetrics() {
return {
totalQueries: 0,
successfulQueries: 0,
failedQueries: 0,
totalLatency: 0,
averageLatency: 0,
minLatency: 0,
maxLatency: 0,
successRate: 0,
totalResultSize: 0,
averageResultSize: 0
};
}
setupMetricsCollection() {
this.metricsCollectionTimer = setInterval(() => {
this.collectAndEmitMetrics();
}, 30000); // Collect every 30 seconds
// Ensure timer doesn't keep Node.js process alive
this.metricsCollectionTimer.unref();
}
collectAndEmitMetrics() {
const allMetrics = this.getPerformanceMetrics();
this.emit('metrics_collected', { metrics: allMetrics, timestamp: new Date() });
}
analyzeQuery(request) {
const query = request.query.toLowerCase();
// Analyze query complexity
let complexity = types_1.QueryComplexity.LOW;
if (query.includes('join') || query.includes('match') && query.includes('where')) {
complexity = types_1.QueryComplexity.MEDIUM;
}
if (query.includes('collect') || query.includes('count') || query.includes('sum') ||
query.includes('avg') || query.includes('max') || query.includes('min')) {
complexity = types_1.QueryComplexity.HIGH;
}
// Estimate result size based on query patterns
let estimatedResultSize = 100; // Default
if (query.includes('limit')) {
const limitMatch = query.match(/limit\s+(\d+)/);
if (limitMatch) {
estimatedResultSize = parseInt(limitMatch[1]);
}
}
else if (query.includes('*') || query.includes('all')) {
estimatedResultSize = 10000; // Large result set
}
// Check for joins and aggregations
const requiresJoins = query.includes('join') || (query.includes('match') && query.includes('-'));
const requiresAggregation = query.includes('collect') || query.includes('count') ||
query.includes('sum') || query.includes('avg');
// Determine if cacheable
const isCacheable = !query.includes('now()') && !query.includes('rand()') &&
!query.includes('timestamp') && complexity !== types_1.QueryComplexity.HIGH;
return {
complexity,
estimatedResultSize,
requiresJoins,
requiresAggregation,
isCacheable
};
}
selectOptimalLayer(analysis, request) {
// Check custom routing rules first
for (const [ruleId, rule] of this.routingRules) {
if (rule.condition(request)) {
logger_1.default.debug(`Query matched routing rule: ${ruleId}`);
return rule.primaryLayer;
}
}
// Default routing logic based on analysis
if (analysis.isCacheable && analysis.complexity === types_1.QueryComplexity.LOW) {
return types_1.StorageLayer.CACHE;
}
if (analysis.requiresAggregation || analysis.complexity === types_1.QueryComplexity.HIGH) {
return types_1.StorageLayer.NEO4J;
}
if (analysis.requiresJoins || request.type === types_1.QueryType.GRAPH_TRAVERSAL) {
return types_1.StorageLayer.NEO4J;
}
if (analysis.estimatedResultSize < 1000 && analysis.complexity === types_1.QueryComplexity.LOW) {
return types_1.StorageLayer.IN_MEMORY;
}
// Default to Neo4j for complex queries
return types_1.StorageLayer.NEO4J;
}
getFallbackLayers(primaryLayer, analysis) {
const fallbacks = [];
switch (primaryLayer) {
case types_1.StorageLayer.CACHE:
fallbacks.push(types_1.StorageLayer.IN_MEMORY, types_1.StorageLayer.NEO4J);
break;
case types_1.StorageLayer.IN_MEMORY:
fallbacks.push(types_1.StorageLayer.NEO4J);
if (analysis.isCacheable) {
fallbacks.unshift(types_1.StorageLayer.CACHE);
}
break;
case types_1.StorageLayer.NEO4J:
if (analysis.complexity === types_1.QueryComplexity.LOW) {
fallbacks.push(types_1.StorageLayer.IN_MEMORY);
}
break;
case types_1.StorageLayer.RDF_FILES:
fallbacks.push(types_1.StorageLayer.NEO4J, types_1.StorageLayer.IN_MEMORY);
break;
}
return fallbacks;
}
generateRoutingReasoning(layer, analysis) {
const reasons = [];
switch (layer) {
case types_1.StorageLayer.CACHE:
reasons.push('Query is cacheable and has low complexity');
break;
case types_1.StorageLayer.IN_MEMORY:
reasons.push('Query has low complexity and small result set');
break;
case types_1.StorageLayer.NEO4J:
if (analysis.requiresAggregation) {
reasons.push('Query requires aggregation operations');
}
if (analysis.requiresJoins) {
reasons.push('Query requires complex joins');
}
if (analysis.complexity === types_1.QueryComplexity.HIGH) {
reasons.push('Query has high complexity');
}
break;
case types_1.StorageLayer.RDF_FILES:
reasons.push('Query requires direct RDF file access');
break;
}
return reasons.join('; ');
}
estimateLatency(layer, analysis) {
const metrics = this.performanceMetrics.get(layer);
const baseLatency = metrics?.averageLatency || this.getDefaultLatency(layer);
// Adjust based on complexity
let multiplier = 1;
switch (analysis.complexity) {
case types_1.QueryComplexity.LOW:
multiplier = 1;
break;
case types_1.QueryComplexity.MEDIUM:
multiplier = 2;
break;
case types_1.QueryComplexity.HIGH:
multiplier = 4;
break;
}
// Adjust based on result size
if (analysis.estimatedResultSize > 1000) {
multiplier *= 1.5;
}
return Math.round(baseLatency * multiplier);
}
getDefaultLatency(layer) {
switch (layer) {
case types_1.StorageLayer.CACHE:
return 1; // 1ms
case types_1.StorageLayer.IN_MEMORY:
return 5; // 5ms
case types_1.StorageLayer.NEO4J:
return 50; // 50ms
case types_1.StorageLayer.RDF_FILES:
return 100; // 100ms
default:
return 50;
}
}
determineCacheStrategy(analysis) {
if (!analysis.isCacheable) {
return 'none';
}
if (analysis.complexity === types_1.QueryComplexity.LOW && analysis.estimatedResultSize < 100) {
return 'long';
}
if (analysis.complexity === types_1.QueryComplexity.MEDIUM) {
return 'medium';
}
return 'short';
}
generateQueryId(request) {
const hash = require('crypto')
.createHash('md5')
.update(request.query + JSON.stringify(request.parameters || {}))
.digest('hex');
return `query_${hash.substring(0, 8)}`;
}
updateRoutingMetrics(decision, routingTime) {
// This could be expanded to track routing-specific metrics
logger_1.default.debug(`Query routed to ${decision.primaryLayer} in ${routingTime}ms`);
}
}
exports.QueryRouter = QueryRouter;
exports.default = QueryRouter;
//# sourceMappingURL=QueryRouter.js.map