Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
392
vendor/ruvector/npm/packages/cloud-run/load-balancer.js
vendored
Normal file
392
vendor/ruvector/npm/packages/cloud-run/load-balancer.js
vendored
Normal file
@@ -0,0 +1,392 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Load Balancer - Intelligent request routing and traffic management
|
||||
*
|
||||
* Features:
|
||||
* - Circuit breaker pattern
|
||||
* - Rate limiting per client
|
||||
* - Regional routing
|
||||
* - Request prioritization
|
||||
* - Health-based routing
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.LoadBalancer = void 0;
|
||||
const events_1 = require("events");
|
||||
const api_1 = require("@opentelemetry/api");
|
||||
const prom_client_1 = require("prom-client");
|
||||
// Metrics
|
||||
const metrics = {
|
||||
routedRequests: new prom_client_1.Counter({
|
||||
name: 'load_balancer_routed_requests_total',
|
||||
help: 'Total number of routed requests',
|
||||
labelNames: ['backend', 'status'],
|
||||
}),
|
||||
rejectedRequests: new prom_client_1.Counter({
|
||||
name: 'load_balancer_rejected_requests_total',
|
||||
help: 'Total number of rejected requests',
|
||||
labelNames: ['reason'],
|
||||
}),
|
||||
circuitBreakerState: new prom_client_1.Gauge({
|
||||
name: 'circuit_breaker_state',
|
||||
help: 'Circuit breaker state (0=closed, 1=open, 2=half-open)',
|
||||
labelNames: ['backend'],
|
||||
}),
|
||||
rateLimitActive: new prom_client_1.Gauge({
|
||||
name: 'rate_limit_active_clients',
|
||||
help: 'Number of clients currently rate limited',
|
||||
}),
|
||||
requestLatency: new prom_client_1.Histogram({
|
||||
name: 'load_balancer_request_latency_seconds',
|
||||
help: 'Request latency in seconds',
|
||||
labelNames: ['backend'],
|
||||
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
|
||||
}),
|
||||
};
|
||||
const tracer = api_1.trace.getTracer('load-balancer', '1.0.0');
|
||||
// Circuit breaker states
|
||||
var CircuitState;
|
||||
(function (CircuitState) {
|
||||
CircuitState[CircuitState["CLOSED"] = 0] = "CLOSED";
|
||||
CircuitState[CircuitState["OPEN"] = 1] = "OPEN";
|
||||
CircuitState[CircuitState["HALF_OPEN"] = 2] = "HALF_OPEN";
|
||||
})(CircuitState || (CircuitState = {}));
|
||||
// Request priority
|
||||
var RequestPriority;
|
||||
(function (RequestPriority) {
|
||||
RequestPriority[RequestPriority["LOW"] = 0] = "LOW";
|
||||
RequestPriority[RequestPriority["NORMAL"] = 1] = "NORMAL";
|
||||
RequestPriority[RequestPriority["HIGH"] = 2] = "HIGH";
|
||||
RequestPriority[RequestPriority["CRITICAL"] = 3] = "CRITICAL";
|
||||
})(RequestPriority || (RequestPriority = {}));
|
||||
/**
|
||||
* Token Bucket Rate Limiter
|
||||
*/
|
||||
class RateLimiter {
|
||||
constructor(requestsPerSecond) {
|
||||
this.buckets = new Map();
|
||||
this.capacity = requestsPerSecond;
|
||||
this.refillRate = requestsPerSecond;
|
||||
}
|
||||
tryAcquire(clientId, tokens = 1) {
|
||||
const now = Date.now();
|
||||
let bucket = this.buckets.get(clientId);
|
||||
if (!bucket) {
|
||||
bucket = { tokens: this.capacity, lastRefill: now };
|
||||
this.buckets.set(clientId, bucket);
|
||||
}
|
||||
// Refill tokens based on time passed
|
||||
const timePassed = (now - bucket.lastRefill) / 1000;
|
||||
const tokensToAdd = timePassed * this.refillRate;
|
||||
bucket.tokens = Math.min(this.capacity, bucket.tokens + tokensToAdd);
|
||||
bucket.lastRefill = now;
|
||||
// Try to consume tokens
|
||||
if (bucket.tokens >= tokens) {
|
||||
bucket.tokens -= tokens;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
reset(clientId) {
|
||||
this.buckets.delete(clientId);
|
||||
}
|
||||
getStats() {
|
||||
let limitedClients = 0;
|
||||
for (const [_, bucket] of this.buckets) {
|
||||
if (bucket.tokens < 1) {
|
||||
limitedClients++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
totalClients: this.buckets.size,
|
||||
limitedClients,
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Circuit Breaker
|
||||
*/
|
||||
class CircuitBreaker {
|
||||
constructor(backendId, threshold, timeout, halfOpenMaxRequests) {
|
||||
this.backendId = backendId;
|
||||
this.threshold = threshold;
|
||||
this.timeout = timeout;
|
||||
this.halfOpenMaxRequests = halfOpenMaxRequests;
|
||||
this.state = CircuitState.CLOSED;
|
||||
this.failures = 0;
|
||||
this.successes = 0;
|
||||
this.lastFailureTime = 0;
|
||||
this.halfOpenRequests = 0;
|
||||
this.updateMetrics();
|
||||
}
|
||||
async execute(fn) {
|
||||
if (this.state === CircuitState.OPEN) {
|
||||
// Check if timeout has passed
|
||||
if (Date.now() - this.lastFailureTime >= this.timeout) {
|
||||
this.state = CircuitState.HALF_OPEN;
|
||||
this.halfOpenRequests = 0;
|
||||
this.updateMetrics();
|
||||
}
|
||||
else {
|
||||
throw new Error(`Circuit breaker open for backend ${this.backendId}`);
|
||||
}
|
||||
}
|
||||
if (this.state === CircuitState.HALF_OPEN) {
|
||||
if (this.halfOpenRequests >= this.halfOpenMaxRequests) {
|
||||
throw new Error(`Circuit breaker half-open limit reached for backend ${this.backendId}`);
|
||||
}
|
||||
this.halfOpenRequests++;
|
||||
}
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const result = await fn();
|
||||
this.onSuccess();
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
metrics.requestLatency.observe({ backend: this.backendId }, duration);
|
||||
metrics.routedRequests.inc({ backend: this.backendId, status: 'success' });
|
||||
return result;
|
||||
}
|
||||
catch (error) {
|
||||
this.onFailure();
|
||||
metrics.routedRequests.inc({ backend: this.backendId, status: 'failure' });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
onSuccess() {
|
||||
this.failures = 0;
|
||||
this.successes++;
|
||||
if (this.state === CircuitState.HALF_OPEN) {
|
||||
if (this.successes >= this.halfOpenMaxRequests) {
|
||||
this.state = CircuitState.CLOSED;
|
||||
this.successes = 0;
|
||||
this.updateMetrics();
|
||||
}
|
||||
}
|
||||
}
|
||||
onFailure() {
|
||||
this.failures++;
|
||||
this.lastFailureTime = Date.now();
|
||||
const failureRate = this.failures / (this.failures + this.successes);
|
||||
if (failureRate >= this.threshold) {
|
||||
this.state = CircuitState.OPEN;
|
||||
this.updateMetrics();
|
||||
}
|
||||
}
|
||||
updateMetrics() {
|
||||
metrics.circuitBreakerState.set({ backend: this.backendId }, this.state);
|
||||
}
|
||||
getState() {
|
||||
return this.state;
|
||||
}
|
||||
reset() {
|
||||
this.state = CircuitState.CLOSED;
|
||||
this.failures = 0;
|
||||
this.successes = 0;
|
||||
this.lastFailureTime = 0;
|
||||
this.halfOpenRequests = 0;
|
||||
this.updateMetrics();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Backend Manager
|
||||
*/
|
||||
class BackendManager {
|
||||
constructor(backends, circuitBreakerThreshold, circuitBreakerTimeout, halfOpenMaxRequests) {
|
||||
this.backends = new Map();
|
||||
for (const backend of backends) {
|
||||
this.backends.set(backend.id, {
|
||||
config: backend,
|
||||
circuitBreaker: new CircuitBreaker(backend.id, circuitBreakerThreshold, circuitBreakerTimeout, halfOpenMaxRequests),
|
||||
activeRequests: 0,
|
||||
healthScore: 1.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
selectBackend(region) {
|
||||
const available = Array.from(this.backends.entries())
|
||||
.filter(([_, backend]) => {
|
||||
// Filter by region if specified
|
||||
if (region && backend.config.region !== region) {
|
||||
return false;
|
||||
}
|
||||
// Filter by circuit breaker state
|
||||
if (backend.circuitBreaker.getState() === CircuitState.OPEN) {
|
||||
return false;
|
||||
}
|
||||
// Filter by concurrency limit
|
||||
if (backend.config.maxConcurrency &&
|
||||
backend.activeRequests >= backend.config.maxConcurrency) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(([id, backend]) => ({
|
||||
id,
|
||||
score: this.calculateScore(backend),
|
||||
}))
|
||||
.sort((a, b) => b.score - a.score);
|
||||
return available.length > 0 ? available[0].id : null;
|
||||
}
|
||||
calculateScore(backend) {
|
||||
const weight = backend.config.weight || 1;
|
||||
const loadFactor = backend.config.maxConcurrency
|
||||
? 1 - (backend.activeRequests / backend.config.maxConcurrency)
|
||||
: 1;
|
||||
return weight * loadFactor * backend.healthScore;
|
||||
}
|
||||
async executeOnBackend(backendId, fn) {
|
||||
const backend = this.backends.get(backendId);
|
||||
if (!backend) {
|
||||
throw new Error(`Backend ${backendId} not found`);
|
||||
}
|
||||
backend.activeRequests++;
|
||||
try {
|
||||
return await backend.circuitBreaker.execute(fn);
|
||||
}
|
||||
finally {
|
||||
backend.activeRequests--;
|
||||
}
|
||||
}
|
||||
updateHealth(backendId, healthScore) {
|
||||
const backend = this.backends.get(backendId);
|
||||
if (backend) {
|
||||
backend.healthScore = Math.max(0, Math.min(1, healthScore));
|
||||
}
|
||||
}
|
||||
getStats() {
|
||||
const stats = {};
|
||||
for (const [id, backend] of this.backends) {
|
||||
stats[id] = {
|
||||
activeRequests: backend.activeRequests,
|
||||
healthScore: backend.healthScore,
|
||||
circuitState: backend.circuitBreaker.getState(),
|
||||
region: backend.config.region,
|
||||
};
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Priority Queue for request scheduling
|
||||
*/
|
||||
class PriorityQueue {
|
||||
constructor() {
|
||||
this.queues = new Map([
|
||||
[RequestPriority.CRITICAL, []],
|
||||
[RequestPriority.HIGH, []],
|
||||
[RequestPriority.NORMAL, []],
|
||||
[RequestPriority.LOW, []],
|
||||
]);
|
||||
}
|
||||
enqueue(item, priority) {
|
||||
const queue = this.queues.get(priority);
|
||||
queue.push(item);
|
||||
}
|
||||
dequeue() {
|
||||
// Process by priority
|
||||
for (const priority of [
|
||||
RequestPriority.CRITICAL,
|
||||
RequestPriority.HIGH,
|
||||
RequestPriority.NORMAL,
|
||||
RequestPriority.LOW,
|
||||
]) {
|
||||
const queue = this.queues.get(priority);
|
||||
if (queue.length > 0) {
|
||||
return queue.shift();
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
size() {
|
||||
return Array.from(this.queues.values()).reduce((sum, q) => sum + q.length, 0);
|
||||
}
|
||||
clear() {
|
||||
for (const queue of this.queues.values()) {
|
||||
queue.length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Load Balancer
|
||||
*/
|
||||
class LoadBalancer extends events_1.EventEmitter {
|
||||
constructor(config) {
|
||||
super();
|
||||
this.config = {
|
||||
maxRequestsPerSecond: config.maxRequestsPerSecond || 10000,
|
||||
circuitBreakerThreshold: config.circuitBreakerThreshold || 0.5,
|
||||
circuitBreakerTimeout: config.circuitBreakerTimeout || 30000,
|
||||
halfOpenMaxRequests: config.halfOpenMaxRequests || 5,
|
||||
backends: config.backends || [{ id: 'default', host: 'localhost' }],
|
||||
enableRegionalRouting: config.enableRegionalRouting !== false,
|
||||
priorityQueueSize: config.priorityQueueSize || 1000,
|
||||
};
|
||||
this.rateLimiter = new RateLimiter(this.config.maxRequestsPerSecond);
|
||||
this.backendManager = new BackendManager(this.config.backends, this.config.circuitBreakerThreshold, this.config.circuitBreakerTimeout, this.config.halfOpenMaxRequests);
|
||||
this.requestQueue = new PriorityQueue();
|
||||
this.updateMetrics();
|
||||
}
|
||||
async route(collection, query, clientId = 'default', priority = RequestPriority.NORMAL) {
|
||||
const span = tracer.startSpan('load-balancer-route', {
|
||||
attributes: { collection, clientId, priority },
|
||||
});
|
||||
try {
|
||||
// Rate limiting check
|
||||
if (!this.rateLimiter.tryAcquire(clientId)) {
|
||||
metrics.rejectedRequests.inc({ reason: 'rate_limit' });
|
||||
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: 'Rate limit exceeded' });
|
||||
return false;
|
||||
}
|
||||
// Queue size check
|
||||
if (this.requestQueue.size() >= this.config.priorityQueueSize) {
|
||||
metrics.rejectedRequests.inc({ reason: 'queue_full' });
|
||||
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: 'Queue full' });
|
||||
return false;
|
||||
}
|
||||
// Select backend
|
||||
const region = query.region;
|
||||
const backendId = this.backendManager.selectBackend(this.config.enableRegionalRouting ? region : undefined);
|
||||
if (!backendId) {
|
||||
metrics.rejectedRequests.inc({ reason: 'no_backend' });
|
||||
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: 'No backend available' });
|
||||
return false;
|
||||
}
|
||||
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error.message });
|
||||
return false;
|
||||
}
|
||||
finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
async executeWithLoadBalancing(fn, region, priority = RequestPriority.NORMAL) {
|
||||
const backendId = this.backendManager.selectBackend(this.config.enableRegionalRouting ? region : undefined);
|
||||
if (!backendId) {
|
||||
throw new Error('No backend available');
|
||||
}
|
||||
return this.backendManager.executeOnBackend(backendId, fn);
|
||||
}
|
||||
updateBackendHealth(backendId, healthScore) {
|
||||
this.backendManager.updateHealth(backendId, healthScore);
|
||||
}
|
||||
updateMetrics() {
|
||||
setInterval(() => {
|
||||
const rateLimitStats = this.rateLimiter.getStats();
|
||||
metrics.rateLimitActive.set(rateLimitStats.limitedClients);
|
||||
}, 5000);
|
||||
}
|
||||
getStats() {
|
||||
return {
|
||||
rateLimit: this.rateLimiter.getStats(),
|
||||
backends: this.backendManager.getStats(),
|
||||
queueSize: this.requestQueue.size(),
|
||||
};
|
||||
}
|
||||
reset() {
|
||||
this.requestQueue.clear();
|
||||
}
|
||||
}
|
||||
exports.LoadBalancer = LoadBalancer;
|
||||
//# sourceMappingURL=load-balancer.js.map
|
||||
Reference in New Issue
Block a user