@jetit/publisher 4.1.1 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PrometheusAdapter = void 0;
4
+ class PrometheusAdapter {
5
+ /**
6
+ *
7
+ * @param streams [Publisher]
8
+ * @param promClient [Prom Client] This needs to be an instance of prom-client
9
+ */
10
+ constructor(streams, promClient) {
11
+ this.streams = streams;
12
+ this.promClient = promClient;
13
+ this.registry = new this.promClient.Registry({ collectDefaultMetrics: { timeout: 60000 } });
14
+ this.initializeMetrics();
15
+ }
16
+ initializeMetrics() {
17
+ this.queueDepth = new this.promClient.Gauge({
18
+ name: 'queue_depth',
19
+ help: 'Total number of messages in all queues',
20
+ registers: [this.registry],
21
+ });
22
+ this.dlqSize = new this.promClient.Gauge({
23
+ name: 'dlq_size',
24
+ help: 'Number of messages in the Dead Letter Queue',
25
+ registers: [this.registry],
26
+ });
27
+ this.duplicateEventCount = new this.promClient.Counter({
28
+ name: 'duplicate_event_count',
29
+ help: 'Number of duplicate events detected',
30
+ registers: [this.registry],
31
+ });
32
+ this.dlqRate = new this.promClient.Gauge({
33
+ name: 'dlq_rate',
34
+ help: 'Rate of messages being added to the Dead Letter Queue',
35
+ registers: [this.registry],
36
+ });
37
+ this.operationCount = new this.promClient.Counter({
38
+ name: 'operation_count',
39
+ help: 'Total number of operations performed',
40
+ registers: [this.registry],
41
+ });
42
+ this.totalTime = new this.promClient.Gauge({
43
+ name: 'total_time',
44
+ help: 'Total processing time in milliseconds',
45
+ registers: [this.registry],
46
+ });
47
+ this.redisOperationTime = new this.promClient.Gauge({
48
+ name: 'redis_operation_time',
49
+ help: 'Time spent on Redis operations in milliseconds',
50
+ registers: [this.registry],
51
+ });
52
+ this.processingTime = new this.promClient.Gauge({
53
+ name: 'processing_time',
54
+ help: 'Time spent on processing events in milliseconds',
55
+ registers: [this.registry],
56
+ });
57
+ this.eventCount = new this.promClient.Counter({
58
+ name: 'event_count',
59
+ help: 'Total number of events processed',
60
+ registers: [this.registry],
61
+ });
62
+ this.publishErrorCount = new this.promClient.Counter({
63
+ name: 'publish_error_count',
64
+ help: 'Number of errors encountered during publish operations',
65
+ registers: [this.registry],
66
+ });
67
+ this.subscribeErrorCount = new this.promClient.Counter({
68
+ name: 'subscribe_error_count',
69
+ help: 'Number of errors encountered during subscribe operations',
70
+ registers: [this.registry],
71
+ });
72
+ this.individualQueueDepth = new this.promClient.Gauge({
73
+ name: 'individual_queue_depth',
74
+ help: 'Number of messages in each individual queue',
75
+ labelNames: ['queue_name'],
76
+ registers: [this.registry],
77
+ });
78
+ this.messageRatePublish = new this.promClient.Gauge({
79
+ name: 'message_rate_publish',
80
+ help: 'Number of messages published per event type',
81
+ labelNames: ['event_type'],
82
+ registers: [this.registry],
83
+ });
84
+ this.messageRateSubscribe = new this.promClient.Gauge({
85
+ name: 'message_rate_subscribe',
86
+ help: 'Number of messages subscribed per event type',
87
+ labelNames: ['event_type'],
88
+ registers: [this.registry],
89
+ });
90
+ this.processingTimeHistogram = new this.promClient.Histogram({
91
+ name: 'processing_time_histogram',
92
+ help: 'Distribution of event processing times',
93
+ buckets: [0, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000],
94
+ registers: [this.registry],
95
+ });
96
+ this.redisCommandLatency = new this.promClient.Gauge({
97
+ name: 'redis_command_latency',
98
+ help: 'Latency of Redis commands',
99
+ labelNames: ['command'],
100
+ registers: [this.registry],
101
+ });
102
+ this.consumerLag = new this.promClient.Gauge({
103
+ name: 'consumer_lag',
104
+ help: 'Lag of consumers in milliseconds',
105
+ labelNames: ['consumer_group'],
106
+ registers: [this.registry],
107
+ });
108
+ this.registry.registerMetric(this.queueDepth);
109
+ this.registry.registerMetric(this.dlqSize);
110
+ this.registry.registerMetric(this.dlqRate);
111
+ this.registry.registerMetric(this.operationCount);
112
+ this.registry.registerMetric(this.totalTime);
113
+ this.registry.registerMetric(this.redisOperationTime);
114
+ this.registry.registerMetric(this.processingTime);
115
+ this.registry.registerMetric(this.eventCount);
116
+ this.registry.registerMetric(this.publishErrorCount);
117
+ this.registry.registerMetric(this.subscribeErrorCount);
118
+ this.registry.registerMetric(this.individualQueueDepth);
119
+ this.registry.registerMetric(this.duplicateEventCount);
120
+ this.registry.registerMetric(this.messageRatePublish);
121
+ this.registry.registerMetric(this.messageRateSubscribe);
122
+ this.registry.registerMetric(this.processingTimeHistogram);
123
+ this.registry.registerMetric(this.redisCommandLatency);
124
+ this.registry.registerMetric(this.consumerLag);
125
+ }
126
+ async updateMetrics() {
127
+ const metrics = await this.streams.getLatestMetrics();
128
+ if (metrics) {
129
+ this.updatePrometheusMetrics(metrics);
130
+ }
131
+ }
132
+ updatePrometheusMetrics(metrics) {
133
+ this.queueDepth.set(metrics.queueDepth);
134
+ this.dlqSize.set(metrics.dlqSize);
135
+ this.dlqRate.set(metrics.dlqRate);
136
+ this.operationCount.inc(metrics.operationCount);
137
+ this.totalTime.set(metrics.totalTime);
138
+ this.redisOperationTime.set(metrics.redisOperationTime);
139
+ this.processingTime.set(metrics.processingTime);
140
+ this.eventCount.inc(metrics.eventCount);
141
+ this.publishErrorCount.inc(metrics.publishErrorCount);
142
+ this.subscribeErrorCount.inc(metrics.subscribeErrorCount);
143
+ this.duplicateEventCount.inc(metrics.duplicateEventsCount);
144
+ if (metrics.individualQueueDepts) {
145
+ Object.entries(metrics.individualQueueDepts).forEach(([queueName, depth]) => {
146
+ this.individualQueueDepth.set({ queue_name: queueName }, depth);
147
+ });
148
+ }
149
+ Object.entries(metrics.messageRatePublish).forEach(([eventType, rate]) => {
150
+ this.messageRatePublish.set({ event_type: eventType }, rate);
151
+ });
152
+ Object.entries(metrics.messageRateSubscribe).forEach(([eventType, rate]) => {
153
+ this.messageRateSubscribe.set({ event_type: eventType }, rate);
154
+ });
155
+ Object.entries(metrics.processingTimeDistribution).forEach(([bucket, count]) => {
156
+ const [lower, upper] = bucket.split('-').map(Number);
157
+ for (let i = 0; i < count; i++) {
158
+ this.processingTimeHistogram.observe(upper || lower);
159
+ }
160
+ });
161
+ Object.entries(metrics.redisCommandLatencies).forEach(([command, latency]) => {
162
+ this.redisCommandLatency.set({ command }, latency);
163
+ });
164
+ Object.entries(metrics.consumerLag).forEach(([consumerGroup, lag]) => {
165
+ this.consumerLag.set({ consumer_group: consumerGroup }, lag);
166
+ });
167
+ }
168
+ /**
169
+ * @param app This needs to be an instance of express or fastify something that supports the express api
170
+ */
171
+ setupEndpoint(app, endPoint = '/metrics') {
172
+ app.get(endPoint, async (_, res) => {
173
+ await this.updateMetrics();
174
+ res.set('Content-Type', this.registry.contentType);
175
+ res.send(await this.registry.metrics());
176
+ });
177
+ }
178
+ }
179
+ exports.PrometheusAdapter = PrometheusAdapter;
@@ -0,0 +1,22 @@
1
+ import { DeadLetterQueue } from '../redis/dlq';
2
+ import { IAggregatedMetrics, IMetricsCollectorConfig, IPerformanceMetrics } from './types';
3
+ export declare class MetricsCollector {
4
+ private redisClient;
5
+ private collectionInterval;
6
+ private retentionPeriod;
7
+ private dlq;
8
+ private metrics;
9
+ private metricsKey;
10
+ constructor(config: IMetricsCollectorConfig, dlq: DeadLetterQueue);
11
+ addMetrics(metrics: IPerformanceMetrics): void;
12
+ private aggregateAndStoreMetrics;
13
+ private aggregateMessageRates;
14
+ private aggregateHistogram;
15
+ private aggregateRedisCommandLatencies;
16
+ private aggregateConsumerLag;
17
+ private calculateAverages;
18
+ private getQueueDepth;
19
+ private storeMetrics;
20
+ getMetrics(startTime: number, endTime: number): Promise<IAggregatedMetrics[]>;
21
+ getLatestMetrics(): Promise<IAggregatedMetrics | null>;
22
+ }
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MetricsCollector = void 0;
4
+ class MetricsCollector {
5
+ constructor(config, dlq) {
6
+ this.metrics = [];
7
+ this.metricsKey = 'publisher:metrics';
8
+ this.redisClient = config.redisClient;
9
+ this.collectionInterval = config.collectionInterval;
10
+ this.retentionPeriod = config.retentionPeriod;
11
+ this.dlq = dlq;
12
+ setInterval(() => this.aggregateAndStoreMetrics(), this.collectionInterval);
13
+ }
14
+ addMetrics(metrics) {
15
+ this.metrics.push(metrics);
16
+ }
17
+ async aggregateAndStoreMetrics() {
18
+ if (this.metrics.length === 0)
19
+ return;
20
+ const queueDepths = await this.getQueueDepth();
21
+ const aggregated = {
22
+ timestamp: Date.now(),
23
+ totalTime: 0,
24
+ redisOperationTime: 0,
25
+ processingTime: 0,
26
+ eventCount: 0,
27
+ subscribeErrorCount: 0,
28
+ publishErrorCount: 0,
29
+ queueDepth: queueDepths.total,
30
+ individualQueueDepts: queueDepths.individual,
31
+ dlqSize: 0,
32
+ dlqRate: 0,
33
+ operationCount: this.metrics.length,
34
+ duplicateEventsCount: 0,
35
+ messageRatePublish: {},
36
+ messageRateSubscribe: {},
37
+ processingTimeDistribution: {},
38
+ redisCommandLatencies: {},
39
+ consumerLag: {},
40
+ };
41
+ for (const metric of this.metrics) {
42
+ aggregated.totalTime += metric.totalTime;
43
+ aggregated.redisOperationTime += metric.redisOperationTime;
44
+ aggregated.processingTime += metric.processingTime;
45
+ aggregated.eventCount += metric.eventCount;
46
+ aggregated.publishErrorCount += metric.publishErrorCount;
47
+ aggregated.subscribeErrorCount += metric.subscribeErrorCount;
48
+ aggregated.duplicateEventsCount += metric.duplicateEventsCount;
49
+ this.aggregateMessageRates(aggregated.messageRatePublish, metric.messageRatePublish);
50
+ this.aggregateMessageRates(aggregated.messageRateSubscribe, metric.messageRateSubscribe);
51
+ this.aggregateHistogram(aggregated.processingTimeDistribution, metric.processingTimeDistribution);
52
+ this.aggregateRedisCommandLatencies(aggregated.redisCommandLatencies, metric.redisCommandLatencies);
53
+ this.aggregateConsumerLag(aggregated.consumerLag, metric.consumerLag);
54
+ }
55
+ // Calculate averages for time-based metrics
56
+ aggregated.totalTime /= aggregated.operationCount;
57
+ aggregated.redisOperationTime /= aggregated.operationCount;
58
+ aggregated.processingTime /= aggregated.operationCount;
59
+ const dlqStats = await this.dlq.getDLQStats();
60
+ aggregated.dlqSize = dlqStats.size;
61
+ aggregated.dlqRate = dlqStats.additionRate;
62
+ this.calculateAverages(aggregated.redisCommandLatencies);
63
+ this.calculateAverages(aggregated.consumerLag);
64
+ await this.storeMetrics(aggregated);
65
+ this.metrics = [];
66
+ }
67
+ aggregateMessageRates(target, source) {
68
+ for (const [eventType, count] of Object.entries(source)) {
69
+ if (!target[eventType])
70
+ target[eventType] = 0;
71
+ target[eventType] += count;
72
+ }
73
+ }
74
+ aggregateHistogram(target, source) {
75
+ for (const [bucket, count] of Object.entries(source)) {
76
+ if (!target[bucket])
77
+ target[bucket] = 0;
78
+ target[bucket] += count;
79
+ }
80
+ }
81
+ aggregateRedisCommandLatencies(target, source) {
82
+ for (const [command, { total, count }] of Object.entries(source)) {
83
+ if (!target[command]) {
84
+ target[command] = { total: 0, count: 0 };
85
+ }
86
+ target[command].total += total;
87
+ target[command].count += count;
88
+ }
89
+ }
90
+ aggregateConsumerLag(target, source) {
91
+ for (const [consumerGroup, { total, count }] of Object.entries(source)) {
92
+ if (!target[consumerGroup]) {
93
+ target[consumerGroup] = { total: 0, count: 0 };
94
+ }
95
+ target[consumerGroup].total += total;
96
+ target[consumerGroup].count += count;
97
+ }
98
+ }
99
+ calculateAverages(data) {
100
+ for (const key in data) {
101
+ if (data[key].count > 0) {
102
+ data[key].total = data[key].total / data[key].count;
103
+ }
104
+ else {
105
+ data[key].total = 0;
106
+ }
107
+ }
108
+ }
109
+ async getQueueDepth() {
110
+ let totalDepth = 0;
111
+ const individualDepths = {};
112
+ let cursor = '0';
113
+ do {
114
+ const [nextCursor, keys] = await this.redisClient.scan(cursor, 'MATCH', `ack:*:cg-*`, 'COUNT', 100);
115
+ cursor = nextCursor;
116
+ if (keys.length > 0) {
117
+ const pipeline = this.redisClient.pipeline();
118
+ keys.forEach((key) => {
119
+ pipeline.xlen(key.slice(4));
120
+ pipeline.zcard(key);
121
+ });
122
+ const results = await pipeline.exec();
123
+ if (!results) {
124
+ continue;
125
+ }
126
+ for (let i = 0; i < results.length; i += 2) {
127
+ const key = keys[i / 2];
128
+ const [streamLengthErr, streamLength] = results[i];
129
+ const [ackCountErr, ackCount] = results[i + 1];
130
+ if (streamLengthErr) {
131
+ console.error(`Error getting length for key: ${streamLengthErr}`);
132
+ continue;
133
+ }
134
+ if (ackCountErr) {
135
+ console.error(`Error getting ack count for key: ${ackCountErr}`);
136
+ continue;
137
+ }
138
+ const queueDepth = Math.max(0, streamLength - ackCount);
139
+ totalDepth += queueDepth;
140
+ individualDepths[key] = queueDepth;
141
+ }
142
+ }
143
+ } while (cursor !== '0');
144
+ return { total: totalDepth, individual: individualDepths };
145
+ }
146
+ async storeMetrics(metrics) {
147
+ const score = metrics.timestamp;
148
+ const member = JSON.stringify(metrics);
149
+ await this.redisClient.zadd(this.metricsKey, score, member);
150
+ // Remove old metrics based on retention period
151
+ const cutoffTime = Date.now() - this.retentionPeriod;
152
+ await this.redisClient.zremrangebyscore(this.metricsKey, 0, cutoffTime);
153
+ }
154
+ async getMetrics(startTime, endTime) {
155
+ const metricsData = await this.redisClient.zrangebyscore(this.metricsKey, startTime, endTime);
156
+ return metricsData.map((data) => JSON.parse(data));
157
+ }
158
+ async getLatestMetrics() {
159
+ const latestMetrics = await this.redisClient.zrevrange(this.metricsKey, 0, 0);
160
+ return latestMetrics.length > 0 ? JSON.parse(latestMetrics[0]) : null;
161
+ }
162
+ }
163
+ exports.MetricsCollector = MetricsCollector;
@@ -0,0 +1,20 @@
1
+ import { IPerformanceMetrics } from './types';
2
+ export declare class MetricsTracker {
3
+ private startTime;
4
+ private metrics;
5
+ constructor();
6
+ startRedisOperation(): void;
7
+ endRedisOperation(): void;
8
+ startProcessing(): void;
9
+ endProcessing(): void;
10
+ incrementEventCount(): void;
11
+ incrementDuplicateEvent(): void;
12
+ incrementErrorCount(type: 'publish' | 'subscribe'): void;
13
+ getMetrics(): IPerformanceMetrics;
14
+ incrementMessageRate(type: 'publish' | 'subscribe', eventType: string): void;
15
+ addProcessingTime(time: number): void;
16
+ private getBucket;
17
+ addRedisCommandLatency(command: string, time: number): void;
18
+ setConsumerLag(consumerGroup: string, lag: number): void;
19
+ reset(): void;
20
+ }
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MetricsTracker = void 0;
4
+ const node_process_1 = require("node:process");
5
+ class MetricsTracker {
6
+ constructor() {
7
+ this.metrics = {
8
+ totalTime: 0,
9
+ redisOperationTime: 0,
10
+ processingTime: 0,
11
+ eventCount: 0,
12
+ publishErrorCount: 0,
13
+ subscribeErrorCount: 0,
14
+ duplicateEventsCount: 0,
15
+ messageRatePublish: {},
16
+ messageRateSubscribe: {},
17
+ processingTimeDistribution: {},
18
+ redisCommandLatencies: {},
19
+ consumerLag: {},
20
+ };
21
+ this.startTime = (0, node_process_1.hrtime)();
22
+ }
23
+ startRedisOperation() {
24
+ this.startTime = (0, node_process_1.hrtime)();
25
+ }
26
+ endRedisOperation() {
27
+ const [seconds, nanoseconds] = (0, node_process_1.hrtime)(this.startTime);
28
+ this.metrics.redisOperationTime += seconds * 1000 + nanoseconds / 1e6;
29
+ }
30
+ startProcessing() {
31
+ this.startTime = (0, node_process_1.hrtime)();
32
+ }
33
+ endProcessing() {
34
+ const [seconds, nanoseconds] = (0, node_process_1.hrtime)(this.startTime);
35
+ this.metrics.processingTime += seconds * 1000 + nanoseconds / 1e6;
36
+ }
37
+ incrementEventCount() {
38
+ this.metrics.eventCount++;
39
+ }
40
+ incrementDuplicateEvent() {
41
+ this.metrics.duplicateEventsCount++;
42
+ }
43
+ incrementErrorCount(type) {
44
+ this.metrics[`${type}ErrorCount`]++;
45
+ }
46
+ getMetrics() {
47
+ const [totalSeconds, totalNanoseconds] = (0, node_process_1.hrtime)(this.startTime);
48
+ this.metrics.totalTime = totalSeconds * 1000 + totalNanoseconds / 1e6;
49
+ return { ...this.metrics };
50
+ }
51
+ incrementMessageRate(type, eventType) {
52
+ const key = `messageRate${type.charAt(0).toUpperCase() + type.slice(1)}`;
53
+ if (!this.metrics[key][eventType]) {
54
+ this.metrics[key][eventType] = 0;
55
+ }
56
+ this.metrics[key][eventType]++;
57
+ }
58
+ addProcessingTime(time) {
59
+ const bucket = this.getBucket(time);
60
+ if (!this.metrics.processingTimeDistribution[bucket]) {
61
+ this.metrics.processingTimeDistribution[bucket] = 0;
62
+ }
63
+ this.metrics.processingTimeDistribution[bucket]++;
64
+ }
65
+ getBucket(time) {
66
+ const buckets = [0, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000];
67
+ for (let i = 0; i < buckets.length; i++) {
68
+ if (time <= buckets[i]) {
69
+ return i === 0 ? `0-${buckets[i]}` : `${buckets[i - 1] + 1}-${buckets[i]}`;
70
+ }
71
+ }
72
+ return `>${buckets[buckets.length - 1]}`;
73
+ }
74
+ addRedisCommandLatency(command, time) {
75
+ if (!this.metrics.redisCommandLatencies[command]) {
76
+ this.metrics.redisCommandLatencies[command] = { total: 0, count: 0 };
77
+ }
78
+ this.metrics.redisCommandLatencies[command].total += time;
79
+ this.metrics.redisCommandLatencies[command].count++;
80
+ }
81
+ setConsumerLag(consumerGroup, lag) {
82
+ if (!this.metrics.consumerLag[consumerGroup]) {
83
+ this.metrics.consumerLag[consumerGroup] = { total: 0, count: 0 };
84
+ }
85
+ this.metrics.consumerLag[consumerGroup].total += lag;
86
+ this.metrics.consumerLag[consumerGroup].count++;
87
+ }
88
+ reset() {
89
+ this.startTime = (0, node_process_1.hrtime)();
90
+ this.metrics = {
91
+ totalTime: 0,
92
+ redisOperationTime: 0,
93
+ processingTime: 0,
94
+ eventCount: 0,
95
+ publishErrorCount: 0,
96
+ subscribeErrorCount: 0,
97
+ duplicateEventsCount: 0,
98
+ messageRatePublish: {},
99
+ messageRateSubscribe: {},
100
+ processingTimeDistribution: {},
101
+ redisCommandLatencies: {},
102
+ consumerLag: {},
103
+ };
104
+ }
105
+ }
106
+ exports.MetricsTracker = MetricsTracker;
@@ -0,0 +1,58 @@
1
+ import { RedisType } from '../redis/registry';
2
+ export interface IAggregatedMetrics extends IPerformanceMetrics {
3
+ timestamp: number;
4
+ queueDepth: number;
5
+ dlqSize: number;
6
+ dlqRate: number;
7
+ operationCount: number;
8
+ totalTime: number;
9
+ redisOperationTime: number;
10
+ processingTime: number;
11
+ eventCount: number;
12
+ publishErrorCount: number;
13
+ subscribeErrorCount: number;
14
+ individualQueueDepts: IQueueDepths['individual'];
15
+ duplicateEventsCount: number;
16
+ }
17
+ export interface IMetricsCollectorConfig {
18
+ redisClient: RedisType;
19
+ collectionInterval: number;
20
+ retentionPeriod: number;
21
+ }
22
+ export type TQueryableMetrics = Omit<IAggregatedMetrics, 'timestamp' | 'individualQueueDepts' | 'messageRatePublish' | 'messageRateSubscribe' | 'processingTimeDistribution' | 'redisCommandLatencies' | 'consumerLag'>;
23
+ export interface IQueueDepths {
24
+ total: number;
25
+ individual: {
26
+ [key: string]: number;
27
+ };
28
+ }
29
+ export interface IPerformanceMetrics {
30
+ totalTime: number;
31
+ redisOperationTime: number;
32
+ processingTime: number;
33
+ eventCount: number;
34
+ publishErrorCount: number;
35
+ subscribeErrorCount: number;
36
+ duplicateEventsCount: number;
37
+ messageRatePublish: {
38
+ [eventType: string]: number;
39
+ };
40
+ messageRateSubscribe: {
41
+ [eventType: string]: number;
42
+ };
43
+ processingTimeDistribution: {
44
+ [bucket: string]: number;
45
+ };
46
+ redisCommandLatencies: {
47
+ [command: string]: {
48
+ total: number;
49
+ count: number;
50
+ };
51
+ };
52
+ consumerLag: {
53
+ [consumerGroup: string]: {
54
+ total: number;
55
+ count: number;
56
+ };
57
+ };
58
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,29 @@
1
+ /// <reference types="node" />
2
+ import { EventEmitter } from 'events';
3
+ import { RedisType } from '../redis/registry';
4
+ import { EventData, IStreamsConfig } from '../redis/types';
5
+ export declare enum CircuitState {
6
+ CLOSED = 0,
7
+ OPEN = 1,
8
+ HALF_OPEN = 2
9
+ }
10
+ export declare class CircuitBreaker extends EventEmitter {
11
+ private readonly config;
12
+ private state;
13
+ private failureCount;
14
+ private successCount;
15
+ private lastStateChange;
16
+ private readonly redisClient;
17
+ constructor(config: IStreamsConfig['circuitBreaker'], redisClient: RedisType);
18
+ recordSuccess(): Promise<void>;
19
+ recordFailure(): Promise<void>;
20
+ isAllowed(): Promise<boolean>;
21
+ private openCircuit;
22
+ private closeCircuit;
23
+ private halfOpenCircuit;
24
+ getState(): CircuitState;
25
+ storeEvent(event: EventData<unknown, string>): Promise<void>;
26
+ getStoredEvents(): Promise<EventData<unknown, string>[]>;
27
+ clearStoredEvents(): Promise<void>;
28
+ private startStateCheck;
29
+ }
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CircuitBreaker = exports.CircuitState = void 0;
4
+ const events_1 = require("events");
5
+ var CircuitState;
6
+ (function (CircuitState) {
7
+ CircuitState[CircuitState["CLOSED"] = 0] = "CLOSED";
8
+ CircuitState[CircuitState["OPEN"] = 1] = "OPEN";
9
+ CircuitState[CircuitState["HALF_OPEN"] = 2] = "HALF_OPEN";
10
+ })(CircuitState || (exports.CircuitState = CircuitState = {}));
11
+ class CircuitBreaker extends events_1.EventEmitter {
12
+ constructor(config, redisClient) {
13
+ super();
14
+ this.config = config;
15
+ this.state = CircuitState.CLOSED;
16
+ this.failureCount = 0;
17
+ this.successCount = 0;
18
+ this.lastStateChange = Date.now();
19
+ this.redisClient = redisClient;
20
+ this.startStateCheck();
21
+ }
22
+ async recordSuccess() {
23
+ this.successCount++;
24
+ if (this.state === CircuitState.HALF_OPEN) {
25
+ if (this.successCount >= this.config.halfOpenStateMaxAttempts) {
26
+ await this.closeCircuit();
27
+ }
28
+ }
29
+ }
30
+ async recordFailure() {
31
+ this.failureCount++;
32
+ if (this.state === CircuitState.CLOSED) {
33
+ const totalAttempts = this.failureCount + this.successCount;
34
+ const failureRate = (this.failureCount / totalAttempts) * 100;
35
+ if (this.failureCount >= this.config.errorThreshold || failureRate >= this.config.errorThresholdPercentage) {
36
+ await this.openCircuit();
37
+ }
38
+ }
39
+ else if (this.state === CircuitState.HALF_OPEN) {
40
+ await this.openCircuit();
41
+ }
42
+ }
43
+ async isAllowed() {
44
+ const now = Date.now();
45
+ if (this.state === CircuitState.OPEN) {
46
+ if (now - this.lastStateChange >= this.config.openStateDuration) {
47
+ await this.halfOpenCircuit();
48
+ }
49
+ else {
50
+ return false;
51
+ }
52
+ }
53
+ return true;
54
+ }
55
+ openCircuit() {
56
+ if (this.state !== CircuitState.OPEN) {
57
+ this.state = CircuitState.OPEN;
58
+ this.lastStateChange = Date.now();
59
+ this.emit('stateChange', CircuitState.OPEN);
60
+ }
61
+ }
62
+ closeCircuit() {
63
+ if (this.state !== CircuitState.CLOSED) {
64
+ this.state = CircuitState.CLOSED;
65
+ this.lastStateChange = Date.now();
66
+ this.failureCount = 0;
67
+ this.successCount = 0;
68
+ this.emit('stateChange', CircuitState.CLOSED);
69
+ }
70
+ }
71
+ halfOpenCircuit() {
72
+ if (this.state !== CircuitState.HALF_OPEN) {
73
+ this.state = CircuitState.HALF_OPEN;
74
+ this.lastStateChange = Date.now();
75
+ this.failureCount = 0;
76
+ this.successCount = 0;
77
+ this.emit('stateChange', CircuitState.HALF_OPEN);
78
+ }
79
+ }
80
+ getState() {
81
+ return this.state;
82
+ }
83
+ async storeEvent(event) {
84
+ const key = `circuit_breaker:stored_events`;
85
+ await this.redisClient.lpush(key, JSON.stringify(event));
86
+ await this.redisClient.ltrim(key, 0, this.config.maxStoredEvents - 1);
87
+ }
88
+ async getStoredEvents() {
89
+ const key = `circuit_breaker:stored_events`;
90
+ const events = await this.redisClient.lrange(key, 0, -1);
91
+ return events.map((event) => JSON.parse(event));
92
+ }
93
+ async clearStoredEvents() {
94
+ const key = `circuit_breaker:stored_events`;
95
+ await this.redisClient.del(key);
96
+ }
97
+ startStateCheck() {
98
+ setInterval(() => {
99
+ this.isAllowed();
100
+ }, 2000);
101
+ }
102
+ }
103
+ exports.CircuitBreaker = CircuitBreaker;