apache-iggy 0.10.0-edge.7 → 0.10.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/README.md +16 -9
- package/dist/client/client.connection.d.ts +1 -1
- package/dist/client/client.connection.js +2 -2
- package/dist/client/client.socket.d.ts +12 -0
- package/dist/client/client.socket.js +271 -5
- package/dist/client/client.socket.test.js +604 -0
- package/dist/wire/command.code.d.ts +4 -0
- package/dist/wire/command.code.js +4 -0
- package/dist/wire/message/header.type.d.ts +1 -1
- package/dist/wire/message/poll-messages.command.js +4 -1
- package/dist/wire/message/poll-messages.command.test.js +11 -0
- package/dist/wire/token/token.utils.js +3 -7
- package/dist/wire/token/token.utils.test.d.ts +2 -0
- package/dist/wire/token/token.utils.test.js +67 -0
- package/dist/wire/vsr/header.d.ts +1 -0
- package/dist/wire/vsr/header.js +1 -0
- package/dist/wire/vsr/index.d.ts +4 -0
- package/dist/wire/vsr/index.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -19,9 +19,11 @@ note: previous works on node.js http client has been moved to [iggy-node-http-cl
|
|
|
19
19
|
## install
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npm i --save apache-iggy
|
|
22
|
+
npm i --save apache-iggy@edge
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
Use the `edge` package with server 0.9.0 or `edge`. Stable Node SDK 0.8.0 uses the older wire protocol. For a local server with the example credentials, follow the [example setup](../../examples/node/README.md#running-examples).
|
|
26
|
+
|
|
25
27
|
## basic usage
|
|
26
28
|
|
|
27
29
|
### Response frame limit
|
|
@@ -49,12 +51,9 @@ Codes absent from the SDK command table use `Operation::NonReplicated` and
|
|
|
49
51
|
carry the command code in the request header's reserved field. The server
|
|
50
52
|
remains authoritative for classifying or rejecting extension commands.
|
|
51
53
|
|
|
52
|
-
Sends
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
rejected before the request is sent.
|
|
56
|
-
<!-- TODO(hubcio): Balanced and MessageKey partitioning to be implemented;
|
|
57
|
-
not decided yet whether it'll be on server side or client side. -->
|
|
54
|
+
Sends encode `Partitioning.PartitionId`, `Partitioning.Balanced` or
|
|
55
|
+
`Partitioning.MessageKey` in the payload. The server resolves the target
|
|
56
|
+
partition at admission.
|
|
58
57
|
|
|
59
58
|
VSR works over TCP and TLS. It restricts `Client` to one pooled connection because authentication, request sequencing, and consumer-group assignments belong to one consensus session. Configurations requesting more than one pooled connection fail before a socket is opened.
|
|
60
59
|
|
|
@@ -69,8 +68,9 @@ The client pings every `heartbeatInterval` milliseconds, 5000 by default, which
|
|
|
69
68
|
keeps an idle session alive when the server's `[heartbeat]` eviction is enabled.
|
|
70
69
|
`heartbeatInterval` also accepts a duration expression such as `"10s"` or
|
|
71
70
|
`"1h 30m"`, like the Rust SDK.
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
With the default heartbeat settings, a group member becomes eligible for
|
|
72
|
+
eviction after 36 s of silence (1.2 x the 30 s interval); the verifier checks
|
|
73
|
+
once per interval. Raising the client interval past that window, or setting it
|
|
74
74
|
to 0 to disable client heartbeats, exposes an idle consumer-group member to
|
|
75
75
|
eviction; a connection holding no group membership is left alone. Any other
|
|
76
76
|
unusable value is rejected instead of silently disabling the heartbeat.
|
|
@@ -146,6 +146,13 @@ Durations accept the same expressions as the Rust SDK, for example `500ms`,
|
|
|
146
146
|
|
|
147
147
|
## use sources
|
|
148
148
|
|
|
149
|
+
Cluster auto-commit polling over TCP/TLS keeps group membership on the coordinator
|
|
150
|
+
and uses separate connections to partition primaries. It requires server support
|
|
151
|
+
for binary commands 14, 103 and 104. Pause binary auto-commit consumers for the
|
|
152
|
+
whole upgrade: upgrade every server first, then the SDKs, and restart consumers
|
|
153
|
+
so they rejoin their groups. Older SDKs can lose membership when a backup refuses
|
|
154
|
+
an offset commit; the new SDK does not fall back to legacy polling.
|
|
155
|
+
|
|
149
156
|
### Install
|
|
150
157
|
|
|
151
158
|
```bash
|
|
@@ -68,7 +68,7 @@ export declare class IggyConnection extends EventEmitter {
|
|
|
68
68
|
*
|
|
69
69
|
* @returns Promise that resolves when connected
|
|
70
70
|
*/
|
|
71
|
-
connect(): Promise<this>;
|
|
71
|
+
connect(boundDial?: boolean): Promise<this>;
|
|
72
72
|
/**
|
|
73
73
|
* Waits for one dial, bounded while other endpoints are queued behind it.
|
|
74
74
|
*
|
|
@@ -229,7 +229,7 @@ export class IggyConnection extends EventEmitter {
|
|
|
229
229
|
*
|
|
230
230
|
* @returns Promise that resolves when connected
|
|
231
231
|
*/
|
|
232
|
-
connect() {
|
|
232
|
+
connect(boundDial = false) {
|
|
233
233
|
if (this.ending)
|
|
234
234
|
return Promise.reject(new Error('connection is closed'));
|
|
235
235
|
if (this.connected)
|
|
@@ -245,7 +245,7 @@ export class IggyConnection extends EventEmitter {
|
|
|
245
245
|
// Bounded here too when the client knows somewhere else to go: this dial
|
|
246
246
|
// is not part of a pass, so an endpoint that never becomes usable would
|
|
247
247
|
// hold it with no timer of its own and the redial pass would never start.
|
|
248
|
-
const connectPromise = this._dialWithin(socket, this._redialCandidates().length > 1);
|
|
248
|
+
const connectPromise = this._dialWithin(socket, boundDial || this._redialCandidates().length > 1);
|
|
249
249
|
this.connectPromise = connectPromise;
|
|
250
250
|
const clearConnectPromise = () => {
|
|
251
251
|
if (this.connectPromise === connectPromise)
|
|
@@ -54,6 +54,12 @@ export declare class CommandResponseStream extends EventEmitter {
|
|
|
54
54
|
heartbeatIntervalHandler?: NodeJS.Timeout;
|
|
55
55
|
/** Whether a heartbeat ping is still awaiting its response */
|
|
56
56
|
private heartbeatInFlight;
|
|
57
|
+
private rememberedCredentials?;
|
|
58
|
+
private clustered?;
|
|
59
|
+
private metadataWatermark;
|
|
60
|
+
private routingGeneration;
|
|
61
|
+
private pollRoutes;
|
|
62
|
+
private pollConnections;
|
|
57
63
|
/**
|
|
58
64
|
* Creates a new CommandResponseStream.
|
|
59
65
|
*
|
|
@@ -109,6 +115,12 @@ export declare class CommandResponseStream extends EventEmitter {
|
|
|
109
115
|
*/
|
|
110
116
|
private _releaseUndecidedMove;
|
|
111
117
|
private _rememberRoster;
|
|
118
|
+
private _pollOnPrimary;
|
|
119
|
+
private _pollRoutingControl;
|
|
120
|
+
private _observeMetadataReply;
|
|
121
|
+
private _rememberCredentials;
|
|
122
|
+
private _signInCredentials;
|
|
123
|
+
private _clearPollRouting;
|
|
112
124
|
/**
|
|
113
125
|
* Processes queued commands sequentially.
|
|
114
126
|
* Emits 'finishQueue' when all commands are processed.
|
|
@@ -20,8 +20,12 @@ import { debug } from './client.debug.js';
|
|
|
20
20
|
import { endpointKey, IggyConnection } from './client.connection.js';
|
|
21
21
|
import { LOGIN, LOGIN_WITH_TOKEN, LOGOUT, PING } from '../wire/index.js';
|
|
22
22
|
import { GET_CLUSTER_METADATA } from '../wire/cluster/get-cluster-metadata.command.js';
|
|
23
|
+
import { deserializeNode } from '../wire/cluster/cluster.utils.js';
|
|
23
24
|
import { COMMAND_CODE } from '../wire/command.code.js';
|
|
24
|
-
import {
|
|
25
|
+
import { serializeIdentifier } from '../wire/identifier.utils.js';
|
|
26
|
+
import { Command, HEADER_SIZE, REPLY_OFFSET, peekCommand, readReplyOperation, readStatus } from '../wire/vsr/header.js';
|
|
27
|
+
import { Operation, isKnownOperation } from '../wire/vsr/operation.js';
|
|
28
|
+
import { decodeVsrResponse, prepareVsrCommand, readRegisteredSession, readWireName, VsrEvictionError, VsrSession, } from '../wire/vsr/index.js';
|
|
25
29
|
import { normalizeClientConfig } from './client.config.js';
|
|
26
30
|
const VSR_RESPONSE_TIMEOUT_MS = 30_000;
|
|
27
31
|
const VSR_RETRY_INTERVAL_MS = 50;
|
|
@@ -30,6 +34,14 @@ const LEADERLESS_POLL_INTERVAL_MS = 250;
|
|
|
30
34
|
const MAX_LEADER_REDIRECTS = 3;
|
|
31
35
|
const TRANSIENT_NOT_COMMITTED = 57;
|
|
32
36
|
const TRANSIENT_NOT_ACCEPTED = 58;
|
|
37
|
+
const UNAUTHENTICATED = 40;
|
|
38
|
+
const STALE_CLIENT = 30;
|
|
39
|
+
const FEATURE_UNAVAILABLE = 5;
|
|
40
|
+
const MAX_POLL_ROUTES = 4096;
|
|
41
|
+
const MAX_POLL_CONNECTIONS = 256;
|
|
42
|
+
const CONSUMER_SESSION_SIZE = 32;
|
|
43
|
+
const METADATA_WATERMARK_OFFSET = 24;
|
|
44
|
+
const POLL_OPTIONS_SIZE = 9 + 4 + 1;
|
|
33
45
|
/**
|
|
34
46
|
* How long a `TRANSIENT_NOT_ACCEPTED` request replays on the same connection
|
|
35
47
|
* before the roster is re-read. A node that stopped being primary refuses
|
|
@@ -121,6 +133,12 @@ export class CommandResponseStream extends EventEmitter {
|
|
|
121
133
|
heartbeatIntervalHandler;
|
|
122
134
|
/** Whether a heartbeat ping is still awaiting its response */
|
|
123
135
|
heartbeatInFlight;
|
|
136
|
+
rememberedCredentials;
|
|
137
|
+
clustered;
|
|
138
|
+
metadataWatermark = 0n;
|
|
139
|
+
routingGeneration = 0;
|
|
140
|
+
pollRoutes = new Map();
|
|
141
|
+
pollConnections = new Map();
|
|
124
142
|
/**
|
|
125
143
|
* Creates a new CommandResponseStream.
|
|
126
144
|
*
|
|
@@ -209,10 +227,26 @@ export class CommandResponseStream extends EventEmitter {
|
|
|
209
227
|
this.pendingSubmissions += 1;
|
|
210
228
|
try {
|
|
211
229
|
const { handleResponse = true, last = true, followsLeaderMoves = true } = options;
|
|
230
|
+
const autoCommitPoll = command === COMMAND_CODE.PollMessages &&
|
|
231
|
+
payload.length > POLL_OPTIONS_SIZE && payload.at(-1) === 1;
|
|
232
|
+
const pollDeadline = autoCommitPoll
|
|
233
|
+
? options.deadline ?? Date.now() + VSR_RESPONSE_TIMEOUT_MS
|
|
234
|
+
: undefined;
|
|
212
235
|
if (!this.connection.connected)
|
|
213
|
-
await this.connection.connect()
|
|
236
|
+
await (pollDeadline === undefined ? this.connection.connect()
|
|
237
|
+
: withinDeadline(this.connection.connect(), pollDeadline));
|
|
214
238
|
if (!this.isAuthenticated && !this.isUnloggedCommand(command))
|
|
215
|
-
await this.authenticate(this.
|
|
239
|
+
await (pollDeadline === undefined ? this.authenticate(this._signInCredentials())
|
|
240
|
+
: withinDeadline(this.authenticate(this._signInCredentials()), pollDeadline));
|
|
241
|
+
if (pollDeadline !== undefined) {
|
|
242
|
+
const deadline = pollDeadline;
|
|
243
|
+
if (this.clustered === undefined)
|
|
244
|
+
await withinDeadline(this.sendCommand(GET_CLUSTER_METADATA.code, GET_CLUSTER_METADATA.serialize(), { deadline, followsLeaderMoves: false }), deadline);
|
|
245
|
+
if (this.clustered === undefined)
|
|
246
|
+
throw new Error('cannot determine the poll routing topology');
|
|
247
|
+
if (this.clustered)
|
|
248
|
+
return await this._pollOnPrimary(payload, deadline, handleResponse);
|
|
249
|
+
}
|
|
216
250
|
// The roster read is itself a queued command and the queue is
|
|
217
251
|
// single-flighted, so the leader re-check cannot happen inside
|
|
218
252
|
// `_processVsr`. The refusal comes back out here instead, where the
|
|
@@ -289,7 +323,7 @@ export class CommandResponseStream extends EventEmitter {
|
|
|
289
323
|
// command fails client-side, a non-replicated one goes out with
|
|
290
324
|
// session 0.
|
|
291
325
|
if (!this.isAuthenticated && !this.isUnloggedCommand(command))
|
|
292
|
-
await this.authenticate(this.
|
|
326
|
+
await this.authenticate(this._signInCredentials());
|
|
293
327
|
}
|
|
294
328
|
}
|
|
295
329
|
if (!isLoginCommand(command) || this.settlingLeader)
|
|
@@ -424,6 +458,9 @@ export class CommandResponseStream extends EventEmitter {
|
|
|
424
458
|
_rememberRoster(response) {
|
|
425
459
|
try {
|
|
426
460
|
const metadata = GET_CLUSTER_METADATA.deserialize(response);
|
|
461
|
+
if (metadata.nodes.length === 0)
|
|
462
|
+
return;
|
|
463
|
+
this.clustered = metadata.nodes.length > 1;
|
|
427
464
|
this.connection.rememberRoster(metadata.nodes
|
|
428
465
|
.filter((node) => node.endpoints.tcp !== 0)
|
|
429
466
|
.map((node) => ({ host: node.ip, port: node.endpoints.tcp })));
|
|
@@ -432,6 +469,201 @@ export class CommandResponseStream extends EventEmitter {
|
|
|
432
469
|
debug('an unreadable roster leaves the redial candidates as they are', error);
|
|
433
470
|
}
|
|
434
471
|
}
|
|
472
|
+
async _pollOnPrimary(payload, deadline, handleResponse) {
|
|
473
|
+
const key = payload.subarray(0, -POLL_OPTIONS_SIZE).toString('hex');
|
|
474
|
+
while (Date.now() < deadline && !this.connection.ending) {
|
|
475
|
+
try {
|
|
476
|
+
const generation = this.routingGeneration;
|
|
477
|
+
let route = this.pollRoutes.get(key);
|
|
478
|
+
if (!route ||
|
|
479
|
+
route.attachment.readBigUInt64LE(METADATA_WATERMARK_OFFSET) <
|
|
480
|
+
this.metadataWatermark) {
|
|
481
|
+
const response = await withinDeadline(this._pollRoutingControl(payload, deadline), deadline);
|
|
482
|
+
if (generation !== this.routingGeneration)
|
|
483
|
+
throw responseError(COMMAND_CODE.PollMessages, TRANSIENT_NOT_ACCEPTED);
|
|
484
|
+
if (response.data.length < CONSUMER_SESSION_SIZE)
|
|
485
|
+
throw new Error('poll routing response is incomplete');
|
|
486
|
+
const node = deserializeNode(response.data, CONSUMER_SESSION_SIZE);
|
|
487
|
+
if (node.length + CONSUMER_SESSION_SIZE !== response.data.length || !node.data.ip)
|
|
488
|
+
throw new Error('poll routing response has no valid TCP endpoint');
|
|
489
|
+
if (node.data.endpoints.tcp === 0)
|
|
490
|
+
throw responseError(COMMAND_CODE.PollMessages, FEATURE_UNAVAILABLE);
|
|
491
|
+
const attachment = Buffer.from(response.data.subarray(0, CONSUMER_SESSION_SIZE));
|
|
492
|
+
if (attachment.readBigUInt64LE(METADATA_WATERMARK_OFFSET) < this.metadataWatermark)
|
|
493
|
+
attachment.writeBigUInt64LE(this.metadataWatermark, METADATA_WATERMARK_OFFSET);
|
|
494
|
+
route = {
|
|
495
|
+
endpoint: { host: node.data.ip, port: node.data.endpoints.tcp },
|
|
496
|
+
attachment
|
|
497
|
+
};
|
|
498
|
+
if (this.pollRoutes.size >= MAX_POLL_ROUTES)
|
|
499
|
+
this.pollRoutes.clear();
|
|
500
|
+
this.pollRoutes.set(key, route);
|
|
501
|
+
}
|
|
502
|
+
const endpoint = endpointKey(route.endpoint);
|
|
503
|
+
let entry = this.pollConnections.get(endpoint);
|
|
504
|
+
if (!entry) {
|
|
505
|
+
if (this.pollConnections.size >= MAX_POLL_CONNECTIONS) {
|
|
506
|
+
const idle = Array.from(this.pollConnections).find(([, value]) => !value.busy);
|
|
507
|
+
if (!idle)
|
|
508
|
+
throw responseError(COMMAND_CODE.PollMessages, TRANSIENT_NOT_ACCEPTED);
|
|
509
|
+
this.pollConnections.delete(idle[0]);
|
|
510
|
+
idle[1].client.destroy();
|
|
511
|
+
}
|
|
512
|
+
entry = {
|
|
513
|
+
client: new CommandResponseStream({
|
|
514
|
+
...this.options,
|
|
515
|
+
options: { ...this.options.options, ...route.endpoint },
|
|
516
|
+
heartbeatInterval: 0,
|
|
517
|
+
reconnect: { enabled: false, interval: 0, maxRetries: 0 }
|
|
518
|
+
})
|
|
519
|
+
};
|
|
520
|
+
this.pollConnections.set(endpoint, entry);
|
|
521
|
+
}
|
|
522
|
+
while (entry.busy)
|
|
523
|
+
await withinDeadline(entry.busy, deadline);
|
|
524
|
+
if (generation !== this.routingGeneration ||
|
|
525
|
+
this.pollConnections.get(endpoint) !== entry ||
|
|
526
|
+
route.attachment.readBigUInt64LE(METADATA_WATERMARK_OFFSET) <
|
|
527
|
+
this.metadataWatermark)
|
|
528
|
+
throw responseError(COMMAND_CODE.PollMessages, TRANSIENT_NOT_ACCEPTED);
|
|
529
|
+
let release;
|
|
530
|
+
entry.busy = new Promise((resolve) => { release = resolve; });
|
|
531
|
+
let polling = false;
|
|
532
|
+
try {
|
|
533
|
+
if (!entry.client.isAuthenticated) {
|
|
534
|
+
entry.attachment = undefined;
|
|
535
|
+
await withinDeadline(entry.client.connection.connect(true), deadline, () => entry.client.destroy());
|
|
536
|
+
const credentials = this._signInCredentials();
|
|
537
|
+
const login = 'token' in credentials ? LOGIN_WITH_TOKEN : LOGIN;
|
|
538
|
+
const loginPayload = 'token' in credentials
|
|
539
|
+
? LOGIN_WITH_TOKEN.serialize(credentials)
|
|
540
|
+
: LOGIN.serialize(credentials);
|
|
541
|
+
await entry.client._queueCommand(login.code, loginPayload, true, true, false, deadline);
|
|
542
|
+
}
|
|
543
|
+
if (!entry.attachment?.equals(route.attachment)) {
|
|
544
|
+
await entry.client._queueCommand(COMMAND_CODE.AttachConsumerSession, route.attachment, true, true, false, deadline);
|
|
545
|
+
entry.attachment = route.attachment;
|
|
546
|
+
}
|
|
547
|
+
if (generation !== this.routingGeneration ||
|
|
548
|
+
route.attachment.readBigUInt64LE(METADATA_WATERMARK_OFFSET) <
|
|
549
|
+
this.metadataWatermark)
|
|
550
|
+
throw responseError(COMMAND_CODE.PollMessages, TRANSIENT_NOT_ACCEPTED);
|
|
551
|
+
polling = true;
|
|
552
|
+
return await entry.client._queueCommand(COMMAND_CODE.PollMessagesOnPrimary, payload, handleResponse, true, false, deadline);
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
if (error instanceof ResponseError &&
|
|
556
|
+
error.errorCode === TRANSIENT_NOT_ACCEPTED) {
|
|
557
|
+
entry.attachment = undefined;
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
if (this.pollConnections.get(endpoint) === entry)
|
|
561
|
+
this.pollConnections.delete(endpoint);
|
|
562
|
+
entry.client.destroy();
|
|
563
|
+
}
|
|
564
|
+
if (!(error instanceof ResponseError) || error instanceof VsrEvictionError ||
|
|
565
|
+
error.errorCode === UNAUTHENTICATED || error.errorCode === STALE_CLIENT)
|
|
566
|
+
throw responseError(COMMAND_CODE.PollMessages, polling ? TRANSIENT_NOT_COMMITTED : TRANSIENT_NOT_ACCEPTED);
|
|
567
|
+
throw error;
|
|
568
|
+
}
|
|
569
|
+
finally {
|
|
570
|
+
entry.busy = undefined;
|
|
571
|
+
release();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
this.pollRoutes.delete(key);
|
|
576
|
+
if (error instanceof VsrResponseTimeoutError)
|
|
577
|
+
throw responseError(COMMAND_CODE.PollMessages, TRANSIENT_NOT_COMMITTED);
|
|
578
|
+
if (!(error instanceof ResponseError) ||
|
|
579
|
+
error.errorCode !== TRANSIENT_NOT_ACCEPTED)
|
|
580
|
+
throw error instanceof ResponseError
|
|
581
|
+
? responseError(COMMAND_CODE.PollMessages, error.errorCode)
|
|
582
|
+
: error;
|
|
583
|
+
if (!worthAnotherAttempt(deadline))
|
|
584
|
+
throw responseError(COMMAND_CODE.PollMessages, TRANSIENT_NOT_ACCEPTED);
|
|
585
|
+
await delay(VSR_RETRY_INTERVAL_MS);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
throw responseError(COMMAND_CODE.PollMessages, TRANSIENT_NOT_ACCEPTED);
|
|
589
|
+
}
|
|
590
|
+
async _pollRoutingControl(payload, deadline) {
|
|
591
|
+
const options = { deadline, followsLeaderMoves: false };
|
|
592
|
+
try {
|
|
593
|
+
return await this.sendCommand(COMMAND_CODE.GetPollRouting, payload, options);
|
|
594
|
+
}
|
|
595
|
+
catch (error) {
|
|
596
|
+
if (error instanceof ResponseError &&
|
|
597
|
+
error.errorCode !== UNAUTHENTICATED && error.errorCode !== STALE_CLIENT)
|
|
598
|
+
throw error;
|
|
599
|
+
if (Date.now() >= deadline)
|
|
600
|
+
throw error;
|
|
601
|
+
// Only a failed coordinator exchange uses normal session recovery.
|
|
602
|
+
// A healthy coordinator refusing a route must retain its group membership.
|
|
603
|
+
if (error instanceof ResponseError) {
|
|
604
|
+
this.connection.abort();
|
|
605
|
+
this.connection.connected = false;
|
|
606
|
+
this._resetSession();
|
|
607
|
+
}
|
|
608
|
+
await this.sendCommand(PING.code, PING.serialize(), { deadline });
|
|
609
|
+
return this.sendCommand(COMMAND_CODE.GetPollRouting, payload, options);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
_observeMetadataReply(frame) {
|
|
613
|
+
if (frame.length < HEADER_SIZE || peekCommand(frame) !== Command.Reply)
|
|
614
|
+
return;
|
|
615
|
+
const operation = readReplyOperation(frame);
|
|
616
|
+
if (!isKnownOperation(operation) || operation === Operation.NonReplicated ||
|
|
617
|
+
operation === Operation.SendMessages || operation === Operation.StoreConsumerOffset ||
|
|
618
|
+
operation === Operation.DeleteConsumerOffset)
|
|
619
|
+
return;
|
|
620
|
+
const commit = frame.readBigUInt64LE(REPLY_OFFSET.commit);
|
|
621
|
+
if (commit > this.metadataWatermark)
|
|
622
|
+
this.metadataWatermark = commit;
|
|
623
|
+
}
|
|
624
|
+
_rememberCredentials(command, payload) {
|
|
625
|
+
if (command === LOGIN.code) {
|
|
626
|
+
const username = readWireName(payload, 0);
|
|
627
|
+
this.rememberedCredentials = {
|
|
628
|
+
username: username.value, password: readWireName(payload, username.next).value
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
else if (command === LOGIN_WITH_TOKEN.code) {
|
|
632
|
+
this.rememberedCredentials = { token: readWireName(payload, 0).value };
|
|
633
|
+
}
|
|
634
|
+
else if ((command === COMMAND_CODE.ChangePassword || command === COMMAND_CODE.UpdateUser) &&
|
|
635
|
+
this.userId !== undefined && this.rememberedCredentials &&
|
|
636
|
+
'username' in this.rememberedCredentials) {
|
|
637
|
+
const credentials = this.rememberedCredentials;
|
|
638
|
+
const identifier = [serializeIdentifier(this.userId), serializeIdentifier(credentials.username)]
|
|
639
|
+
.find((encoded) => payload.subarray(0, encoded.length).equals(encoded));
|
|
640
|
+
if (!identifier)
|
|
641
|
+
return;
|
|
642
|
+
if (command === COMMAND_CODE.ChangePassword) {
|
|
643
|
+
const currentPassword = readWireName(payload, identifier.length);
|
|
644
|
+
const password = readWireName(payload, currentPassword.next).value;
|
|
645
|
+
this.rememberedCredentials = { ...credentials, password };
|
|
646
|
+
if ('username' in this.options.credentials && this.options.credentials.username === credentials.username)
|
|
647
|
+
this.options.credentials = { ...this.options.credentials, password };
|
|
648
|
+
}
|
|
649
|
+
else if (payload[identifier.length] === 1) {
|
|
650
|
+
const username = readWireName(payload, identifier.length + 1).value;
|
|
651
|
+
this.rememberedCredentials = { ...credentials, username };
|
|
652
|
+
if ('username' in this.options.credentials && this.options.credentials.username === credentials.username)
|
|
653
|
+
this.options.credentials = { ...this.options.credentials, username };
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
_signInCredentials() {
|
|
658
|
+
return this.rememberedCredentials ?? this.options.credentials;
|
|
659
|
+
}
|
|
660
|
+
_clearPollRouting() {
|
|
661
|
+
this.routingGeneration += 1;
|
|
662
|
+
this.pollRoutes.clear();
|
|
663
|
+
for (const entry of this.pollConnections.values())
|
|
664
|
+
entry.client.destroy();
|
|
665
|
+
this.pollConnections.clear();
|
|
666
|
+
}
|
|
435
667
|
/**
|
|
436
668
|
* Processes queued commands sequentially.
|
|
437
669
|
* Emits 'finishQueue' when all commands are processed.
|
|
@@ -535,8 +767,15 @@ export class CommandResponseStream extends EventEmitter {
|
|
|
535
767
|
finally {
|
|
536
768
|
requestWritten ||= exchangeState.written;
|
|
537
769
|
}
|
|
538
|
-
|
|
770
|
+
this._observeMetadataReply(response);
|
|
771
|
+
if (!handleResp) {
|
|
772
|
+
// Routing still needs refusals when the caller decodes the frame.
|
|
773
|
+
if (command === COMMAND_CODE.PollMessagesOnPrimary &&
|
|
774
|
+
peekCommand(response) === Command.Reply &&
|
|
775
|
+
readStatus(response) === TRANSIENT_NOT_ACCEPTED)
|
|
776
|
+
throw responseError(command, TRANSIENT_NOT_ACCEPTED);
|
|
539
777
|
return response;
|
|
778
|
+
}
|
|
540
779
|
try {
|
|
541
780
|
parsed = decodeVsrResponse(response, command);
|
|
542
781
|
break;
|
|
@@ -545,6 +784,10 @@ export class CommandResponseStream extends EventEmitter {
|
|
|
545
784
|
if (!(error instanceof ResponseError) ||
|
|
546
785
|
!isTransientVsrError(error.errorCode))
|
|
547
786
|
throw error;
|
|
787
|
+
if (command === COMMAND_CODE.PollMessagesOnPrimary ||
|
|
788
|
+
command === COMMAND_CODE.GetPollRouting ||
|
|
789
|
+
command === COMMAND_CODE.AttachConsumerSession)
|
|
790
|
+
throw error;
|
|
548
791
|
lastTransientError = error;
|
|
549
792
|
// A not-admitted refusal is a statement about who leads, not about
|
|
550
793
|
// load: a node that stopped being primary refuses forever, so
|
|
@@ -568,7 +811,9 @@ export class CommandResponseStream extends EventEmitter {
|
|
|
568
811
|
this.isAuthenticated = true;
|
|
569
812
|
this.userId = parsed.data.readUInt32LE(0);
|
|
570
813
|
}
|
|
814
|
+
this._rememberCredentials(command, payload);
|
|
571
815
|
if (prepared.command === COMMAND_CODE.LogoutUser) {
|
|
816
|
+
this.rememberedCredentials = undefined;
|
|
572
817
|
this._resetSession();
|
|
573
818
|
}
|
|
574
819
|
// Every roster read feeds the redial candidates, whoever asked for it
|
|
@@ -735,6 +980,8 @@ export class CommandResponseStream extends EventEmitter {
|
|
|
735
980
|
this._execQueue = [];
|
|
736
981
|
}
|
|
737
982
|
_resetSession() {
|
|
983
|
+
this._clearPollRouting();
|
|
984
|
+
this.clustered = undefined;
|
|
738
985
|
if (!this.isAuthenticated &&
|
|
739
986
|
this.userId === undefined &&
|
|
740
987
|
!this.vsrSession.hasActivity)
|
|
@@ -855,6 +1102,8 @@ export class CommandResponseStream extends EventEmitter {
|
|
|
855
1102
|
* Stops heartbeat and destroys the connection.
|
|
856
1103
|
*/
|
|
857
1104
|
destroy() {
|
|
1105
|
+
this.rememberedCredentials = undefined;
|
|
1106
|
+
this._clearPollRouting();
|
|
858
1107
|
if (this.heartbeatIntervalHandler)
|
|
859
1108
|
clearInterval(this.heartbeatIntervalHandler);
|
|
860
1109
|
return this.connection._destroy();
|
|
@@ -873,6 +1122,23 @@ export function getRawClient(options) {
|
|
|
873
1122
|
const isLoginCommand = (command) => command === COMMAND_CODE.LoginUser ||
|
|
874
1123
|
command === COMMAND_CODE.LoginWithAccessToken;
|
|
875
1124
|
const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1125
|
+
const withinDeadline = async (pending, deadline, expired) => {
|
|
1126
|
+
let timer;
|
|
1127
|
+
try {
|
|
1128
|
+
return await Promise.race([
|
|
1129
|
+
pending,
|
|
1130
|
+
new Promise((_resolve, reject) => {
|
|
1131
|
+
timer = setTimeout(() => {
|
|
1132
|
+
expired?.();
|
|
1133
|
+
reject(new VsrResponseTimeoutError(VSR_RESPONSE_TIMEOUT_MS));
|
|
1134
|
+
}, Math.max(0, deadline - Date.now()));
|
|
1135
|
+
})
|
|
1136
|
+
]);
|
|
1137
|
+
}
|
|
1138
|
+
finally {
|
|
1139
|
+
clearTimeout(timer);
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
876
1142
|
const isTransientVsrError = (errorCode) => errorCode === TRANSIENT_NOT_COMMITTED ||
|
|
877
1143
|
errorCode === TRANSIENT_NOT_ACCEPTED;
|
|
878
1144
|
//# sourceMappingURL=client.socket.js.map
|