@verdeect/kit 0.0.0-stage → 1.0.0-rc.2
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.
- package/CHANGELOG.md +138 -0
- package/README.md +804 -2
- package/alerts/verdeect-kit.rules.yml +133 -0
- package/dist/cli/commands/crypto.d.ts +9 -0
- package/dist/cli/commands/crypto.js +87 -0
- package/dist/cli/commands/db.d.ts +11 -0
- package/dist/cli/commands/db.js +151 -0
- package/dist/cli/commands/openapi.d.ts +9 -0
- package/dist/cli/commands/openapi.js +144 -0
- package/dist/cli/commands/rabbitmq.d.ts +11 -0
- package/dist/cli/commands/rabbitmq.js +124 -0
- package/dist/cli/commands/redis.d.ts +11 -0
- package/dist/cli/commands/redis.js +119 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +31 -0
- package/dist/config/cli-schemas.d.ts +29 -0
- package/dist/config/cli-schemas.js +15 -0
- package/dist/config/define-env.d.ts +21 -0
- package/dist/config/define-env.js +43 -0
- package/dist/config/fragments.d.ts +105 -0
- package/dist/config/fragments.js +86 -0
- package/dist/config/helpers.d.ts +26 -0
- package/dist/config/helpers.js +62 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +7 -0
- package/dist/config/kit-config.module.d.ts +20 -0
- package/dist/config/kit-config.module.js +23 -0
- package/dist/config/parse-env.d.ts +14 -0
- package/dist/config/parse-env.js +28 -0
- package/dist/crypto/cipher.d.ts +61 -0
- package/dist/crypto/cipher.js +136 -0
- package/dist/crypto/index.d.ts +2 -0
- package/dist/crypto/index.js +2 -0
- package/dist/database/batches.d.ts +42 -0
- package/dist/database/batches.js +49 -0
- package/dist/database/db.d.ts +93 -0
- package/dist/database/db.js +234 -0
- package/dist/database/if-newer.d.ts +9 -0
- package/dist/database/if-newer.js +15 -0
- package/dist/database/index.d.ts +17 -0
- package/dist/database/index.js +13 -0
- package/dist/database/kit-database.module.d.ts +47 -0
- package/dist/database/kit-database.module.js +181 -0
- package/dist/database/migrations.d.ts +42 -0
- package/dist/database/migrations.js +142 -0
- package/dist/database/pg.d.ts +20 -0
- package/dist/database/pg.js +19 -0
- package/dist/database/prepare-database.d.ts +39 -0
- package/dist/database/prepare-database.js +75 -0
- package/dist/database/run-exclusive.d.ts +19 -0
- package/dist/database/run-exclusive.js +62 -0
- package/dist/database/tenant.d.ts +34 -0
- package/dist/database/tenant.js +28 -0
- package/dist/http/api/api.decorator.d.ts +18 -0
- package/dist/http/api/api.decorator.js +9 -0
- package/dist/http/api/openapi-document.d.ts +40 -0
- package/dist/http/api/openapi-document.js +452 -0
- package/dist/http/bootstrap-service.d.ts +33 -0
- package/dist/http/bootstrap-service.js +107 -0
- package/dist/http/create-http-app.d.ts +38 -0
- package/dist/http/create-http-app.js +137 -0
- package/dist/http/errors.d.ts +118 -0
- package/dist/http/errors.js +94 -0
- package/dist/http/index.d.ts +12 -0
- package/dist/http/index.js +8 -0
- package/dist/http/kit-exception.filter.d.ts +18 -0
- package/dist/http/kit-exception.filter.js +151 -0
- package/dist/http/kit-http-core.module.d.ts +19 -0
- package/dist/http/kit-http-core.module.js +122 -0
- package/dist/http/pagination.d.ts +30 -0
- package/dist/http/pagination.js +73 -0
- package/dist/http/zod-validation.pipe.d.ts +15 -0
- package/dist/http/zod-validation.pipe.js +47 -0
- package/dist/idempotency/idempotency.interceptor.d.ts +55 -0
- package/dist/idempotency/idempotency.interceptor.js +317 -0
- package/dist/idempotency/idempotent.decorator.d.ts +7 -0
- package/dist/idempotency/idempotent.decorator.js +16 -0
- package/dist/idempotency/index.d.ts +7 -0
- package/dist/idempotency/index.js +6 -0
- package/dist/idempotency/kit-idempotency.module.d.ts +47 -0
- package/dist/idempotency/kit-idempotency.module.js +173 -0
- package/dist/idempotency/peer.d.ts +1 -0
- package/dist/idempotency/peer.js +4 -0
- package/dist/idempotency/table.d.ts +203 -0
- package/dist/idempotency/table.js +86 -0
- package/dist/internal/cleanup-result.d.ts +7 -0
- package/dist/internal/cleanup-result.js +12 -0
- package/dist/internal/ids.d.ts +13 -0
- package/dist/internal/ids.js +41 -0
- package/dist/internal/metadata.d.ts +12 -0
- package/dist/internal/metadata.js +3 -0
- package/dist/internal/outbox-policy.d.ts +8 -0
- package/dist/internal/outbox-policy.js +10 -0
- package/dist/internal/peer.d.ts +16 -0
- package/dist/internal/peer.js +56 -0
- package/dist/internal/pg-errors.d.ts +21 -0
- package/dist/internal/pg-errors.js +115 -0
- package/dist/internal/preparsed-env.d.ts +4 -0
- package/dist/internal/preparsed-env.js +14 -0
- package/dist/internal/rabbitmq-definitions.d.ts +85 -0
- package/dist/internal/rabbitmq-definitions.js +73 -0
- package/dist/internal/redis-acl.d.ts +21 -0
- package/dist/internal/redis-acl.js +30 -0
- package/dist/internal/service-name.d.ts +4 -0
- package/dist/internal/service-name.js +10 -0
- package/dist/internal/sql-identifier.d.ts +5 -0
- package/dist/internal/sql-identifier.js +12 -0
- package/dist/internal/tokens.d.ts +10 -0
- package/dist/internal/tokens.js +12 -0
- package/dist/internal/tx-context.d.ts +5 -0
- package/dist/internal/tx-context.js +9 -0
- package/dist/messaging/cleanup.d.ts +49 -0
- package/dist/messaging/cleanup.js +129 -0
- package/dist/messaging/config.d.ts +43 -0
- package/dist/messaging/config.js +8 -0
- package/dist/messaging/define-event.d.ts +29 -0
- package/dist/messaging/define-event.js +38 -0
- package/dist/messaging/envelope.d.ts +66 -0
- package/dist/messaging/envelope.js +49 -0
- package/dist/messaging/errors.d.ts +45 -0
- package/dist/messaging/errors.js +63 -0
- package/dist/messaging/inbox.d.ts +4 -0
- package/dist/messaging/inbox.js +9 -0
- package/dist/messaging/index.d.ts +13 -0
- package/dist/messaging/index.js +10 -0
- package/dist/messaging/kit-messaging.module.d.ts +39 -0
- package/dist/messaging/kit-messaging.module.js +309 -0
- package/dist/messaging/outbox-relay.d.ts +67 -0
- package/dist/messaging/outbox-relay.js +260 -0
- package/dist/messaging/outbox-store.d.ts +75 -0
- package/dist/messaging/outbox-store.js +126 -0
- package/dist/messaging/peer.d.ts +1 -0
- package/dist/messaging/peer.js +5 -0
- package/dist/messaging/rabbit-connection.d.ts +144 -0
- package/dist/messaging/rabbit-connection.js +472 -0
- package/dist/messaging/redelivery.d.ts +6 -0
- package/dist/messaging/redelivery.js +17 -0
- package/dist/messaging/subscriber.d.ts +102 -0
- package/dist/messaging/subscriber.js +412 -0
- package/dist/messaging/tables.d.ts +315 -0
- package/dist/messaging/tables.js +72 -0
- package/dist/messaging/topology.d.ts +56 -0
- package/dist/messaging/topology.js +56 -0
- package/dist/observability/fail-startup.d.ts +14 -0
- package/dist/observability/fail-startup.js +51 -0
- package/dist/observability/health-registry.d.ts +34 -0
- package/dist/observability/health-registry.js +81 -0
- package/dist/observability/index.d.ts +16 -0
- package/dist/observability/index.js +10 -0
- package/dist/observability/lifecycle.d.ts +52 -0
- package/dist/observability/lifecycle.js +151 -0
- package/dist/observability/logger-options.d.ts +54 -0
- package/dist/observability/logger-options.js +91 -0
- package/dist/observability/metrics-server.d.ts +33 -0
- package/dist/observability/metrics-server.js +108 -0
- package/dist/observability/metrics.d.ts +30 -0
- package/dist/observability/metrics.js +53 -0
- package/dist/observability/redaction.d.ts +23 -0
- package/dist/observability/redaction.js +65 -0
- package/dist/observability/sanitize-error.d.ts +47 -0
- package/dist/observability/sanitize-error.js +194 -0
- package/dist/observability/sentry.d.ts +64 -0
- package/dist/observability/sentry.js +233 -0
- package/dist/redis/channels.d.ts +34 -0
- package/dist/redis/channels.js +128 -0
- package/dist/redis/client.d.ts +42 -0
- package/dist/redis/client.js +85 -0
- package/dist/redis/index.d.ts +7 -0
- package/dist/redis/index.js +7 -0
- package/dist/redis/kit-redis.module.d.ts +18 -0
- package/dist/redis/kit-redis.module.js +131 -0
- package/dist/redis/peer.d.ts +1 -0
- package/dist/redis/peer.js +4 -0
- package/dist/redis/rate-limiter.d.ts +49 -0
- package/dist/redis/rate-limiter.js +139 -0
- package/dist/testing/count-queries.d.ts +30 -0
- package/dist/testing/count-queries.js +58 -0
- package/dist/testing/create-test-app.d.ts +26 -0
- package/dist/testing/create-test-app.js +32 -0
- package/dist/testing/expect-matches-api.d.ts +8 -0
- package/dist/testing/expect-matches-api.js +49 -0
- package/dist/testing/expect-problem.d.ts +24 -0
- package/dist/testing/expect-problem.js +23 -0
- package/dist/testing/images.d.ts +10 -0
- package/dist/testing/images.js +14 -0
- package/dist/testing/index.d.ts +23 -0
- package/dist/testing/index.js +17 -0
- package/dist/testing/launch-command.d.ts +52 -0
- package/dist/testing/launch-command.js +510 -0
- package/dist/testing/outbox-coverage.d.ts +26 -0
- package/dist/testing/outbox-coverage.js +98 -0
- package/dist/testing/rabbitmq-admin.d.ts +81 -0
- package/dist/testing/rabbitmq-admin.js +146 -0
- package/dist/testing/redis-worker.d.ts +10 -0
- package/dist/testing/redis-worker.js +15 -0
- package/dist/testing/repo-conventions.d.ts +10 -0
- package/dist/testing/repo-conventions.js +524 -0
- package/dist/testing/repo-files.d.ts +113 -0
- package/dist/testing/repo-files.js +401 -0
- package/dist/testing/scratch-database.d.ts +19 -0
- package/dist/testing/scratch-database.js +75 -0
- package/dist/testing/start-test-stack.d.ts +85 -0
- package/dist/testing/start-test-stack.js +342 -0
- package/dist/testing/tcp-proxy.d.ts +40 -0
- package/dist/testing/tcp-proxy.js +164 -0
- package/dist/testing/tenant-isolation.d.ts +31 -0
- package/dist/testing/tenant-isolation.js +205 -0
- package/dist/testing/test-events.d.ts +37 -0
- package/dist/testing/test-events.js +150 -0
- package/dist/testing/timeout-scale.d.ts +4 -0
- package/dist/testing/timeout-scale.js +17 -0
- package/dist/testing/timing-guard.d.ts +22 -0
- package/dist/testing/timing-guard.js +54 -0
- package/dist/testing/worker-scope.d.ts +31 -0
- package/dist/testing/worker-scope.js +68 -0
- package/package.json +177 -4
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { ChannelWrapper, Options } from 'amqp-connection-manager';
|
|
2
|
+
import type { ConfirmChannel, ConsumeMessage, Message, Options as AmqpOptions } from 'amqplib';
|
|
3
|
+
/** Опции канала. */
|
|
4
|
+
export interface ChannelOptions {
|
|
5
|
+
/** Имя канала (уникально в соединении). */
|
|
6
|
+
readonly name: string;
|
|
7
|
+
/** Канал с подтверждениями брокера. */
|
|
8
|
+
readonly confirm: boolean;
|
|
9
|
+
/** Таймаут подтверждения публикации, мс. */
|
|
10
|
+
readonly publishTimeout?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Настройка канала после проверки exchange; вызывается при каждом (пере)подключении. `MessagingStartupError` —
|
|
13
|
+
* фатальная ошибка: канал не повторяется.
|
|
14
|
+
*/
|
|
15
|
+
readonly setup?: (channel: ConfirmChannel) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/** Логгер фоновых операций /messaging: pino экземпляра (`service`, `replica` в `base`) или адаптер логгера Nest. */
|
|
18
|
+
export interface MessagingLog {
|
|
19
|
+
/** Уровень debug. */
|
|
20
|
+
debug(fields: object, message: string): void;
|
|
21
|
+
/** Уровень info. */
|
|
22
|
+
info(fields: object, message: string): void;
|
|
23
|
+
/** Уровень warn. */
|
|
24
|
+
warn(fields: object, message: string): void;
|
|
25
|
+
/** Уровень error. */
|
|
26
|
+
error(fields: object, message: string): void;
|
|
27
|
+
}
|
|
28
|
+
/** Адаптер логгера Nest (приложение без KIT_LOGGER). */
|
|
29
|
+
export declare function nestLog(context: string): MessagingLog;
|
|
30
|
+
/** Опции соединения. */
|
|
31
|
+
export interface RabbitConnectionOptions {
|
|
32
|
+
/** Логгер; по умолчанию — логгер Nest. */
|
|
33
|
+
readonly log?: MessagingLog;
|
|
34
|
+
/** RABBITMQ_URL. */
|
|
35
|
+
readonly url: string;
|
|
36
|
+
/** Пауза переподключения и повтора настройки канала, с (RABBITMQ_RETRY_SECONDS). */
|
|
37
|
+
readonly retrySeconds: number;
|
|
38
|
+
/** Общий срок обработчиков остановки, мс (handlersDeadlineMs). */
|
|
39
|
+
readonly deadlineMs: number;
|
|
40
|
+
}
|
|
41
|
+
/** Первый исход настройки канала после createChannel. */
|
|
42
|
+
export type ChannelOutcome = {
|
|
43
|
+
readonly kind: 'ready';
|
|
44
|
+
} | {
|
|
45
|
+
readonly kind: 'fatal';
|
|
46
|
+
readonly error: Error;
|
|
47
|
+
} | {
|
|
48
|
+
readonly kind: 'failed';
|
|
49
|
+
readonly error: Error;
|
|
50
|
+
};
|
|
51
|
+
/** host:port адреса брокера, без учётных данных и vhost. */
|
|
52
|
+
export declare function brokerEndpoint(url: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* Канал relay и подписчика: ссылка не меняется, когда RabbitConnection заменяет ChannelWrapper после сбоя настройки;
|
|
55
|
+
* потребители переносятся. Delivery tag действует только в канале amqplib, выдавшем доставку: каждое открытие —
|
|
56
|
+
* новое поколение; ack/nack прошлого поколения не отправляются (чужой tag дал бы 406 и закрыл новый канал).
|
|
57
|
+
*/
|
|
58
|
+
export declare class ManagedChannel {
|
|
59
|
+
readonly name: string;
|
|
60
|
+
private current;
|
|
61
|
+
private readonly onClose;
|
|
62
|
+
private readonly logger;
|
|
63
|
+
private readonly consumers;
|
|
64
|
+
private readonly deliveries;
|
|
65
|
+
private generation;
|
|
66
|
+
private closed;
|
|
67
|
+
constructor(name: string, current: ChannelWrapper, onClose: () => void, logger?: MessagingLog);
|
|
68
|
+
/** Открыт новый канал amqplib (в начале setup, до переподписки потребителей). */
|
|
69
|
+
markOpened(): void;
|
|
70
|
+
/** Доставка пришла через текущий канал amqplib (или не отслеживается). */
|
|
71
|
+
isCurrent(message: Message): boolean;
|
|
72
|
+
/** Публикация (на confirm-канале — Promise подтверждения). */
|
|
73
|
+
publish(exchange: string, routingKey: string, content: Buffer, options?: Options.Publish): Promise<boolean>;
|
|
74
|
+
/** Публикация в очередь. */
|
|
75
|
+
sendToQueue(queue: string, content: Buffer, options?: Options.Publish): Promise<boolean>;
|
|
76
|
+
/** Потребитель; переносится на следующий wrapper. */
|
|
77
|
+
consume(queue: string, onMessage: (msg: ConsumeMessage) => void, options?: AmqpOptions.Consume): Promise<unknown>;
|
|
78
|
+
/** ack; устаревшая доставка пропускается. */
|
|
79
|
+
ack(message: Message): void;
|
|
80
|
+
/** nack; устаревшая доставка пропускается. */
|
|
81
|
+
nack(message: Message, allUpTo?: boolean, requeue?: boolean): void;
|
|
82
|
+
/** Снять всех потребителей (basic.cancel): канал открыт — ack/nack доставок в работе ещё уходят. */
|
|
83
|
+
cancelConsumers(): Promise<void>;
|
|
84
|
+
/** Не дольше CHANNEL_CLOSE_TIMEOUT_MS: брокер молчит — канал бросается, его закроет закрытие соединения. */
|
|
85
|
+
close(): Promise<void>;
|
|
86
|
+
/** Новый wrapper вместо старого; false — канал уже закрыт владельцем. */
|
|
87
|
+
replace(next: ChannelWrapper): Promise<boolean>;
|
|
88
|
+
private isStale;
|
|
89
|
+
}
|
|
90
|
+
/** Соединение сервиса с брокером: одно на relay и подписчика, ленивое (`start`), переподключение и heartbeat. */
|
|
91
|
+
export declare class RabbitConnection {
|
|
92
|
+
private readonly options;
|
|
93
|
+
private readonly logger;
|
|
94
|
+
private manager;
|
|
95
|
+
private starting;
|
|
96
|
+
private first;
|
|
97
|
+
private readonly endpoint;
|
|
98
|
+
private readonly channels;
|
|
99
|
+
private readonly readyListeners;
|
|
100
|
+
/** Ошибка exchange уже отправлена (сбрасывается, когда все каналы настроены). */
|
|
101
|
+
private exchangeMissingReported;
|
|
102
|
+
/** Каналы, чьё исключение обработчика готовности уже отправлено. */
|
|
103
|
+
private readonly readyFailureReported;
|
|
104
|
+
private retryTimer;
|
|
105
|
+
/** Остановка началась: таймер повтора больше не взводится. */
|
|
106
|
+
private stopped;
|
|
107
|
+
private stopDeadline;
|
|
108
|
+
constructor(options: RabbitConnectionOptions);
|
|
109
|
+
/** Загружает amqp-connection-manager и запускает соединение в фоне (без ожидания подключения); идемпотентный. */
|
|
110
|
+
start(): Promise<void>;
|
|
111
|
+
/** Первый исход подключения после `start`: `connected` или `failed`. */
|
|
112
|
+
firstAttempt(): Promise<'connected' | 'failed'>;
|
|
113
|
+
/** Соединение установлено. */
|
|
114
|
+
isConnected(): boolean;
|
|
115
|
+
/** Все созданные каналы настроены; без каналов — `true` (проба отражает только соединение). */
|
|
116
|
+
topologyReady(): boolean;
|
|
117
|
+
/** Канал с этим именем настроен. */
|
|
118
|
+
isChannelReady(name: string): boolean;
|
|
119
|
+
/** Подписка на каждую успешную настройку канала; возвращает отписку. */
|
|
120
|
+
onChannelReady(name: string, listener: () => void): () => void;
|
|
121
|
+
/**
|
|
122
|
+
* Общий срок остановки обмена с брокером: отсчёт — с первого вызова в beforeApplicationShutdown (relay,
|
|
123
|
+
* подписчик), resolve(false) через `deadlineMs`. Nest вызывает хуки по очереди — у каждого свой срок сложился бы.
|
|
124
|
+
*/
|
|
125
|
+
shutdownDeadline(): Promise<false>;
|
|
126
|
+
/** Первый исход настройки канала: готов, фатальная ошибка `setup` или прочая ошибка стадии. */
|
|
127
|
+
channelOutcome(name: string): Promise<ChannelOutcome>;
|
|
128
|
+
/** Канал; `start()` должен быть завершён. */
|
|
129
|
+
createChannel(options: ChannelOptions): ManagedChannel;
|
|
130
|
+
/** Шаг 4 остановки: закрытие соединения не дольше CONNECTION_CLOSE_TIMEOUT_MS. */
|
|
131
|
+
onApplicationShutdown(): Promise<void>;
|
|
132
|
+
private open;
|
|
133
|
+
private openWrapper;
|
|
134
|
+
private runStage;
|
|
135
|
+
private markAllNotReady;
|
|
136
|
+
private onSetupDone;
|
|
137
|
+
/** Брокер закрыл настроенный канал: ChannelWrapper сам его не пересоздаст — заменяем, как после сбоя настройки. */
|
|
138
|
+
private onClosedByBroker;
|
|
139
|
+
private onSetupFailed;
|
|
140
|
+
private reportStageFailure;
|
|
141
|
+
/** Один таймер на соединение; повтор заменяет только каналы в ошибке (кроме фатальных). */
|
|
142
|
+
private scheduleRetry;
|
|
143
|
+
private retryFailedChannels;
|
|
144
|
+
}
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
// RabbitConnection, ManagedChannel (prd.md §3.4 «соединение — основа O», AC-5.4, AC-5.11, AC-5.19). Эталон — O
|
|
2
|
+
// rabbit-connection.ts (P — без cancelConsumers/stopped). Отличия: фабрика модуля вместо DI, amqp-connection-manager —
|
|
3
|
+
// loadPeer в start() (без top-level await, AC-1.2), срок остановки из SHUTDOWN_TIMEOUT_MS (П9), captureError пакета,
|
|
4
|
+
// фатальная ошибка настройки (MessagingStartupError, 406 TTL .retry) без повторов (AC-5.19, П8), topologyReady без
|
|
5
|
+
// каналов — true, первый исход подключения и настройки канала (firstAttempt, channelOutcome). Exchange
|
|
6
|
+
// verdeect.events — только checkExchange. В лог идёт только host:port: RABBITMQ_URL содержит пароль.
|
|
7
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
8
|
+
import { Logger } from '@nestjs/common';
|
|
9
|
+
import { loadPeer } from '../internal/peer.js';
|
|
10
|
+
import { sanitizeError } from '../observability/sanitize-error.js';
|
|
11
|
+
import { captureError } from '../observability/sentry.js';
|
|
12
|
+
import { MessagingStartupError } from './errors.js';
|
|
13
|
+
import { CHANNEL_CLOSE_TIMEOUT_MS, CONNECTION_CLOSE_TIMEOUT_MS, EVENTS_EXCHANGE, HEARTBEAT_SECONDS, } from './topology.js';
|
|
14
|
+
/** Адаптер логгера Nest (приложение без KIT_LOGGER). */
|
|
15
|
+
export function nestLog(context) {
|
|
16
|
+
const logger = new Logger(context);
|
|
17
|
+
return {
|
|
18
|
+
debug: (fields, message) => logger.debug(fields, message),
|
|
19
|
+
info: (fields, message) => logger.log(fields, message),
|
|
20
|
+
warn: (fields, message) => logger.warn(fields, message),
|
|
21
|
+
error: (fields, message) => logger.error(fields, message),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** host:port адреса брокера, без учётных данных и vhost. */
|
|
25
|
+
export function brokerEndpoint(url) {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(url);
|
|
28
|
+
// URL не хранит порт по умолчанию: amqps — 5671, amqp — 5672.
|
|
29
|
+
const port = parsed.port || (parsed.protocol === 'amqps:' ? '5671' : '5672');
|
|
30
|
+
return `${parsed.hostname}:${port}`;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return '(invalid url)';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** true — операция завершилась (успехом или ошибкой) не позже срока. */
|
|
37
|
+
async function settlesWithin(operation, timeoutMs) {
|
|
38
|
+
return Promise.race([
|
|
39
|
+
operation.then(() => true, () => true),
|
|
40
|
+
sleep(timeoutMs, false, { ref: false }),
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
/** Канал закрыт разрывом соединения: менеджер переподключится и повторит setup сам. */
|
|
44
|
+
function isConnectionLoss(error) {
|
|
45
|
+
return error instanceof Error && error.name === 'IllegalOperationError';
|
|
46
|
+
}
|
|
47
|
+
function asError(error) {
|
|
48
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Канал relay и подписчика: ссылка не меняется, когда RabbitConnection заменяет ChannelWrapper после сбоя настройки;
|
|
52
|
+
* потребители переносятся. Delivery tag действует только в канале amqplib, выдавшем доставку: каждое открытие —
|
|
53
|
+
* новое поколение; ack/nack прошлого поколения не отправляются (чужой tag дал бы 406 и закрыл новый канал).
|
|
54
|
+
*/
|
|
55
|
+
export class ManagedChannel {
|
|
56
|
+
name;
|
|
57
|
+
current;
|
|
58
|
+
onClose;
|
|
59
|
+
logger;
|
|
60
|
+
consumers = [];
|
|
61
|
+
deliveries = new WeakMap();
|
|
62
|
+
generation = 0;
|
|
63
|
+
closed = false;
|
|
64
|
+
constructor(name, current, onClose, logger = nestLog('ManagedChannel')) {
|
|
65
|
+
this.name = name;
|
|
66
|
+
this.current = current;
|
|
67
|
+
this.onClose = onClose;
|
|
68
|
+
this.logger = logger;
|
|
69
|
+
}
|
|
70
|
+
/** Открыт новый канал amqplib (в начале setup, до переподписки потребителей). */
|
|
71
|
+
markOpened() {
|
|
72
|
+
this.generation += 1;
|
|
73
|
+
}
|
|
74
|
+
/** Доставка пришла через текущий канал amqplib (или не отслеживается). */
|
|
75
|
+
isCurrent(message) {
|
|
76
|
+
const generation = this.deliveries.get(message);
|
|
77
|
+
return generation === undefined || generation === this.generation;
|
|
78
|
+
}
|
|
79
|
+
/** Публикация (на confirm-канале — Promise подтверждения). */
|
|
80
|
+
publish(exchange, routingKey, content, options) {
|
|
81
|
+
return this.current.publish(exchange, routingKey, content, options);
|
|
82
|
+
}
|
|
83
|
+
/** Публикация в очередь. */
|
|
84
|
+
sendToQueue(queue, content, options) {
|
|
85
|
+
return this.current.sendToQueue(queue, content, options);
|
|
86
|
+
}
|
|
87
|
+
/** Потребитель; переносится на следующий wrapper. */
|
|
88
|
+
consume(queue, onMessage, options) {
|
|
89
|
+
const tracked = (msg) => {
|
|
90
|
+
this.deliveries.set(msg, this.generation);
|
|
91
|
+
onMessage(msg);
|
|
92
|
+
};
|
|
93
|
+
this.consumers.push({ queue, onMessage: tracked, options });
|
|
94
|
+
return this.current.consume(queue, tracked, options);
|
|
95
|
+
}
|
|
96
|
+
/** ack; устаревшая доставка пропускается. */
|
|
97
|
+
ack(message) {
|
|
98
|
+
if (this.isStale(message, 'ack')) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.current.ack(message);
|
|
102
|
+
}
|
|
103
|
+
/** nack; устаревшая доставка пропускается. */
|
|
104
|
+
nack(message, allUpTo, requeue) {
|
|
105
|
+
if (this.isStale(message, 'nack')) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.current.nack(message, allUpTo, requeue);
|
|
109
|
+
}
|
|
110
|
+
/** Снять всех потребителей (basic.cancel): канал открыт — ack/nack доставок в работе ещё уходят. */
|
|
111
|
+
async cancelConsumers() {
|
|
112
|
+
this.consumers.length = 0;
|
|
113
|
+
await this.current.cancelAll().catch(() => undefined);
|
|
114
|
+
}
|
|
115
|
+
/** Не дольше CHANNEL_CLOSE_TIMEOUT_MS: брокер молчит — канал бросается, его закроет закрытие соединения. */
|
|
116
|
+
async close() {
|
|
117
|
+
this.closed = true;
|
|
118
|
+
this.onClose();
|
|
119
|
+
if (!(await settlesWithin(this.current.close(), CHANNEL_CLOSE_TIMEOUT_MS))) {
|
|
120
|
+
this.logger.warn({ channel: this.name, timeoutMs: CHANNEL_CLOSE_TIMEOUT_MS }, 'rabbitmq: брокер не ответил на закрытие канала, канал брошен');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Новый wrapper вместо старого; false — канал уже закрыт владельцем. */
|
|
124
|
+
async replace(next) {
|
|
125
|
+
if (this.closed) {
|
|
126
|
+
await next.close();
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const previous = this.current;
|
|
130
|
+
this.current = next;
|
|
131
|
+
await previous.close().catch(() => undefined);
|
|
132
|
+
for (const consumer of this.consumers) {
|
|
133
|
+
// Без соединения consume только регистрирует потребителя: он подключится при connect.
|
|
134
|
+
void next
|
|
135
|
+
.consume(consumer.queue, consumer.onMessage, consumer.options)
|
|
136
|
+
.catch(() => undefined);
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
isStale(message, action) {
|
|
141
|
+
if (this.isCurrent(message)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
this.logger.debug({ channel: this.name, action, generation: this.deliveries.get(message) }, 'rabbitmq: подтверждение устаревшей доставки пропущено');
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Соединение сервиса с брокером: одно на relay и подписчика, ленивое (`start`), переподключение и heartbeat. */
|
|
149
|
+
export class RabbitConnection {
|
|
150
|
+
options;
|
|
151
|
+
logger;
|
|
152
|
+
manager = null;
|
|
153
|
+
starting = null;
|
|
154
|
+
first = null;
|
|
155
|
+
endpoint;
|
|
156
|
+
channels = new Map();
|
|
157
|
+
readyListeners = new Map();
|
|
158
|
+
/** Ошибка exchange уже отправлена (сбрасывается, когда все каналы настроены). */
|
|
159
|
+
exchangeMissingReported = false;
|
|
160
|
+
/** Каналы, чьё исключение обработчика готовности уже отправлено. */
|
|
161
|
+
readyFailureReported = new Set();
|
|
162
|
+
retryTimer;
|
|
163
|
+
/** Остановка началась: таймер повтора больше не взводится. */
|
|
164
|
+
stopped = false;
|
|
165
|
+
stopDeadline = null;
|
|
166
|
+
constructor(options) {
|
|
167
|
+
this.options = options;
|
|
168
|
+
this.endpoint = brokerEndpoint(options.url);
|
|
169
|
+
this.logger = options.log ?? nestLog('RabbitConnection');
|
|
170
|
+
}
|
|
171
|
+
/** Загружает amqp-connection-manager и запускает соединение в фоне (без ожидания подключения); идемпотентный. */
|
|
172
|
+
start() {
|
|
173
|
+
this.starting ??= this.open();
|
|
174
|
+
return this.starting;
|
|
175
|
+
}
|
|
176
|
+
/** Первый исход подключения после `start`: `connected` или `failed`. */
|
|
177
|
+
firstAttempt() {
|
|
178
|
+
if (this.first === null) {
|
|
179
|
+
throw new Error('RabbitConnection: start() не вызван');
|
|
180
|
+
}
|
|
181
|
+
return this.first;
|
|
182
|
+
}
|
|
183
|
+
/** Соединение установлено. */
|
|
184
|
+
isConnected() {
|
|
185
|
+
return this.manager?.isConnected() ?? false;
|
|
186
|
+
}
|
|
187
|
+
/** Все созданные каналы настроены; без каналов — `true` (проба отражает только соединение). */
|
|
188
|
+
topologyReady() {
|
|
189
|
+
return [...this.channels.values()].every((state) => state.ready);
|
|
190
|
+
}
|
|
191
|
+
/** Канал с этим именем настроен. */
|
|
192
|
+
isChannelReady(name) {
|
|
193
|
+
return this.channels.get(name)?.ready ?? false;
|
|
194
|
+
}
|
|
195
|
+
/** Подписка на каждую успешную настройку канала; возвращает отписку. */
|
|
196
|
+
onChannelReady(name, listener) {
|
|
197
|
+
let listeners = this.readyListeners.get(name);
|
|
198
|
+
if (listeners === undefined) {
|
|
199
|
+
listeners = new Set();
|
|
200
|
+
this.readyListeners.set(name, listeners);
|
|
201
|
+
}
|
|
202
|
+
listeners.add(listener);
|
|
203
|
+
return () => {
|
|
204
|
+
this.readyListeners.get(name)?.delete(listener);
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Общий срок остановки обмена с брокером: отсчёт — с первого вызова в beforeApplicationShutdown (relay,
|
|
209
|
+
* подписчик), resolve(false) через `deadlineMs`. Nest вызывает хуки по очереди — у каждого свой срок сложился бы.
|
|
210
|
+
*/
|
|
211
|
+
shutdownDeadline() {
|
|
212
|
+
this.stopDeadline ??= sleep(this.options.deadlineMs, false, { ref: false });
|
|
213
|
+
return this.stopDeadline;
|
|
214
|
+
}
|
|
215
|
+
/** Первый исход настройки канала: готов, фатальная ошибка `setup` или прочая ошибка стадии. */
|
|
216
|
+
channelOutcome(name) {
|
|
217
|
+
const state = this.channels.get(name);
|
|
218
|
+
if (state === undefined) {
|
|
219
|
+
throw new Error(`RabbitConnection: канала ${name} нет`);
|
|
220
|
+
}
|
|
221
|
+
return state.outcome;
|
|
222
|
+
}
|
|
223
|
+
/** Канал; `start()` должен быть завершён. */
|
|
224
|
+
createChannel(options) {
|
|
225
|
+
const name = options.name;
|
|
226
|
+
let settle = () => undefined;
|
|
227
|
+
const outcome = new Promise((resolve) => {
|
|
228
|
+
settle = resolve;
|
|
229
|
+
});
|
|
230
|
+
const state = {
|
|
231
|
+
options,
|
|
232
|
+
channel: undefined,
|
|
233
|
+
ready: false,
|
|
234
|
+
fatal: false,
|
|
235
|
+
raw: null,
|
|
236
|
+
lastBrokerError: null,
|
|
237
|
+
outcome,
|
|
238
|
+
settle: (value) => {
|
|
239
|
+
settle(value);
|
|
240
|
+
state.settle = () => undefined;
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
this.channels.set(name, state);
|
|
244
|
+
const channel = new ManagedChannel(name, this.openWrapper(options), () => {
|
|
245
|
+
this.channels.delete(name);
|
|
246
|
+
}, this.logger);
|
|
247
|
+
state.channel = channel;
|
|
248
|
+
return channel;
|
|
249
|
+
}
|
|
250
|
+
/** Шаг 4 остановки: закрытие соединения не дольше CONNECTION_CLOSE_TIMEOUT_MS. */
|
|
251
|
+
async onApplicationShutdown() {
|
|
252
|
+
this.stopped = true;
|
|
253
|
+
clearTimeout(this.retryTimer);
|
|
254
|
+
this.retryTimer = undefined;
|
|
255
|
+
this.readyListeners.clear();
|
|
256
|
+
this.readyFailureReported.clear();
|
|
257
|
+
await this.starting?.catch(() => undefined);
|
|
258
|
+
const manager = this.manager;
|
|
259
|
+
if (manager === null) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
this.manager = null;
|
|
263
|
+
this.channels.clear();
|
|
264
|
+
if (!(await settlesWithin(manager.close(), CONNECTION_CLOSE_TIMEOUT_MS))) {
|
|
265
|
+
this.logger.warn({ broker: this.endpoint, timeoutMs: CONNECTION_CLOSE_TIMEOUT_MS }, 'rabbitmq: брокер не ответил на закрытие соединения, соединение брошено');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
this.logger.info({ broker: this.endpoint }, 'rabbitmq: соединение закрыто');
|
|
269
|
+
}
|
|
270
|
+
async open() {
|
|
271
|
+
const acm = await loadPeer('amqp-connection-manager', 'messaging');
|
|
272
|
+
if (this.stopped) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const manager = acm.connect([this.options.url], {
|
|
276
|
+
heartbeatIntervalInSeconds: HEARTBEAT_SECONDS,
|
|
277
|
+
reconnectTimeInSeconds: this.options.retrySeconds,
|
|
278
|
+
});
|
|
279
|
+
this.first = new Promise((resolve) => {
|
|
280
|
+
manager.once('connect', () => resolve('connected'));
|
|
281
|
+
manager.once('connectFailed', () => resolve('failed'));
|
|
282
|
+
});
|
|
283
|
+
manager.on('connect', () => {
|
|
284
|
+
this.logger.info({ broker: this.endpoint }, 'rabbitmq: соединение установлено');
|
|
285
|
+
});
|
|
286
|
+
manager.on('disconnect', ({ err }) => {
|
|
287
|
+
this.markAllNotReady();
|
|
288
|
+
this.logger.warn({ broker: this.endpoint, err: sanitizeError(err) }, 'rabbitmq: соединение потеряно');
|
|
289
|
+
});
|
|
290
|
+
manager.on('connectFailed', ({ err }) => {
|
|
291
|
+
this.logger.debug({ broker: this.endpoint, error: err.message }, 'rabbitmq: попытка подключения не удалась');
|
|
292
|
+
});
|
|
293
|
+
// Без обработчика 'error' EventEmitter бросил бы исключение процессу.
|
|
294
|
+
manager.on('error', (err) => {
|
|
295
|
+
this.logger.warn({ broker: this.endpoint, err: sanitizeError(err) }, 'rabbitmq: ошибка соединения');
|
|
296
|
+
});
|
|
297
|
+
this.manager = manager;
|
|
298
|
+
}
|
|
299
|
+
openWrapper(options) {
|
|
300
|
+
if (this.manager === null) {
|
|
301
|
+
throw new Error('RabbitConnection: createChannel до завершения start()');
|
|
302
|
+
}
|
|
303
|
+
const name = options.name;
|
|
304
|
+
const channelOptions = {
|
|
305
|
+
name,
|
|
306
|
+
confirm: options.confirm,
|
|
307
|
+
json: false,
|
|
308
|
+
setup: async (channel) => {
|
|
309
|
+
this.channels.get(name)?.channel.markOpened();
|
|
310
|
+
// Брокер закрывает канал так: 'error' (код канала), затем 'close'. Разрыв соединения, замена канала и
|
|
311
|
+
// остановка дают только 'close'.
|
|
312
|
+
let closedByBroker = false;
|
|
313
|
+
channel.on('error', (err) => {
|
|
314
|
+
closedByBroker = true;
|
|
315
|
+
const state = this.channels.get(name);
|
|
316
|
+
if (state !== undefined) {
|
|
317
|
+
state.lastBrokerError = err;
|
|
318
|
+
}
|
|
319
|
+
this.logger.debug({ channel: name, error: err.message }, 'rabbitmq: канал закрыт с ошибкой');
|
|
320
|
+
});
|
|
321
|
+
channel.on('close', () => {
|
|
322
|
+
if (closedByBroker) {
|
|
323
|
+
this.onClosedByBroker(name, channel);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
await this.runStage(name, channel, 'exchange', () => channel.checkExchange(EVENTS_EXCHANGE));
|
|
327
|
+
await this.runStage(name, channel, 'setup', () => options.setup ? options.setup(channel) : Promise.resolve());
|
|
328
|
+
await this.runStage(name, channel, 'ready', () => {
|
|
329
|
+
this.onSetupDone(name, channel);
|
|
330
|
+
return Promise.resolve();
|
|
331
|
+
});
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
if (options.publishTimeout !== undefined) {
|
|
335
|
+
channelOptions.publishTimeout = options.publishTimeout;
|
|
336
|
+
}
|
|
337
|
+
const wrapper = this.manager.createChannel(channelOptions);
|
|
338
|
+
wrapper.on('error', (err, info) => {
|
|
339
|
+
this.logger.debug({ channel: info.name, error: err instanceof Error ? err.message : String(err) }, 'rabbitmq: ошибка канала');
|
|
340
|
+
});
|
|
341
|
+
return wrapper;
|
|
342
|
+
}
|
|
343
|
+
async runStage(name, channel, stage, work) {
|
|
344
|
+
try {
|
|
345
|
+
await work();
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
this.onSetupFailed(name, stage, error);
|
|
349
|
+
// Ошибка брокера уже закрыла канал; ошибка кода — нет: закрываем, чтобы повтор открыл новый.
|
|
350
|
+
await channel.close().catch(() => undefined);
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
markAllNotReady() {
|
|
355
|
+
for (const state of this.channels.values()) {
|
|
356
|
+
state.ready = false;
|
|
357
|
+
state.raw = null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
onSetupDone(name, channel) {
|
|
361
|
+
const state = this.channels.get(name);
|
|
362
|
+
if (state !== undefined) {
|
|
363
|
+
state.ready = true;
|
|
364
|
+
state.raw = channel;
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
// Синхронно: до отката готовности при исключении никто не увидит канал готовым.
|
|
368
|
+
for (const listener of this.readyListeners.get(name) ?? []) {
|
|
369
|
+
listener();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
if (state !== undefined) {
|
|
374
|
+
state.ready = false;
|
|
375
|
+
state.raw = null;
|
|
376
|
+
}
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
state?.settle({ kind: 'ready' });
|
|
380
|
+
this.readyFailureReported.delete(name);
|
|
381
|
+
this.logger.info({ channel: name }, 'rabbitmq: канал готов');
|
|
382
|
+
if (!this.topologyReady()) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
this.exchangeMissingReported = false;
|
|
386
|
+
clearTimeout(this.retryTimer);
|
|
387
|
+
this.retryTimer = undefined;
|
|
388
|
+
}
|
|
389
|
+
/** Брокер закрыл настроенный канал: ChannelWrapper сам его не пересоздаст — заменяем, как после сбоя настройки. */
|
|
390
|
+
onClosedByBroker(name, channel) {
|
|
391
|
+
const state = this.channels.get(name);
|
|
392
|
+
if (state === undefined || state.raw !== channel || this.manager === null) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
state.raw = null;
|
|
396
|
+
state.ready = false;
|
|
397
|
+
this.logger.warn({ channel: name }, 'rabbitmq: канал закрыт брокером');
|
|
398
|
+
this.scheduleRetry();
|
|
399
|
+
}
|
|
400
|
+
onSetupFailed(name, stage, error) {
|
|
401
|
+
const state = this.channels.get(name);
|
|
402
|
+
if (state !== undefined) {
|
|
403
|
+
state.ready = false;
|
|
404
|
+
}
|
|
405
|
+
if (isConnectionLoss(error)) {
|
|
406
|
+
this.logger.debug({ channel: name }, 'rabbitmq: канал закрыт разрывом соединения');
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const safeError = sanitizeError(error);
|
|
410
|
+
if (error instanceof MessagingStartupError) {
|
|
411
|
+
// Фатально (406 TTL .retry): повтор не изменит объявление очереди брокера (AC-5.19, П8).
|
|
412
|
+
if (state !== undefined) {
|
|
413
|
+
state.fatal = true;
|
|
414
|
+
state.settle({ kind: 'fatal', error });
|
|
415
|
+
}
|
|
416
|
+
this.logger.error({ channel: name, code: error.startupCode, err: safeError }, 'rabbitmq: фатальная ошибка настройки канала, повторов не будет');
|
|
417
|
+
captureError(error);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
state?.settle({ kind: 'failed', error: state.lastBrokerError ?? asError(error) });
|
|
421
|
+
this.reportStageFailure(name, stage, safeError);
|
|
422
|
+
this.scheduleRetry();
|
|
423
|
+
}
|
|
424
|
+
reportStageFailure(name, stage, safeError) {
|
|
425
|
+
if (stage !== 'exchange') {
|
|
426
|
+
this.logger.warn({ channel: name, stage, err: safeError }, 'rabbitmq: сбой настройки канала');
|
|
427
|
+
if (stage === 'ready' && !this.readyFailureReported.has(name)) {
|
|
428
|
+
// Ошибка кода, не брокера: одно событие GlitchTip на серию повторов канала.
|
|
429
|
+
captureError(safeError);
|
|
430
|
+
this.readyFailureReported.add(name);
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (this.exchangeMissingReported) {
|
|
435
|
+
this.logger.warn({ channel: name, exchange: EVENTS_EXCHANGE }, 'rabbitmq: exchange verdeect.events всё ещё отсутствует');
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
// Exchange объявляет definitions.json инфраструктуры до запуска сервиса (§3.4).
|
|
439
|
+
this.logger.error({ channel: name, exchange: EVENTS_EXCHANGE, err: safeError }, 'rabbitmq: exchange verdeect.events отсутствует');
|
|
440
|
+
captureError(safeError);
|
|
441
|
+
this.exchangeMissingReported = true;
|
|
442
|
+
}
|
|
443
|
+
/** Один таймер на соединение; повтор заменяет только каналы в ошибке (кроме фатальных). */
|
|
444
|
+
scheduleRetry() {
|
|
445
|
+
if (this.stopped || this.retryTimer !== undefined) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
this.retryTimer = setTimeout(() => {
|
|
449
|
+
this.retryTimer = undefined;
|
|
450
|
+
void this.retryFailedChannels();
|
|
451
|
+
}, this.options.retrySeconds * 1000);
|
|
452
|
+
this.retryTimer.unref();
|
|
453
|
+
}
|
|
454
|
+
async retryFailedChannels() {
|
|
455
|
+
if (this.stopped || this.manager === null || !this.manager.isConnected()) {
|
|
456
|
+
// Нет соединения: после переподключения менеджер сам повторит setup всех каналов.
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
for (const [name, state] of this.channels) {
|
|
460
|
+
if (state.ready || state.fatal) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
this.logger.debug({ channel: name }, 'rabbitmq: канал открывается заново');
|
|
464
|
+
try {
|
|
465
|
+
await state.channel.replace(this.openWrapper(state.options));
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
this.logger.warn({ channel: name, err: sanitizeError(error) }, 'rabbitmq: открыть канал заново не удалось');
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Решение после сбоя текущей доставки. */
|
|
2
|
+
export type RedeliveryDecision = 'retry' | 'dlq';
|
|
3
|
+
/** Сколько раз сообщение уходило из `queue` с `reason: rejected` (`x-death`); нет заголовка — 0. */
|
|
4
|
+
export declare function retryCount(headers: Record<string, unknown> | undefined, queue: string): number;
|
|
5
|
+
/** Повторить, пока повторов меньше MAX_RETRIES, иначе — DLQ. */
|
|
6
|
+
export declare function decideRedelivery(headers: Record<string, unknown> | undefined, queue: string): RedeliveryDecision;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Решение о повторной доставке (prd.md §3.4 «5 повторов по x-death», AC-5.7): сколько раз сообщение уже отклонялось из
|
|
2
|
+
// основной очереди и прошло <svc>.retry. Эталон — O redelivery.ts; из B — очередь параметром.
|
|
3
|
+
import { MAX_RETRIES } from './topology.js';
|
|
4
|
+
/** Сколько раз сообщение уходило из `queue` с `reason: rejected` (`x-death`); нет заголовка — 0. */
|
|
5
|
+
export function retryCount(headers, queue) {
|
|
6
|
+
const deaths = headers?.['x-death'];
|
|
7
|
+
if (!Array.isArray(deaths)) {
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
|
10
|
+
const rejected = deaths.find((entry) => entry.queue === queue && entry.reason === 'rejected');
|
|
11
|
+
const count = Number(rejected?.count ?? 0);
|
|
12
|
+
return Number.isFinite(count) ? count : 0;
|
|
13
|
+
}
|
|
14
|
+
/** Повторить, пока повторов меньше MAX_RETRIES, иначе — DLQ. */
|
|
15
|
+
export function decideRedelivery(headers, queue) {
|
|
16
|
+
return retryCount(headers, queue) < MAX_RETRIES ? 'retry' : 'dlq';
|
|
17
|
+
}
|