@onlineapps/conn-orch-registry 6.0.3 → 8.0.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/API.md +852 -0
- package/CHANGELOG.md +621 -0
- package/README.md +55 -12
- package/docs/REGISTRY_CLIENT_GUIDE.md +2 -2
- package/examples/basicUsage.js +58 -64
- package/examples/env.js +59 -0
- package/examples/event-consumer-example.js +56 -44
- package/package.json +7 -5
- package/src/registryClient.js +309 -36
- package/src/registryEventConsumer.js +14 -2
package/README.md
CHANGED
|
@@ -25,11 +25,11 @@ Duty sections that apply:
|
|
|
25
25
|
|
|
26
26
|
> A lightweight client for microservice registration and heartbeat over RabbitMQ.
|
|
27
27
|
> Sends the full service specification (including `operations`) in the `register`
|
|
28
|
-
> message — see [
|
|
28
|
+
> message — see [registration-wire.md §2](../../../docs/biz/30-operations/registration-wire.md).
|
|
29
29
|
|
|
30
30
|
## 🚀 Features
|
|
31
31
|
|
|
32
|
-
* Automatic queue management (`workflow`, `<serviceName>.registry`, `api_services_queuer`, `registry.register`)
|
|
32
|
+
* Automatic queue management (`workflow`, `<serviceName>.registry`, `<serviceName>.registry.<instanceId>`, `api_services_queuer`, `registry.register`)
|
|
33
33
|
* Periodic heartbeat messages with metadata
|
|
34
34
|
* Full specification (endpoints + operations) published inside `register`
|
|
35
35
|
* Event-driven API using `EventEmitter`
|
|
@@ -56,22 +56,28 @@ yarn add @onlineapps/conn-orch-registry
|
|
|
56
56
|
// NOTE: Do NOT call dotenv.config() in libraries. Export environment variables explicitly.
|
|
57
57
|
|
|
58
58
|
const { ServiceRegistryClient, EVENTS } = require('@onlineapps/conn-orch-registry');
|
|
59
|
+
// requireEnv(name, description), requirePositiveIntegerEnv(name, description) — see examples/env.js
|
|
59
60
|
|
|
60
61
|
const client = new ServiceRegistryClient({
|
|
61
|
-
amqpUrl:
|
|
62
|
+
amqpUrl: requireEnv('RABBITMQ_URL', 'RabbitMQ URL, amqp://user:pass@host:port'),
|
|
62
63
|
serviceName: 'invoicing',
|
|
63
64
|
version: '1.0.0',
|
|
64
65
|
specificationEndpoint: '/api/v1/specification', // required — see § Configuration
|
|
65
|
-
heartbeatInterval:
|
|
66
|
+
heartbeatInterval: requirePositiveIntegerEnv('BIZ_HEARTBEAT_INTERVAL_MS', 'milliseconds between heartbeats'), // required; see § Configuration
|
|
67
|
+
instanceId: requireEnv('SERVICE_INSTANCE_ID', 'id of THIS running copy, one value per process'), // required; see § Configuration
|
|
68
|
+
discoveryTimeoutMs: requirePositiveIntegerEnv('REGISTRY_DISCOVERY_TIMEOUT_MS', 'deadline in ms of one getService() read'), // required; see § Configuration
|
|
69
|
+
redis, // node-redis client getService() reads the projection with
|
|
66
70
|
logger // required — see § Logger contract below
|
|
67
71
|
});
|
|
68
72
|
|
|
69
73
|
await client.init();
|
|
70
74
|
|
|
71
75
|
// Publish full service spec in the register message (single round-trip).
|
|
76
|
+
// Only the keys of the registration contract are accepted — endpoints, operations,
|
|
77
|
+
// metadata, health, workspaceScoped, timeout; any other key is refused by name.
|
|
72
78
|
await client.register({
|
|
73
79
|
endpoints: [/* ... */],
|
|
74
|
-
operations: await loadOperationsJson(), // see
|
|
80
|
+
operations: await loadOperationsJson(), // see registration-wire.md §3
|
|
75
81
|
metadata: { /* ... */ },
|
|
76
82
|
health: '/health'
|
|
77
83
|
});
|
|
@@ -95,7 +101,7 @@ process.on('SIGINT', async () => {
|
|
|
95
101
|
|
|
96
102
|
## 📄 Configuration
|
|
97
103
|
|
|
98
|
-
Configuration
|
|
104
|
+
Configuration is passed as constructor options; the client reads no environment variable:
|
|
99
105
|
|
|
100
106
|
| Option | Description | Default |
|
|
101
107
|
| ----------------------- | --------------------------------------------- | -------------------- |
|
|
@@ -104,6 +110,8 @@ Configuration can be provided via environment variables or constructor options:
|
|
|
104
110
|
| `version` | Service version in SemVer format (required) | — |
|
|
105
111
|
| `specificationEndpoint` | Path the `register`/`heartbeat` messages carry (**required**) | — |
|
|
106
112
|
| `heartbeatInterval` | Milliseconds between heartbeats (**required**) | — |
|
|
113
|
+
| `instanceId` | Non-empty string naming THIS running copy, one value per process (**required**) | — |
|
|
114
|
+
| `discoveryTimeoutMs` | Deadline (ms) of one `getService()` read of the registry projection (**required**) | — |
|
|
107
115
|
| `apiQueue` | Queue name for heartbeat/API messages | `api_services_queuer` |
|
|
108
116
|
| `registryQueue` | Queue name for registry requests | `registry.register` |
|
|
109
117
|
|
|
@@ -115,13 +123,32 @@ whenever the caller forgets the first (architecture-principles §3):
|
|
|
115
123
|
([registration-wire.md](/api/docs/biz/30-operations/registration-wire.md) §2.1) and the
|
|
116
124
|
registry stores what it is sent, so a substituted path lands in `registry:services` for
|
|
117
125
|
a service that never declared it;
|
|
118
|
-
* the heartbeat cadence has ONE owner — `
|
|
119
|
-
`
|
|
126
|
+
* the heartbeat cadence has ONE owner — the environment key `BIZ_HEARTBEAT_INTERVAL_MS`,
|
|
127
|
+
which `ServiceWrapper` reads and passes to this client; the config key
|
|
128
|
+
`wrapper.registry.heartbeatInterval` is retired and refused (owner confirmation
|
|
120
129
|
[`biz-health-freshness.md`](/api/docs/governance/confirmations/biz-health-freshness.md)
|
|
121
130
|
001 point 3) — and the freshness window the monitoring side computes is a multiple of it.
|
|
122
131
|
|
|
123
132
|
Both are refused at construction, by value, naming the fix.
|
|
124
133
|
|
|
134
|
+
**`discoveryTimeoutMs` carries no default** (d.1093). Without a deadline `getService()`
|
|
135
|
+
waited for as long as Redis was down or half-open, and the workflow step waiting on it hung
|
|
136
|
+
until the broker expired its message. The value belongs to the platform env, which
|
|
137
|
+
`ServiceWrapper` reads and passes here. A lookup that does not end in time is refused with
|
|
138
|
+
`code: 'REGISTRY_DISCOVERY_TIMEOUT'`, and a client that already knows Redis is gone
|
|
139
|
+
(`isReady === false`) with `code: 'REGISTRY_DISCOVERY_UNAVAILABLE'` — both `type: 'TRANSIENT'`,
|
|
140
|
+
so the step is retried. The deadline is a race beside the command, not node-redis'
|
|
141
|
+
`AbortSignal`: aborting a command node-redis 4.7.1 has already sent corrupts its command
|
|
142
|
+
queue (see the CHANGELOG entry of d.1093).
|
|
143
|
+
|
|
144
|
+
**`instanceId` carries no default either** (d.817, owner confirmation
|
|
145
|
+
`registry-instance-identity` 001). The service name says which service, the instance id
|
|
146
|
+
says which copy; `register`, `heartbeat` and `deregister` carry it as `instance_id`, and
|
|
147
|
+
the registry deletes a registration only when it matches. The client never makes one:
|
|
148
|
+
`@onlineapps/service-wrapper` makes it once at boot (`SERVICE_INSTANCE_ID` when the
|
|
149
|
+
deployment gives one, otherwise a UUID v4) and hands the same value to monitoring as
|
|
150
|
+
`serviceInstanceId`.
|
|
151
|
+
|
|
125
152
|
`amqpUrl` carries the broker credential, so no error message and no log line of this
|
|
126
153
|
package renders it as given: the URL passes through `redactUrl` from
|
|
127
154
|
`@onlineapps/logger-contract`, which drops the userinfo (`user:password@`) and keeps
|
|
@@ -132,8 +159,11 @@ implementace přestěhovala do core vrstvy; one concern, one rail.)
|
|
|
132
159
|
|
|
133
160
|
## 📬 Delivery of registry messages
|
|
134
161
|
|
|
135
|
-
|
|
136
|
-
|
|
162
|
+
The consumers in this package — the shared `<service>.registry` (the registry's own
|
|
163
|
+
prompts, such as `revalidate`, heard by every copy), the answer queue of THIS copy
|
|
164
|
+
`<service>.registry.<instanceId>` (named as `responseQueue` in `register`, so a copy
|
|
165
|
+
never reads the answer meant for another — owner confirmation `registry-instance-identity`
|
|
166
|
+
001 (c), d.817c) and the `<service>.registry.events` fanout copy — run on the delivery contract of
|
|
137
167
|
[`@onlineapps/mq-client-core`](https://www.npmjs.com/package/@onlineapps/mq-client-core)
|
|
138
168
|
(§ `consume()` — the delivery contract), registered with `requeueOnError: false`.
|
|
139
169
|
|
|
@@ -191,15 +221,25 @@ demands is a logger that component writes to). Standard:
|
|
|
191
221
|
### Register Request
|
|
192
222
|
```javascript
|
|
193
223
|
{
|
|
224
|
+
id: "request-uuid", // which request
|
|
225
|
+
instance_id: "copy-id", // which running copy (the constructor's instanceId)
|
|
194
226
|
type: "register",
|
|
195
227
|
serviceName: "hello-service",
|
|
196
228
|
version: "1.0.0",
|
|
197
|
-
|
|
229
|
+
specificationEndpoint: "/api/v1/specification",
|
|
198
230
|
endpoints: [...],
|
|
231
|
+
operations: { ... },
|
|
232
|
+
metadata: { ... },
|
|
233
|
+
health: "/health",
|
|
234
|
+
responseQueue: "hello-service.registry.copy-id", // THIS copy's answer queue (d.817c)
|
|
199
235
|
timestamp: "ISO-8601"
|
|
236
|
+
// + workspaceScoped when the service declares it,
|
|
237
|
+
// + validationProof / validationData when a proof was injected
|
|
200
238
|
}
|
|
201
239
|
```
|
|
202
240
|
|
|
241
|
+
The wire format has one owner: `api/docs/biz/30-operations/registration-wire.md` §2.
|
|
242
|
+
|
|
203
243
|
### Register Confirmed
|
|
204
244
|
```javascript
|
|
205
245
|
{
|
|
@@ -225,9 +265,12 @@ fixes it: `api/docs/biz/30-operations/registration-wire.md` §8.
|
|
|
225
265
|
### Heartbeat
|
|
226
266
|
```javascript
|
|
227
267
|
{
|
|
268
|
+
id: "message-uuid",
|
|
228
269
|
type: "heartbeat",
|
|
229
270
|
serviceName: "hello-service",
|
|
230
|
-
|
|
271
|
+
instance_id: "copy-id",
|
|
272
|
+
version: "1.0.0",
|
|
273
|
+
specificationEndpoint: "/api/v1/specification",
|
|
231
274
|
timestamp: "ISO-8601"
|
|
232
275
|
}
|
|
233
276
|
```
|
|
@@ -31,7 +31,7 @@ Microservice Registry Backend
|
|
|
31
31
|
> The legacy `apiDescriptionRequest` / `sendApiDescription` round-trip has
|
|
32
32
|
> been removed — the full service specification (including the `operations`
|
|
33
33
|
> map) travels as part of the `register` message. See
|
|
34
|
-
> [
|
|
34
|
+
> [registration-wire.md §2.1](../../../../docs/biz/30-operations/registration-wire.md).
|
|
35
35
|
|
|
36
36
|
## Usage
|
|
37
37
|
|
|
@@ -131,7 +131,7 @@ against the Operations Registry contract and either confirms or rejects.
|
|
|
131
131
|
|
|
132
132
|
**Parameters:**
|
|
133
133
|
- `serviceInfo.endpoints` - Array of API endpoints
|
|
134
|
-
- `serviceInfo.operations` - Operations map (see [
|
|
134
|
+
- `serviceInfo.operations` - Operations map (see [registration-wire.md §3](../../../../docs/biz/30-operations/registration-wire.md))
|
|
135
135
|
- `serviceInfo.metadata` - Service metadata object
|
|
136
136
|
- `serviceInfo.health` - Health check endpoint
|
|
137
137
|
|
package/examples/basicUsage.js
CHANGED
|
@@ -1,58 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* basicUsage.js
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
8
|
-
* 4.
|
|
9
|
-
* 5.
|
|
10
|
-
* 6.
|
|
4
|
+
* The lifecycle of ServiceRegistryClient:
|
|
5
|
+
* 1. read the settings from the environment (examples/env.js)
|
|
6
|
+
* 2. construct the client with every required option
|
|
7
|
+
* 3. init() — the response queues and their consumers
|
|
8
|
+
* 4. register() the service specification and branch on the registry's verdict
|
|
9
|
+
* 5. startHeartbeat() with its failure budget
|
|
10
|
+
* 6. on SIGINT/SIGTERM: deregister() this copy, then close()
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* Environment: RABBITMQ_URL, SERVICE_NAME, SERVICE_VERSION, SPECIFICATION_ENDPOINT,
|
|
13
|
+
* SERVICE_INSTANCE_ID, BIZ_HEARTBEAT_INTERVAL_MS, REGISTRY_DISCOVERY_TIMEOUT_MS.
|
|
14
|
+
*
|
|
15
|
+
* @see README.md § Configuration
|
|
16
|
+
* @see api/docs/biz/30-operations/registration-wire.md §2 MQ wire format
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
|
-
function requireEnv(name, description) {
|
|
16
|
-
const value = process.env[name];
|
|
17
|
-
if (!value || String(value).trim() === '') {
|
|
18
|
-
throw new Error(`[conn-orch-registry][example] Missing environment variable - ${name} is required. ${description || ''}`);
|
|
19
|
-
}
|
|
20
|
-
return String(value).trim();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
19
|
// Requiring the package by its own name does not resolve from inside the package
|
|
24
|
-
// (no self-reference `exports` field), so the example loads the sources directly
|
|
25
|
-
//
|
|
26
|
-
// '@onlineapps/conn-orch-registry'; see README § Quick Start.
|
|
20
|
+
// (no self-reference `exports` field), so the example loads the sources directly.
|
|
21
|
+
// Consumers require '@onlineapps/conn-orch-registry'; see README § Quick Start.
|
|
27
22
|
const { ServiceRegistryClient, EVENTS } = require('../src/index');
|
|
23
|
+
const { requireEnv, requirePositiveIntegerEnv } = require('./env');
|
|
28
24
|
|
|
29
|
-
// The logger is
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// the caller's decision, not a fallback inside the library.
|
|
25
|
+
// The logger is required by every constructor in this package (README § Logger
|
|
26
|
+
// contract). An example script has no service logger, so it passes `console`
|
|
27
|
+
// explicitly — it implements info/warn/error/debug.
|
|
33
28
|
const registryClient = new ServiceRegistryClient({
|
|
34
29
|
logger: console,
|
|
35
|
-
amqpUrl: requireEnv('
|
|
36
|
-
serviceName: requireEnv('SERVICE_NAME', '
|
|
37
|
-
version: requireEnv('SERVICE_VERSION', '
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
apiQueue: process.env.API_QUEUE,
|
|
43
|
-
registryQueue: process.env.REGISTRY_QUEUE
|
|
30
|
+
amqpUrl: requireEnv('RABBITMQ_URL', 'RabbitMQ URL, amqp://user:pass@host:port'),
|
|
31
|
+
serviceName: requireEnv('SERVICE_NAME', 'service logical name'),
|
|
32
|
+
version: requireEnv('SERVICE_VERSION', 'service version, SemVer'),
|
|
33
|
+
specificationEndpoint: requireEnv('SPECIFICATION_ENDPOINT', 'path the register and heartbeat messages carry, e.g. /api/v1/specification'),
|
|
34
|
+
instanceId: requireEnv('SERVICE_INSTANCE_ID', 'id of THIS running copy, one value per process'),
|
|
35
|
+
heartbeatInterval: requirePositiveIntegerEnv('BIZ_HEARTBEAT_INTERVAL_MS', 'milliseconds between heartbeats'),
|
|
36
|
+
discoveryTimeoutMs: requirePositiveIntegerEnv('REGISTRY_DISCOVERY_TIMEOUT_MS', 'deadline in ms of one getService() read')
|
|
44
37
|
});
|
|
45
38
|
|
|
46
39
|
registryClient.on(EVENTS.HEARTBEAT_SENT, (msg) => {
|
|
47
|
-
console.log(`[Heartbeat] Sent at ${msg.timestamp} for ${msg.serviceName}@${msg.version}`);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
registryClient.on(EVENTS.REGISTER_CONFIRMED, (msg) => {
|
|
51
|
-
console.log(`[Register] Confirmed for ${msg.serviceName || registryClient.serviceName}`);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
registryClient.on(EVENTS.REGISTER_REJECTED, (msg) => {
|
|
55
|
-
console.error('[Register] Rejected', msg);
|
|
40
|
+
console.log(`[Heartbeat] Sent at ${msg.timestamp} for ${msg.serviceName}@${msg.version} (${msg.instance_id})`);
|
|
56
41
|
});
|
|
57
42
|
|
|
58
43
|
registryClient.on(EVENTS.REVALIDATE, (payload) => {
|
|
@@ -68,8 +53,15 @@ registryClient.on(EVENTS.ERROR, (err) => {
|
|
|
68
53
|
await registryClient.init();
|
|
69
54
|
console.log('RegistryClient initialized: queues are ready.');
|
|
70
55
|
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
// register() resolves with the registry's verdict; `errorCode` is the machine
|
|
57
|
+
// reason of a refusal (registration-wire.md §8).
|
|
58
|
+
const verdict = await registryClient.register(serviceSpecification(registryClient.version));
|
|
59
|
+
if (verdict.success !== true) {
|
|
60
|
+
console.error(`[Register] Refused: ${verdict.errorCode} - ${verdict.message}`);
|
|
61
|
+
await registryClient.close();
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
console.log(`[Register] Confirmed for ${verdict.serviceName}@${verdict.version}`);
|
|
73
65
|
|
|
74
66
|
registryClient.startHeartbeat({
|
|
75
67
|
maxFailures: 5,
|
|
@@ -82,53 +74,55 @@ registryClient.on(EVENTS.ERROR, (err) => {
|
|
|
82
74
|
|
|
83
75
|
const shutdown = async () => {
|
|
84
76
|
console.log('Shutting down RegistryClient...');
|
|
77
|
+
// deregister() stops the heartbeat first; the registry deletes only the
|
|
78
|
+
// registration whose instance_id matches this copy.
|
|
79
|
+
await registryClient.deregister();
|
|
85
80
|
await registryClient.close();
|
|
86
81
|
process.exit(0);
|
|
87
82
|
};
|
|
88
83
|
process.on('SIGINT', shutdown);
|
|
89
84
|
process.on('SIGTERM', shutdown);
|
|
90
85
|
} catch (err) {
|
|
91
|
-
console.error('Failed to
|
|
86
|
+
console.error('Failed to run RegistryClient', err);
|
|
92
87
|
process.exit(1);
|
|
93
88
|
}
|
|
94
89
|
})();
|
|
95
90
|
|
|
96
91
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
92
|
+
* The service specification the register message carries. A real service sends the
|
|
93
|
+
* `operations` map of its config/service/operations.json; every operation is a v3
|
|
94
|
+
* declaration.
|
|
100
95
|
*
|
|
101
|
-
* @
|
|
96
|
+
* @param {string} version - the service version the metadata repeats
|
|
97
|
+
* @see api/docs/biz/30-operations/schema-v3.md § Per-operation keys
|
|
98
|
+
* @see api/docs/biz/30-operations/registration-wire.md §2.1
|
|
102
99
|
*/
|
|
103
|
-
|
|
100
|
+
function serviceSpecification(version) {
|
|
104
101
|
return {
|
|
105
|
-
endpoints: [
|
|
106
|
-
{ path: '/invoices', method: 'POST' },
|
|
107
|
-
{ path: '/invoices/:id', method: 'GET' }
|
|
108
|
-
],
|
|
102
|
+
endpoints: [],
|
|
109
103
|
operations: {
|
|
110
104
|
'create-invoice': {
|
|
111
105
|
description: 'Create a new invoice',
|
|
112
|
-
|
|
113
|
-
|
|
106
|
+
handler: 'handlers/invoices#createInvoice',
|
|
107
|
+
bundle_scope: 'workspace',
|
|
114
108
|
mutates: true,
|
|
115
109
|
resource_type: 'invoice',
|
|
116
110
|
minRole: 'EDITOR',
|
|
117
|
-
input: {},
|
|
118
|
-
output: {}
|
|
111
|
+
input: { type: 'object' },
|
|
112
|
+
output: { type: 'object' }
|
|
119
113
|
},
|
|
120
114
|
'get-invoice': {
|
|
121
|
-
description: 'Retrieve invoice by
|
|
122
|
-
|
|
123
|
-
|
|
115
|
+
description: 'Retrieve an invoice by id',
|
|
116
|
+
handler: 'handlers/invoices#getInvoice',
|
|
117
|
+
bundle_scope: 'workspace',
|
|
124
118
|
mutates: false,
|
|
125
|
-
resource_type:
|
|
119
|
+
resource_type: 'invoice',
|
|
126
120
|
minRole: 'VIEWER',
|
|
127
|
-
input: {},
|
|
128
|
-
output: {}
|
|
121
|
+
input: { type: 'object' },
|
|
122
|
+
output: { type: 'object' }
|
|
129
123
|
}
|
|
130
124
|
},
|
|
131
|
-
metadata: {},
|
|
125
|
+
metadata: { version, description: 'Invoice example' },
|
|
132
126
|
health: '/health'
|
|
133
127
|
};
|
|
134
128
|
}
|
package/examples/env.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* env.js — how the examples read their settings: by key name, and a key that is
|
|
5
|
+
* missing or malformed stops the example naming the key and the fix. No default
|
|
6
|
+
* stands in for an unset key (architecture-principles.md §3, §5).
|
|
7
|
+
*
|
|
8
|
+
* The key names are the platform's own (`config/env-templates/shared.env`), so an
|
|
9
|
+
* example runs with the same environment a biz service gets.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} name - environment key
|
|
14
|
+
* @param {string} description - what the value is, for the error message
|
|
15
|
+
* @returns {string} the trimmed value
|
|
16
|
+
* @throws {Error} when the key is unset or blank
|
|
17
|
+
*/
|
|
18
|
+
function requireEnv(name, description) {
|
|
19
|
+
const value = process.env[name];
|
|
20
|
+
if (value === undefined) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`[conn-orch-registry][example] Missing environment variable - ${name} is required (${description}). `
|
|
23
|
+
+ `Fix: export ${name} before running the example.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (value.trim() === '') {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`[conn-orch-registry][example] Empty environment variable - ${name} is set but blank (${description}). `
|
|
29
|
+
+ `Fix: export ${name} with a value.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return value.trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} name - environment key
|
|
37
|
+
* @param {string} description - what the value is, for the error message
|
|
38
|
+
* @returns {number} a positive whole number
|
|
39
|
+
* @throws {Error} when the key is unset, blank, not a whole number or not positive
|
|
40
|
+
*/
|
|
41
|
+
function requirePositiveIntegerEnv(name, description) {
|
|
42
|
+
const raw = requireEnv(name, description);
|
|
43
|
+
const value = Number(raw);
|
|
44
|
+
if (!Number.isInteger(value)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`[conn-orch-registry][example] Invalid environment variable - ${name} is ${JSON.stringify(raw)}. `
|
|
47
|
+
+ `Expected: a positive whole number (${description}). Fix: export ${name} as a whole number.`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (value <= 0) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`[conn-orch-registry][example] Invalid environment variable - ${name} is ${value}. `
|
|
53
|
+
+ `Expected: a positive whole number (${description}). Fix: export ${name} above zero.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { requireEnv, requirePositiveIntegerEnv };
|
|
@@ -1,81 +1,85 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* event-consumer-example.js
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* service's spec, use createRegistryReader from @onlineapps/service-common.
|
|
4
|
+
* What this example shows: a client that registers, beats, and opts in to the
|
|
5
|
+
* registry change events through subscribeToChanges(), with listeners for them.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* The consumer runs in index-only mode: no storageConfig, because
|
|
8
|
+
* subscribeToChanges() (src/registryClient.js) builds it without a StorageConnector
|
|
9
|
+
* and the RegistryEventConsumer constructor refuses a complete storageConfig
|
|
10
|
+
* without one. Redis persists the local service index.
|
|
11
|
+
*
|
|
12
|
+
* Environment: RABBITMQ_URL, REDIS_URL, SERVICE_NAME, SERVICE_VERSION,
|
|
13
|
+
* SPECIFICATION_ENDPOINT, SERVICE_INSTANCE_ID, BIZ_HEARTBEAT_INTERVAL_MS,
|
|
14
|
+
* REGISTRY_DISCOVERY_TIMEOUT_MS.
|
|
15
|
+
*
|
|
16
|
+
* @see README.md § Configuration
|
|
17
|
+
* @see api/docs/biz/30-operations/registration-wire.md §7
|
|
9
18
|
*/
|
|
10
19
|
|
|
20
|
+
const { createClient } = require('redis');
|
|
11
21
|
const { ServiceRegistryClient } = require('../src/index');
|
|
12
|
-
const
|
|
22
|
+
const { requireEnv, requirePositiveIntegerEnv } = require('./env');
|
|
13
23
|
|
|
14
24
|
async function main() {
|
|
15
|
-
//
|
|
16
|
-
const redis =
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
// node-redis, the client getService() reads the registry projection with.
|
|
26
|
+
const redis = createClient({
|
|
27
|
+
url: requireEnv('REDIS_URL', 'Redis URL, redis://:password@host:port'),
|
|
28
|
+
disableOfflineQueue: true
|
|
19
29
|
});
|
|
30
|
+
await redis.connect();
|
|
20
31
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// `console` explicitly — it implements info/warn/error/debug, and choosing it here is
|
|
25
|
-
// the caller's decision, not a fallback inside the library.
|
|
32
|
+
// The logger is required by every constructor in this package (README § Logger
|
|
33
|
+
// contract). An example script has no service logger, so it passes `console`
|
|
34
|
+
// explicitly — it implements info/warn/error/debug.
|
|
26
35
|
const client = new ServiceRegistryClient({
|
|
27
36
|
logger: console,
|
|
28
|
-
amqpUrl:
|
|
29
|
-
serviceName: '
|
|
30
|
-
version: '
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
|
|
37
|
-
secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin'
|
|
38
|
-
}
|
|
37
|
+
amqpUrl: requireEnv('RABBITMQ_URL', 'RabbitMQ URL, amqp://user:pass@host:port'),
|
|
38
|
+
serviceName: requireEnv('SERVICE_NAME', 'service logical name'),
|
|
39
|
+
version: requireEnv('SERVICE_VERSION', 'service version, SemVer'),
|
|
40
|
+
specificationEndpoint: requireEnv('SPECIFICATION_ENDPOINT', 'path the register and heartbeat messages carry, e.g. /api/v1/specification'),
|
|
41
|
+
instanceId: requireEnv('SERVICE_INSTANCE_ID', 'id of THIS running copy, one value per process'),
|
|
42
|
+
heartbeatInterval: requirePositiveIntegerEnv('BIZ_HEARTBEAT_INTERVAL_MS', 'milliseconds between heartbeats'),
|
|
43
|
+
discoveryTimeoutMs: requirePositiveIntegerEnv('REGISTRY_DISCOVERY_TIMEOUT_MS', 'deadline in ms of one getService() read'),
|
|
44
|
+
redis
|
|
39
45
|
});
|
|
40
46
|
|
|
41
|
-
// Initialize connection
|
|
42
47
|
await client.init();
|
|
43
48
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
await client.register({
|
|
48
|
-
endpoints: [{ path: '/hello', method: 'GET' }],
|
|
49
|
+
// register() resolves with the registry's verdict (registration-wire.md §2.4, §8).
|
|
50
|
+
const verdict = await client.register({
|
|
51
|
+
endpoints: [],
|
|
49
52
|
operations: {
|
|
50
53
|
'say-hello': {
|
|
51
54
|
description: 'Say hello',
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
handler: 'handlers/hello#sayHello',
|
|
56
|
+
bundle_scope: 'platform',
|
|
54
57
|
mutates: false,
|
|
55
58
|
resource_type: null,
|
|
56
59
|
minRole: 'VIEWER',
|
|
57
|
-
input: {},
|
|
58
|
-
output: {}
|
|
60
|
+
input: { type: 'object' },
|
|
61
|
+
output: { type: 'object' }
|
|
59
62
|
}
|
|
60
63
|
},
|
|
61
|
-
metadata: {},
|
|
64
|
+
metadata: { version: client.version, description: 'Event consumer example' },
|
|
62
65
|
health: '/health'
|
|
63
66
|
});
|
|
67
|
+
if (verdict.success !== true) {
|
|
68
|
+
throw new Error(`[conn-orch-registry][example] Registration refused - ${verdict.errorCode}: ${verdict.message}`);
|
|
69
|
+
}
|
|
64
70
|
|
|
65
|
-
// Start heartbeat
|
|
66
71
|
client.startHeartbeat({
|
|
67
72
|
maxFailures: 5,
|
|
68
73
|
onFatal: (err) => {
|
|
69
74
|
console.error(err.message);
|
|
70
|
-
process.exit(1);
|
|
75
|
+
process.exit(1); // the restart policy boots it again and it re-registers
|
|
71
76
|
}
|
|
72
77
|
});
|
|
73
78
|
|
|
74
|
-
// OPT-IN:
|
|
79
|
+
// OPT-IN: subscribe to registry changes.
|
|
75
80
|
console.log('Subscribing to registry changes...');
|
|
76
81
|
await client.subscribeToChanges();
|
|
77
82
|
|
|
78
|
-
// Listen for events
|
|
79
83
|
client.on('serviceUpdated', ({ service, fingerprint, version }) => {
|
|
80
84
|
console.log(`Service updated: ${service} v${version} (${fingerprint})`);
|
|
81
85
|
});
|
|
@@ -88,14 +92,22 @@ async function main() {
|
|
|
88
92
|
console.log(`Received snapshot with ${count} services`);
|
|
89
93
|
});
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
client.on('error', (err) => {
|
|
96
|
+
console.error('[RegistryClient Error]', err);
|
|
97
|
+
});
|
|
98
|
+
|
|
92
99
|
process.on('SIGINT', async () => {
|
|
93
100
|
console.log('\nShutting down...');
|
|
101
|
+
// deregister() stops the heartbeat first; the registry deletes only the
|
|
102
|
+
// registration whose instance_id matches this copy.
|
|
103
|
+
await client.deregister();
|
|
94
104
|
await client.close();
|
|
95
105
|
await redis.quit();
|
|
96
106
|
process.exit(0);
|
|
97
107
|
});
|
|
98
108
|
}
|
|
99
109
|
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
main().catch((err) => {
|
|
111
|
+
console.error(err);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/conn-orch-registry",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "conn-orch-registry provides the core communication mechanism for microservices in this environment. It enables them to interact with a services_registry to receive and fulfill tasks by submitting heartbeats or their API descriptions.",
|
|
6
6
|
"keywords": [
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"docs/",
|
|
19
19
|
"examples/",
|
|
20
20
|
"README.md",
|
|
21
|
-
"LICENSE"
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"CHANGELOG.md",
|
|
23
|
+
"API.md"
|
|
22
24
|
],
|
|
23
25
|
"oa": {
|
|
24
26
|
"category": "orchestration"
|
|
@@ -37,14 +39,14 @@
|
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
41
|
"@onlineapps/logger-contract": "2.0.0",
|
|
40
|
-
"@onlineapps/mq-client-core": "
|
|
42
|
+
"@onlineapps/mq-client-core": "5.1.0",
|
|
41
43
|
"uuid": "^9.0.1"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
44
46
|
"amqplib": "0.10.9",
|
|
45
|
-
"ioredis": "^5.8.0",
|
|
46
47
|
"jest": "^29.6.1",
|
|
47
|
-
"jsdoc-to-markdown": "^8.0.0"
|
|
48
|
+
"jsdoc-to-markdown": "^8.0.0",
|
|
49
|
+
"redis": "4.7.1"
|
|
48
50
|
},
|
|
49
51
|
"engines": {
|
|
50
52
|
"node": ">=24.0.0 <25"
|