@onlineapps/mq-client-core 4.0.1 → 4.2.0
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 +220 -0
- package/README.md +174 -0
- package/package.json +1 -1
- package/src/config/queueConfig.js +272 -21
- package/src/config/recurrencesSchema.js +121 -0
- package/src/index.js +47 -0
- package/src/monitoring-initiated.js +187 -0
- package/src/scheduler-request.js +481 -0
- package/src/transports/rabbitmqClient.js +71 -5
|
@@ -60,7 +60,13 @@ const SECTION_PREFIXES = Object.freeze({
|
|
|
60
60
|
monitoring: 'monitoring',
|
|
61
61
|
telemetry: 'telemetry',
|
|
62
62
|
alerts: 'alert',
|
|
63
|
-
registry: 'registry'
|
|
63
|
+
registry: 'registry',
|
|
64
|
+
scheduler: 'scheduler',
|
|
65
|
+
// The one section whose names travel WITHOUT a prefix: `dlx` is the wire name every
|
|
66
|
+
// business queue dead-letters to (`x-dead-letter-exchange`), and renaming it would
|
|
67
|
+
// re-declare live topology. The empty prefix is a declared case, read explicitly by
|
|
68
|
+
// `queueName()`; a section absent from this map is still refused (d.902).
|
|
69
|
+
deadLetter: ''
|
|
64
70
|
});
|
|
65
71
|
|
|
66
72
|
/**
|
|
@@ -81,7 +87,8 @@ const INFRASTRUCTURE_PREFIX_LOOKUPS = Object.freeze({
|
|
|
81
87
|
validation: 'getValidationQueueConfig',
|
|
82
88
|
monitoring: 'getMonitoringQueueConfig',
|
|
83
89
|
telemetry: 'getTelemetryQueueConfig',
|
|
84
|
-
delivery: 'getDeliveryQueueConfig'
|
|
90
|
+
delivery: 'getDeliveryQueueConfig',
|
|
91
|
+
scheduler: 'getSchedulerQueueConfig'
|
|
85
92
|
});
|
|
86
93
|
|
|
87
94
|
const INFRASTRUCTURE_PREFIXES = Object.freeze(Object.keys(INFRASTRUCTURE_PREFIX_LOOKUPS));
|
|
@@ -627,10 +634,17 @@ module.exports = {
|
|
|
627
634
|
/**
|
|
628
635
|
* monitoring.workflow - Unified workflow lifecycle monitoring
|
|
629
636
|
* Bound to monitoring.workflow.fanout exchange (fanout).
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
637
|
+
*
|
|
638
|
+
* WHO publishes onto that fanout: the Delivery Dispatcher after it processes a terminal
|
|
639
|
+
* workflow message, the in-process orchestrator during a run, and this client's own
|
|
640
|
+
* dead-letter hook. WHICH event_type each of them sends is not written here - the
|
|
641
|
+
* vocabulary has one owner and it grows, so a list copied into this comment would be a
|
|
642
|
+
* hand-written descriptive fact that ages in silence
|
|
643
|
+
* (`.claude/rules/doc-code-binding.md` §1).
|
|
644
|
+
*
|
|
645
|
+
* The message carries the cookbook and the context snapshot the trace store keeps.
|
|
646
|
+
*
|
|
647
|
+
* @see api/docs/biz/70-contracts/monitoring-events.md
|
|
634
648
|
*/
|
|
635
649
|
'workflow': {
|
|
636
650
|
durable: true,
|
|
@@ -644,15 +658,20 @@ module.exports = {
|
|
|
644
658
|
|
|
645
659
|
/**
|
|
646
660
|
* monitoring.services - Service lifecycle events monitoring
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
661
|
+
*
|
|
662
|
+
* WHO publishes here: the Registry, which owns the service lifecycle, and the
|
|
663
|
+
* InfrastructureHealthTracker with service status changes. WHICH event_type each of them
|
|
664
|
+
* sends is not written here — that vocabulary has one owner and it grows, and a list
|
|
665
|
+
* copied into this comment would be a hand-written descriptive fact that ages in silence
|
|
666
|
+
* (`.claude/rules/doc-code-binding.md` §1).
|
|
667
|
+
*
|
|
651
668
|
* NOTE: Heartbeats are NOT published here - they are too frequent. The cadence
|
|
652
669
|
* is not a number this file owns: see the `heartbeatInterval` constructor option
|
|
653
670
|
* of RegistryClient (@onlineapps/conn-orch-registry), which ServiceWrapper feeds
|
|
654
671
|
* from BIZ_HEARTBEAT_INTERVAL_MS.
|
|
655
672
|
* NOTE: Healthcheck messages go to infrastructure.health.events exchange
|
|
673
|
+
*
|
|
674
|
+
* @see api/docs/biz/70-contracts/monitoring-events.md
|
|
656
675
|
*/
|
|
657
676
|
'services': {
|
|
658
677
|
durable: true,
|
|
@@ -689,6 +708,33 @@ module.exports = {
|
|
|
689
708
|
durable: true
|
|
690
709
|
},
|
|
691
710
|
|
|
711
|
+
/**
|
|
712
|
+
* monitoring.resource - Topic exchange for resource-changed events
|
|
713
|
+
*
|
|
714
|
+
* The monitoring consumer's resource-changed watcher publishes here with a routing
|
|
715
|
+
* key of the shape `resource.changed.<resource_type>`; the delivery endpoint binds
|
|
716
|
+
* `delivery.resource.events` to it by the pattern `resource.changed.#` for WS push.
|
|
717
|
+
* Three sides of ONE wire — the publisher, this exchange and the bound queue — and
|
|
718
|
+
* until d.619 the name had an owner on none of them: a module constant on the
|
|
719
|
+
* publishing side and a required env key on the binding side, so an override or a
|
|
720
|
+
* typo on either side loses every event silently. That is the same reasoning d.270
|
|
721
|
+
* settled for the health-events fanout, and the reason the name lives here and is
|
|
722
|
+
* published by `src/index.js` § topology.
|
|
723
|
+
*
|
|
724
|
+
* `topic`, `durable`, no arguments: what the publisher asserts, and what the running
|
|
725
|
+
* broker answers for it. An exchange holds nothing to dead-letter, so — like every
|
|
726
|
+
* other exchange in this file — it carries no route, even though every QUEUE of this
|
|
727
|
+
* section routes to `monitoring.dlq`.
|
|
728
|
+
*
|
|
729
|
+
* @see api/infra/api_monitoring/src/consumer/watchers/resourceChangedPublisher.js
|
|
730
|
+
* @see api/docs/biz/70-contracts/resource-changed.md
|
|
731
|
+
*/
|
|
732
|
+
resource: {
|
|
733
|
+
type: 'exchange',
|
|
734
|
+
exchangeType: 'topic',
|
|
735
|
+
durable: true
|
|
736
|
+
},
|
|
737
|
+
|
|
692
738
|
/**
|
|
693
739
|
* monitoring.infrastructure.health.events - the monitoring consumer's own copy of
|
|
694
740
|
* the infrastructure health events, bound to the infrastructure.health.events
|
|
@@ -743,8 +789,8 @@ module.exports = {
|
|
|
743
789
|
* Telemetry queue configurations
|
|
744
790
|
*
|
|
745
791
|
* The OpenTelemetry intake of the monitoring consumer: every service publishes
|
|
746
|
-
* logs, metrics and spans to `telemetry.exchange` with a routing
|
|
747
|
-
* own name, and these three queues are bound to it
|
|
792
|
+
* logs, metrics and spans to the `telemetry.exchange` declared below, with a routing
|
|
793
|
+
* key carrying its own name, and these three queues are bound to it
|
|
748
794
|
* (`docs/standards/monitoring-queues.md`, `docs/architecture/monitoring.md`).
|
|
749
795
|
*
|
|
750
796
|
* The TTL and the cap are the ones the queues are created with today; this config is
|
|
@@ -758,11 +804,50 @@ module.exports = {
|
|
|
758
804
|
* @see api/docs/governance/confirmations/mq-consumer-contract.md 003
|
|
759
805
|
*/
|
|
760
806
|
telemetry: {
|
|
807
|
+
/**
|
|
808
|
+
* telemetry.exchange - the topic exchange every telemetry producer on the platform
|
|
809
|
+
* publishes to, and the one the three queues below are bound to.
|
|
810
|
+
*
|
|
811
|
+
* The widest wire there is: every biz service through `@onlineapps/service-wrapper`,
|
|
812
|
+
* every infrastructure service through `@onlineapps/monitoring-core` (its logger and
|
|
813
|
+
* its OpenTelemetry exporter), and the monitoring consumer, which asserts the
|
|
814
|
+
* exchange, binds the three queues and drains them — and since I16 publishes its own
|
|
815
|
+
* lines onto it as well.
|
|
816
|
+
*
|
|
817
|
+
* Until d.708 the name appeared HERE only in the prose above, while the other three
|
|
818
|
+
* platform exchanges of this file (`infrastructure.health.events`,
|
|
819
|
+
* `monitoring.workflow.fanout`, `monitoring.resource`) each had a template. So one
|
|
820
|
+
* name had three owners — a module constant in the monitoring consumer
|
|
821
|
+
* (`infra/api_monitoring/src/consumer/topology.js`) and a config default in each of
|
|
822
|
+
* monitoring-core's two publishers — which is exactly the shape d.270 gave an owner
|
|
823
|
+
* for the health-events fanout and d.619 for the resource-changed topic
|
|
824
|
+
* (`change-discipline.md` § One rail per concern).
|
|
825
|
+
*
|
|
826
|
+
* `topic`, `durable`, no arguments: what all three of those sides assert today
|
|
827
|
+
* (`assertExchange(<name>, 'topic', { durable: true })`), measured before this entry
|
|
828
|
+
* was written. The broker compares an exchange's type and durability at EVERY
|
|
829
|
+
* declaration, so a value invented here would be `406 PRECONDITION-FAILED` for
|
|
830
|
+
* whichever side booted second. An exchange holds nothing to dead-letter, so — like
|
|
831
|
+
* every other exchange in this file — it carries no route, even though every QUEUE
|
|
832
|
+
* of this section routes to `telemetry.dlq`.
|
|
833
|
+
*
|
|
834
|
+
* Declaring it is not creating it: the exchange is brought into being by the service
|
|
835
|
+
* that asserts it, the same rule as `telemetry.dlq` below.
|
|
836
|
+
*
|
|
837
|
+
* @see api/infra/api_monitoring/src/consumer/index.js — assertMonitoringTopology()
|
|
838
|
+
* @see api/shared/monitoring/monitoring-core/src/exporters/rabbitmq.js
|
|
839
|
+
*/
|
|
840
|
+
exchange: {
|
|
841
|
+
type: 'exchange',
|
|
842
|
+
exchangeType: 'topic',
|
|
843
|
+
durable: true
|
|
844
|
+
},
|
|
845
|
+
|
|
761
846
|
/** telemetry.logs.queue - OpenTelemetry log records, drained into Loki. */
|
|
762
847
|
'logs.queue': {
|
|
763
848
|
durable: true,
|
|
764
849
|
arguments: {
|
|
765
|
-
'x-message-ttl':
|
|
850
|
+
'x-message-ttl': 300000, // 5 min — okno deploye bez konzumenta; conf telemetry-queue-ttl 001
|
|
766
851
|
'x-max-length': 100000, // Max 100k messages in queue
|
|
767
852
|
'x-dead-letter-exchange': '', // Default exchange: routing key IS the queue name
|
|
768
853
|
'x-dead-letter-routing-key': 'telemetry.dlq'
|
|
@@ -773,7 +858,7 @@ module.exports = {
|
|
|
773
858
|
'metrics.queue': {
|
|
774
859
|
durable: true,
|
|
775
860
|
arguments: {
|
|
776
|
-
'x-message-ttl':
|
|
861
|
+
'x-message-ttl': 300000, // 5 min — okno deploye bez konzumenta; conf telemetry-queue-ttl 001
|
|
777
862
|
'x-max-length': 100000,
|
|
778
863
|
'x-dead-letter-exchange': '',
|
|
779
864
|
'x-dead-letter-routing-key': 'telemetry.dlq'
|
|
@@ -784,7 +869,7 @@ module.exports = {
|
|
|
784
869
|
'traces.queue': {
|
|
785
870
|
durable: true,
|
|
786
871
|
arguments: {
|
|
787
|
-
'x-message-ttl':
|
|
872
|
+
'x-message-ttl': 300000, // 5 min — okno deploye bez konzumenta; conf telemetry-queue-ttl 001
|
|
788
873
|
'x-max-length': 100000,
|
|
789
874
|
'x-dead-letter-exchange': '',
|
|
790
875
|
'x-dead-letter-routing-key': 'telemetry.dlq'
|
|
@@ -932,11 +1017,155 @@ module.exports = {
|
|
|
932
1017
|
},
|
|
933
1018
|
|
|
934
1019
|
/**
|
|
935
|
-
* registry.changes -
|
|
936
|
-
*
|
|
1020
|
+
* registry.changes - the fanout exchange registry index changes travel on.
|
|
1021
|
+
*
|
|
1022
|
+
* One module is every side of this wire: `@onlineapps/conn-orch-registry`,
|
|
1023
|
+
* `src/registryEventConsumer.js`. It holds the only spelling of the name (`:92`),
|
|
1024
|
+
* it is the only party that ASSERTS the exchange (`:117`,
|
|
1025
|
+
* `assertExchange(name, 'fanout', { durable: true })`), it binds each service's
|
|
1026
|
+
* `{service}.registry.events` to it with the pattern `''` (`:133`), and it publishes
|
|
1027
|
+
* the snapshot request onto it (`:331`, `routingKey: ''`). The publish path declares
|
|
1028
|
+
* nothing since d.342, so there is no second founder.
|
|
1029
|
+
*
|
|
1030
|
+
* Until d.712 this was the ONE exchange of this file with no `exchangeType`, so the
|
|
1031
|
+
* founder had to decide the kind on its own while the template only said "an
|
|
1032
|
+
* exchange, durable". `type` is one of the two properties the broker compares at
|
|
1033
|
+
* every declaration, which makes it exactly the half that cannot live somewhere
|
|
1034
|
+
* else (`change-discipline.md` § One rail per concern; the same step d.619 made for
|
|
1035
|
+
* `monitoring.resource` and d.708 for `telemetry.exchange`).
|
|
1036
|
+
*
|
|
1037
|
+
* `fanout`, `durable`, no arguments: measured on that founder, not chosen. The
|
|
1038
|
+
* binding pattern `''` only routes on a fanout, so the type is load-bearing rather
|
|
1039
|
+
* than decorative. An exchange holds nothing to dead-letter, so — like every other
|
|
1040
|
+
* exchange in this file — it carries no route.
|
|
1041
|
+
*
|
|
1042
|
+
* Declaring it is not creating it: the exchange is brought into being by the service
|
|
1043
|
+
* whose registry client subscribes to changes.
|
|
1044
|
+
*
|
|
1045
|
+
* @see api/shared/connector/conn-orch-registry/src/registryEventConsumer.js
|
|
937
1046
|
*/
|
|
938
1047
|
changes: {
|
|
939
1048
|
type: 'exchange',
|
|
1049
|
+
exchangeType: 'fanout',
|
|
1050
|
+
durable: true
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Scheduler queue configurations — the platform's one holder of time.
|
|
1056
|
+
*
|
|
1057
|
+
* The scheduler consumes requests for a run: a recurrence that came due, or a deferred
|
|
1058
|
+
* request somebody filed. Both are published by other services and both are the ONLY
|
|
1059
|
+
* record that the run was owed, so the family is shaped for loss-aversion rather than
|
|
1060
|
+
* for freshness (`docs/governance/confirmations/scheduler.md` 001, 004).
|
|
1061
|
+
*
|
|
1062
|
+
* @see api/infra/api_scheduler/README.md
|
|
1063
|
+
*/
|
|
1064
|
+
scheduler: {
|
|
1065
|
+
/**
|
|
1066
|
+
* scheduler.request - every request for a run, from every producer.
|
|
1067
|
+
*
|
|
1068
|
+
* NO `x-message-ttl`, deliberately, and it is the one property that differs from the
|
|
1069
|
+
* shape of a workflow queue: a request that waited out a consumer outage is still the
|
|
1070
|
+
* run that was due, and a TTL would turn that outage into runs which silently never
|
|
1071
|
+
* happened. What bounds the queue is its length, and what catches a request the
|
|
1072
|
+
* scheduler could not process is the family's dead-letter queue below — over the
|
|
1073
|
+
* DEFAULT exchange, where the routing key IS the destination name, the same way every
|
|
1074
|
+
* other infrastructure family here routes (the `dlx` exchange belongs to the business
|
|
1075
|
+
* templates, whose queues one service owns).
|
|
1076
|
+
*
|
|
1077
|
+
* Without this declaration `consume()` refuses to attach a consumer at all — a queue
|
|
1078
|
+
* with no declared route would have the broker DROP what the policy rejects
|
|
1079
|
+
* (`docs/governance/confirmations/mq-consumer-contract.md` 002).
|
|
1080
|
+
*/
|
|
1081
|
+
request: {
|
|
1082
|
+
durable: true,
|
|
1083
|
+
arguments: {
|
|
1084
|
+
'x-max-length': 10000,
|
|
1085
|
+
'x-dead-letter-exchange': '', // Default exchange: routing key IS the queue name
|
|
1086
|
+
'x-dead-letter-routing-key': 'scheduler.dlq'
|
|
1087
|
+
}
|
|
1088
|
+
},
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* scheduler.workflow.events - where the scheduler learns that a run it started ENDED.
|
|
1092
|
+
*
|
|
1093
|
+
* The third queue bound to `monitoring.workflow.fanout`, beside `monitoring.workflow`
|
|
1094
|
+
* and `delivery.workflow.events`. The owner settled it in
|
|
1095
|
+
* `docs/governance/confirmations/scheduler.md` 007: the terminal event of a run already
|
|
1096
|
+
* travels on that exchange, so the scheduler reads it there rather than asking the
|
|
1097
|
+
* observer over HTTP or reading a database that is not its own. No producer changes; the
|
|
1098
|
+
* binding is the scheduler's own act at boot (the pattern
|
|
1099
|
+
* `infra/api_delivery_endpoint/src/index.js:164` already uses), and this file only
|
|
1100
|
+
* DECLARES what that queue is.
|
|
1101
|
+
*
|
|
1102
|
+
* WHICH event_type values those are is NOT written here: the vocabulary has one owner and
|
|
1103
|
+
* it grows. A list copied into this comment would be a hand-written descriptive fact that
|
|
1104
|
+
* ages in silence (`.claude/rules/doc-code-binding.md` §1).
|
|
1105
|
+
*
|
|
1106
|
+
* THE TTL IS STATED, NOT INHERITED, and it is a property of that decision rather than a
|
|
1107
|
+
* default: a fanout cannot filter, so this queue receives EVERY workflow event of the
|
|
1108
|
+
* platform and the scheduler discards most of them. 300 000 ms is what
|
|
1109
|
+
* `monitoring.workflow` carries on the same exchange - the sibling whose readings this
|
|
1110
|
+
* one follows. What an outage longer than the TTL costs is on record: those events are
|
|
1111
|
+
* lost and the run is closed by the per-schedule timeout, which 007 keeps as the
|
|
1112
|
+
* backstop rather than replacing it.
|
|
1113
|
+
*
|
|
1114
|
+
* The route goes over the DEFAULT exchange, like every other infrastructure family
|
|
1115
|
+
* here, and ends in this family's own `scheduler.dlq`. Without a declared route
|
|
1116
|
+
* `consume()` refuses to attach at all (`BaseClient.js`,
|
|
1117
|
+
* `CONSUMER_DEAD_LETTER_ROUTE_MISSING`; `mq-consumer-contract` 002).
|
|
1118
|
+
*
|
|
1119
|
+
* @see api/docs/governance/confirmations/scheduler.md 007
|
|
1120
|
+
* @see api/docs/biz/70-contracts/monitoring-events.md
|
|
1121
|
+
*/
|
|
1122
|
+
'workflow.events': {
|
|
1123
|
+
durable: true,
|
|
1124
|
+
arguments: {
|
|
1125
|
+
'x-message-ttl': 300000, // Stated by confirmation 007, like monitoring.workflow
|
|
1126
|
+
'x-max-length': 10000,
|
|
1127
|
+
'x-dead-letter-exchange': '', // Default exchange: routing key IS the queue name
|
|
1128
|
+
'x-dead-letter-routing-key': 'scheduler.dlq'
|
|
1129
|
+
}
|
|
1130
|
+
},
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* scheduler.dlq - the END of this family's chain.
|
|
1134
|
+
*
|
|
1135
|
+
* Durable, capped higher, no TTL and no onward route — the three properties every
|
|
1136
|
+
* dead-letter queue in this file carries, and the reason the operator's `*.dlq`
|
|
1137
|
+
* dashboard finds it (`mq-consumer-contract` 003).
|
|
1138
|
+
*/
|
|
1139
|
+
dlq: {
|
|
1140
|
+
durable: true,
|
|
1141
|
+
arguments: {
|
|
1142
|
+
// No TTL for DLQ - messages should persist
|
|
1143
|
+
'x-max-length': 50000 // Higher limit for DLQ
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
},
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* The platform's dead-letter exchange, DECLARED — name, type and durability.
|
|
1150
|
+
*
|
|
1151
|
+
* Every business template dead-letters here (`x-dead-letter-exchange`), and
|
|
1152
|
+
* `@onlineapps/conn-infra-mq` `QueueManager.setupServiceQueues()` brings it into
|
|
1153
|
+
* being as a durable `direct` exchange before binding `{service}.dlq` to it. The
|
|
1154
|
+
* broker compares type and durability at EVERY `assertExchange`, so they belong to
|
|
1155
|
+
* the declaration, where a second reader — `api/scripts/ci/declare-broker-exchanges.mjs`,
|
|
1156
|
+
* which declares exchanges on a CI broker nobody's services booted against — finds
|
|
1157
|
+
* them through the same loop as every other exchange of this file, instead of keeping
|
|
1158
|
+
* a copy of the founder's line (TODO INFRA 349, d.902).
|
|
1159
|
+
*
|
|
1160
|
+
* Its wire name carries no prefix (`SECTION_PREFIXES.deadLetter` is `''`), and the key
|
|
1161
|
+
* is the one constant at the top of this file, so the name has one spelling.
|
|
1162
|
+
* An exchange holds nothing to dead-letter, so — like every other exchange here — it
|
|
1163
|
+
* carries no route.
|
|
1164
|
+
*/
|
|
1165
|
+
deadLetter: {
|
|
1166
|
+
[DEAD_LETTER_EXCHANGE]: {
|
|
1167
|
+
type: 'exchange',
|
|
1168
|
+
exchangeType: 'direct',
|
|
940
1169
|
durable: true
|
|
941
1170
|
}
|
|
942
1171
|
},
|
|
@@ -950,9 +1179,13 @@ module.exports = {
|
|
|
950
1179
|
* a name this configuration does not declare would be asserted against the broker
|
|
951
1180
|
* by whoever imported it, which is how a typo becomes a second queue.
|
|
952
1181
|
*
|
|
1182
|
+
* A section whose declared prefix is `''` (`deadLetter`) composes the bare key: its
|
|
1183
|
+
* names travel without a prefix. That is a value of the map, not a default — a
|
|
1184
|
+
* section missing from it is refused below.
|
|
1185
|
+
*
|
|
953
1186
|
* @param {string} section - Section holding the entry (e.g. 'deliveryEvents').
|
|
954
1187
|
* @param {string} key - The entry's key within that section (e.g. 'health.events').
|
|
955
|
-
* @returns {string} The full name (e.g. 'delivery.health.events').
|
|
1188
|
+
* @returns {string} The full name (e.g. 'delivery.health.events'; the bare key for 'deadLetter').
|
|
956
1189
|
* @throws {ValidationError} If the section composes no wire name, or the entry is undeclared.
|
|
957
1190
|
*/
|
|
958
1191
|
queueName(section, key) {
|
|
@@ -974,7 +1207,7 @@ module.exports = {
|
|
|
974
1207
|
);
|
|
975
1208
|
}
|
|
976
1209
|
|
|
977
|
-
return `${prefix}.${key}`;
|
|
1210
|
+
return prefix === '' ? key : `${prefix}.${key}`;
|
|
978
1211
|
},
|
|
979
1212
|
|
|
980
1213
|
/**
|
|
@@ -1219,6 +1452,23 @@ module.exports = {
|
|
|
1219
1452
|
return this.getQueueConfig('infrastructure', name);
|
|
1220
1453
|
},
|
|
1221
1454
|
|
|
1455
|
+
/**
|
|
1456
|
+
* Get scheduler queue configuration
|
|
1457
|
+
* @param {string} queueName - Queue name (e.g., 'scheduler.request')
|
|
1458
|
+
* @returns {Object} Queue configuration
|
|
1459
|
+
* @throws {ValidationError} If the name carries another prefix, or nothing declares it.
|
|
1460
|
+
*/
|
|
1461
|
+
getSchedulerQueueConfig(queueName) {
|
|
1462
|
+
if (!queueName.startsWith('scheduler.')) {
|
|
1463
|
+
throw new ValidationError(
|
|
1464
|
+
`[queueConfig] Queue ${queueName} is not a scheduler queue - Expected: a name starting with "scheduler.". `
|
|
1465
|
+
+ 'Fix: call getInfrastructureQueueConfig(), which routes each prefix to the right lookup.'
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
const name = queueName.slice('scheduler.'.length);
|
|
1469
|
+
return this.getQueueConfig('scheduler', name);
|
|
1470
|
+
},
|
|
1471
|
+
|
|
1222
1472
|
/**
|
|
1223
1473
|
* Get monitoring queue configuration
|
|
1224
1474
|
* @param {string} queueName - Queue name (e.g., 'monitoring.workflow')
|
|
@@ -1428,11 +1678,12 @@ module.exports = {
|
|
|
1428
1678
|
* Every business template routes here (`x-dead-letter-exchange`), and so does
|
|
1429
1679
|
* `getDeadLetterRoute()` for a business queue. `@onlineapps/conn-infra-mq` binds
|
|
1430
1680
|
* `<service>.dlq` to it and had to read the name back out of a queue's arguments
|
|
1431
|
-
* for want of an export.
|
|
1681
|
+
* for want of an export. Composed from the `deadLetter` entry, which also declares
|
|
1682
|
+
* the exchange's type and durability — one declaration, not a name beside it.
|
|
1432
1683
|
*
|
|
1433
1684
|
* @returns {string}
|
|
1434
1685
|
*/
|
|
1435
1686
|
deadLetterExchange() {
|
|
1436
|
-
return DEAD_LETTER_EXCHANGE;
|
|
1687
|
+
return this.queueName('deadLetter', DEAD_LETTER_EXCHANGE);
|
|
1437
1688
|
}
|
|
1438
1689
|
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* recurrencesSchema.js
|
|
5
|
+
*
|
|
6
|
+
* JSON Schema used by Ajv (in `src/scheduler-request.js`) to validate the recurrences a service
|
|
7
|
+
* declares about itself.
|
|
8
|
+
*
|
|
9
|
+
* WHY THE SHAPE LIVES HERE. `docs/architecture/scheduler.md` §7 has the wrapper register a
|
|
10
|
+
* service's recurrences at boot — "idempotent, so every boot re-asserts the declared state" —
|
|
11
|
+
* and the service answers with them through ONE hook, platform ones as a constant list and
|
|
12
|
+
* tenant ones derived from its own data (confirmation `scheduler` 011). The shape of a record
|
|
13
|
+
* is the library's; where the record comes from is not. One owner of the shape means every service's file is the
|
|
14
|
+
* same file, and the wrapper reading it never has to guess.
|
|
15
|
+
*
|
|
16
|
+
* WHAT THE FILE DOES NOT CARRY, and why that is a rule rather than an omission:
|
|
17
|
+
*
|
|
18
|
+
* - **`tenant_id` and `workspace_id`.** They travel beside the record, not in it: the
|
|
19
|
+
* builder takes them as `tenantId` and `workspaceId`, and the caller supplies them with
|
|
20
|
+
* every recurrence — the service knows them from its own data, and nothing reads them from
|
|
21
|
+
* the environment or defaults them.
|
|
22
|
+
* @see api/docs/governance/confirmations/scheduler.md 002, 010
|
|
23
|
+
* - **`requested_by`.** It is the service that declared the record; the caller passes the name
|
|
24
|
+
* it already knows, so a record cannot disagree with the service that answered it.
|
|
25
|
+
*
|
|
26
|
+
* `additionalProperties: false` at both levels, deliberately: the consumer's own contract
|
|
27
|
+
* (`api/infra/api_scheduler/contracts/scheduler.request.schema.json`) closes its keys the same
|
|
28
|
+
* way, so a key accepted here and refused there would turn a boot into a dead-lettered
|
|
29
|
+
* request nobody reads.
|
|
30
|
+
*
|
|
31
|
+
* @see ../scheduler-request.js — the shape check, the builders and the send
|
|
32
|
+
* @see api/docs/architecture/scheduler.md §4, §7
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Five whitespace-separated fields — the same expression the scheduler's contract takes
|
|
37
|
+
* (`recurrence.cron`, pattern `^\S+( \S+){4}$`). Whether those fields describe a reachable
|
|
38
|
+
* instant is the tick engine's answer, not this file's.
|
|
39
|
+
*/
|
|
40
|
+
const CRON_PATTERN = '^\\S+( \\S+){4}$';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The id of one schedule: it becomes `idempotency_key`, so it is a name, not a sentence.
|
|
44
|
+
*
|
|
45
|
+
* As WIDE as the consumer's own rule and no wider: the scheduler takes any string of 1–255
|
|
46
|
+
* characters for that key, so this file refuses only what would make the key a sentence —
|
|
47
|
+
* whitespace. The narrower spelling this schema first carried (lower-case letters and dashes)
|
|
48
|
+
* was the library being stricter than the contract for no reason, and it refused the very
|
|
49
|
+
* convention the first caller uses: `<source_uuid>:<cookbook_ref>` (BIZ-ingest, 2026-09-23).
|
|
50
|
+
*/
|
|
51
|
+
const ID_PATTERN = '^\\S+$';
|
|
52
|
+
|
|
53
|
+
const recurrencesSchema = {
|
|
54
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
55
|
+
$id: 'https://onlineapps.cz/schemas/mq-client-core/recurrences.v1.schema.json',
|
|
56
|
+
title: 'Service recurrence declaration',
|
|
57
|
+
description:
|
|
58
|
+
'What a service declares about its own recurring runs. The wrapper re-asserts every record '
|
|
59
|
+
+ 'at boot as a `scheduler.request` of kind `recurring`.',
|
|
60
|
+
type: 'object',
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
required: ['recurrences'],
|
|
63
|
+
properties: {
|
|
64
|
+
recurrences: {
|
|
65
|
+
description:
|
|
66
|
+
'Every recurrence this service owns. An empty list is a valid declaration of nothing, '
|
|
67
|
+
+ 'and a service with no recurrences may answer one.',
|
|
68
|
+
type: 'array',
|
|
69
|
+
items: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
additionalProperties: false,
|
|
72
|
+
required: ['id', 'cookbook_ref', 'cron', 'timezone'],
|
|
73
|
+
properties: {
|
|
74
|
+
id: {
|
|
75
|
+
description:
|
|
76
|
+
'Stable id of THIS schedule; it travels as `idempotency_key`, so re-registration '
|
|
77
|
+
+ 'upserts instead of creating a second schedule.',
|
|
78
|
+
type: 'string',
|
|
79
|
+
minLength: 1,
|
|
80
|
+
maxLength: 255,
|
|
81
|
+
pattern: ID_PATTERN
|
|
82
|
+
},
|
|
83
|
+
cookbook_ref: {
|
|
84
|
+
description:
|
|
85
|
+
'Id of the cookbook to run. Its `triggers` must name this service and the kind '
|
|
86
|
+
+ '`recurring`, or the scheduler refuses the request (confirmation scheduler 004).',
|
|
87
|
+
type: 'string',
|
|
88
|
+
minLength: 1,
|
|
89
|
+
maxLength: 255
|
|
90
|
+
},
|
|
91
|
+
cron: {
|
|
92
|
+
description: 'Cron expression, five fields.',
|
|
93
|
+
type: 'string',
|
|
94
|
+
pattern: CRON_PATTERN
|
|
95
|
+
},
|
|
96
|
+
timezone: {
|
|
97
|
+
description: 'IANA zone, explicit. No implicit zone exists (scheduler.md §4).',
|
|
98
|
+
type: 'string',
|
|
99
|
+
minLength: 1,
|
|
100
|
+
maxLength: 64
|
|
101
|
+
},
|
|
102
|
+
input: {
|
|
103
|
+
description:
|
|
104
|
+
'Input handed to the cookbook unchanged. Present only where the run needs one — '
|
|
105
|
+
+ 'a scheduled import that writes says so here, while the same cookbook run by hand '
|
|
106
|
+
+ 'can be given a different input.',
|
|
107
|
+
type: 'object'
|
|
108
|
+
},
|
|
109
|
+
catch_up: {
|
|
110
|
+
description:
|
|
111
|
+
'Whether a missed window runs once when the scheduler comes back. Absent means '
|
|
112
|
+
+ 'false, and the scheduler materialises that default — this file does not.',
|
|
113
|
+
type: 'boolean'
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
module.exports = { recurrencesSchema, CRON_PATTERN, ID_PATTERN };
|
package/src/index.js
CHANGED
|
@@ -36,6 +36,19 @@ const {
|
|
|
36
36
|
publishToMonitoringServices,
|
|
37
37
|
isQueueUnavailableError,
|
|
38
38
|
} = require('./monitoring-publish');
|
|
39
|
+
const {
|
|
40
|
+
buildWorkflowInitiatedEvent,
|
|
41
|
+
publishWorkflowInitiated,
|
|
42
|
+
} = require('./monitoring-initiated');
|
|
43
|
+
const {
|
|
44
|
+
schedulerRequestQueue,
|
|
45
|
+
validateRecurrences,
|
|
46
|
+
buildRecurringRequest,
|
|
47
|
+
buildEventRequest,
|
|
48
|
+
buildCancelRequest,
|
|
49
|
+
publishSchedulerRequest,
|
|
50
|
+
createSchedulerPublisher,
|
|
51
|
+
} = require('./scheduler-request');
|
|
39
52
|
const queueConfig = require('./config/queueConfig');
|
|
40
53
|
const deliveryPolicy = require('./config/deliveryPolicy');
|
|
41
54
|
const { redactUrl, UNPARSEABLE_PLACEHOLDER } = require('@onlineapps/logger-contract');
|
|
@@ -84,6 +97,28 @@ module.exports.monitoring = {
|
|
|
84
97
|
publishToMonitoringWorkflow,
|
|
85
98
|
publishToMonitoringServices,
|
|
86
99
|
isQueueUnavailableError,
|
|
100
|
+
// d.819 - the one builder of `workflow_initiated`, beside the transport half it uses
|
|
101
|
+
// (`docs/governance/confirmations/scheduler.md` 009). The gateway and the scheduler each
|
|
102
|
+
// built that event in their own words; two rails over one event is a defect by default.
|
|
103
|
+
buildWorkflowInitiatedEvent,
|
|
104
|
+
publishWorkflowInitiated,
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* The recurrences a service declares, and how one of them is registered (d.833).
|
|
108
|
+
*
|
|
109
|
+
* `docs/architecture/scheduler.md` §7 gives this library the shape of one declared recurrence
|
|
110
|
+
* and the wrapper the job of re-asserting what the service answers at every boot (one hook,
|
|
111
|
+
* confirmation `scheduler` 011). The queue name travels with them, so no caller composes
|
|
112
|
+
* `scheduler.request` itself.
|
|
113
|
+
*/
|
|
114
|
+
module.exports.scheduler = {
|
|
115
|
+
requestQueue: schedulerRequestQueue,
|
|
116
|
+
validateRecurrences,
|
|
117
|
+
buildRecurringRequest,
|
|
118
|
+
buildEventRequest,
|
|
119
|
+
buildCancelRequest,
|
|
120
|
+
publishSchedulerRequest,
|
|
121
|
+
createSchedulerPublisher,
|
|
87
122
|
};
|
|
88
123
|
/** The only public way to reach the central queue classification; `src/config/queueConfig` is an internal path, not a contract. */
|
|
89
124
|
module.exports.queueConfig = queueConfig;
|
|
@@ -109,6 +144,18 @@ module.exports.topology = Object.freeze({
|
|
|
109
144
|
monitoringInfrastructureHealthEventsQueue: queueConfig.queueName('monitoring', 'infrastructure.health.events'),
|
|
110
145
|
/** The delivery endpoint's queue bound to that exchange, for WS push. */
|
|
111
146
|
deliveryHealthEventsQueue: queueConfig.queueName('deliveryEvents', 'health.events'),
|
|
147
|
+
/**
|
|
148
|
+
* Topic exchange: the monitoring consumer's resource-changed watcher publishes here,
|
|
149
|
+
* with a routing key of the shape `resource.changed.<resource_type>`.
|
|
150
|
+
*
|
|
151
|
+
* Declared in `queueConfig` since d.619. Until then the name was a module constant on
|
|
152
|
+
* the publishing side and a required env key on the binding side
|
|
153
|
+
* (`MONITORING_RESOURCE_EXCHANGE`) — the third side of this wire with no owner, while
|
|
154
|
+
* the queue bound to it had one here since d.270.
|
|
155
|
+
*
|
|
156
|
+
* @see api/infra/api_monitoring/src/consumer/watchers/resourceChangedPublisher.js
|
|
157
|
+
*/
|
|
158
|
+
monitoringResourceExchange: queueConfig.queueName('monitoring', 'resource'),
|
|
112
159
|
/**
|
|
113
160
|
* The delivery endpoint's queue for resource-changed events, bound to the
|
|
114
161
|
* `monitoring.resource` exchange by the routing pattern `resource.changed.#`.
|