apache-iggy 0.10.0 → 0.10.1-edge.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/README.md +248 -28
- package/dist/client/client.config.d.ts +2 -0
- package/dist/client/client.config.js +13 -2
- package/dist/client/client.config.test.js +15 -2
- package/dist/client/client.connection-string.js +13 -1
- package/dist/client/client.connection-string.test.js +43 -1
- package/dist/client/client.connection.d.ts +33 -135
- package/dist/client/client.connection.js +190 -453
- package/dist/client/client.connection.test.js +244 -669
- package/dist/client/client.d.ts +4 -35
- package/dist/client/client.js +8 -101
- package/dist/client/client.operation.d.ts +120 -0
- package/dist/client/client.operation.js +428 -0
- package/dist/client/client.operation.test.d.ts +2 -0
- package/dist/client/client.operation.test.js +285 -0
- package/dist/client/client.socket.d.ts +92 -209
- package/dist/client/client.socket.js +1205 -981
- package/dist/client/client.socket.test.js +4865 -679
- package/dist/client/client.type.d.ts +65 -22
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client.e2e.js +3 -5
- package/dist/e2e/resource-options.e2e.d.ts +2 -0
- package/dist/{wire/stream/purge-stream.command.js → e2e/resource-options.e2e.js} +12 -20
- package/dist/e2e/resource-options.suite.d.ts +15 -0
- package/dist/e2e/resource-options.suite.js +272 -0
- package/dist/e2e/tcp.cluster.e2e.js +1 -0
- package/dist/e2e/tcp.consumer-stream.e2e.js +4 -0
- package/dist/e2e/tcp.parallel.e2e.js +5 -6
- package/dist/e2e/tcp.stream.e2e.js +0 -3
- package/dist/e2e/tcp.system.e2e.js +10 -1
- package/dist/e2e/tcp.token.e2e.js +8 -2
- package/dist/e2e/tcp.topic.e2e.js +0 -3
- package/dist/e2e/tcp.user.e2e.js +46 -3
- package/dist/e2e/tls.system.e2e.js +21 -12
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/stream/consumer-stream.js +97 -12
- package/dist/wire/cluster/cluster.type.d.ts +2 -2
- package/dist/wire/cluster/cluster.utils.d.ts +1 -0
- package/dist/wire/cluster/cluster.utils.js +4 -1
- package/dist/wire/command-set.d.ts +8 -8
- package/dist/wire/command-set.js +18 -9
- package/dist/wire/command.code.d.ts +3 -10
- package/dist/wire/command.code.js +9 -11
- package/dist/wire/command.utils.d.ts +3 -2
- package/dist/wire/command.utils.js +3 -2
- package/dist/wire/error.code.js +17 -1
- package/dist/wire/error.code.test.js +28 -1
- package/dist/wire/error.utils.d.ts +4 -2
- package/dist/wire/error.utils.js +6 -3
- package/dist/wire/identifier.utils.d.ts +2 -0
- package/dist/wire/identifier.utils.js +2 -0
- package/dist/wire/index.d.ts +1 -0
- package/dist/wire/index.js +1 -0
- package/dist/wire/message/header.type.d.ts +1 -1
- package/dist/wire/message/message-batch.test.js +11 -5
- package/dist/wire/message/message.utils.d.ts +0 -10
- package/dist/wire/message/message.utils.js +1 -22
- package/dist/wire/message/poll-messages.command.d.ts +12 -1
- package/dist/wire/message/poll-messages.command.js +64 -16
- package/dist/wire/message/poll-messages.command.test.js +143 -5
- package/dist/wire/message/poll.utils.d.ts +4 -0
- package/dist/wire/message/poll.utils.js +6 -1
- package/dist/wire/message/send-messages.command.d.ts +3 -3
- package/dist/wire/message/send-messages.command.js +103 -3
- package/dist/wire/message/send-messages.command.test.js +113 -2
- package/dist/wire/offset/delete-offset.command.d.ts +11 -4
- package/dist/wire/offset/delete-offset.command.js +1 -1
- package/dist/wire/offset/get-offset.command.d.ts +1 -1
- package/dist/wire/offset/offset-write.command.test.d.ts +2 -0
- package/dist/wire/offset/offset-write.command.test.js +43 -0
- package/dist/wire/offset/offset.utils.js +10 -6
- package/dist/wire/offset/store-offset.command.d.ts +10 -0
- package/dist/wire/offset/store-offset.command.js +1 -1
- package/dist/wire/options.utils.d.ts +46 -0
- package/dist/wire/options.utils.js +57 -0
- package/dist/wire/partition/partition-route.d.ts +12 -0
- package/dist/wire/partition/partition-route.js +40 -0
- package/dist/wire/partition/partition-route.test.d.ts +2 -0
- package/dist/wire/partition/partition-route.test.js +42 -0
- package/dist/wire/session/login-with-token.command.d.ts +2 -2
- package/dist/wire/session/login-with-token.command.js +7 -2
- package/dist/wire/session/login.command.d.ts +2 -2
- package/dist/wire/session/login.command.js +7 -2
- package/dist/wire/stream/create-stream.command.d.ts +5 -2
- package/dist/wire/stream/create-stream.command.js +20 -2
- package/dist/wire/stream/index.d.ts +1 -1
- package/dist/wire/stream/index.js +1 -1
- package/dist/wire/stream/stream-options.d.ts +77 -0
- package/dist/wire/stream/stream-options.js +101 -0
- package/dist/wire/stream/stream-options.test.d.ts +2 -0
- package/dist/wire/stream/stream-options.test.js +103 -0
- package/dist/wire/stream/stream.utils.d.ts +9 -0
- package/dist/wire/stream/stream.utils.js +17 -3
- package/dist/wire/stream/update-stream.command.d.ts +23 -5
- package/dist/wire/stream/update-stream.command.js +10 -17
- package/dist/wire/system/describe-options.command.d.ts +23 -6
- package/dist/wire/system/describe-options.command.js +21 -2
- package/dist/wire/system/get-stats.command.d.ts +17 -0
- package/dist/wire/system/get-stats.command.js +69 -16
- package/dist/wire/system/get-stats.command.test.d.ts +2 -0
- package/dist/wire/system/get-stats.command.test.js +97 -0
- package/dist/wire/system/index.d.ts +2 -0
- package/dist/wire/system/index.js +2 -0
- package/dist/wire/system/snapshot.command.d.ts +64 -0
- package/dist/wire/system/snapshot.command.js +76 -0
- package/dist/wire/system/snapshot.command.test.d.ts +2 -0
- package/dist/wire/system/snapshot.command.test.js +86 -0
- package/dist/wire/system/update-resource.command.d.ts +53 -0
- package/dist/wire/system/update-resource.command.js +130 -0
- package/dist/wire/system/update-resource.command.test.d.ts +2 -0
- package/dist/wire/system/update-resource.command.test.js +497 -0
- package/dist/wire/token/create-token.command.d.ts +1 -1
- package/dist/wire/token/create-token.command.js +1 -1
- package/dist/wire/token/create-token.command.test.js +6 -0
- package/dist/wire/topic/create-topic.command.d.ts +20 -12
- package/dist/wire/topic/create-topic.command.js +11 -4
- package/dist/wire/topic/index.d.ts +1 -2
- package/dist/wire/topic/index.js +1 -2
- package/dist/wire/topic/topic.utils.d.ts +14 -5
- package/dist/wire/topic/topic.utils.js +26 -11
- package/dist/wire/topic/topic.utils.test.js +13 -13
- package/dist/wire/topic/update-topic.command.d.ts +22 -15
- package/dist/wire/topic/update-topic.command.js +12 -34
- package/dist/wire/user/create-user.command.d.ts +12 -2
- package/dist/wire/user/create-user.command.js +7 -3
- package/dist/wire/user/index.d.ts +1 -0
- package/dist/wire/user/index.js +1 -0
- package/dist/wire/user/update-user.command.d.ts +25 -7
- package/dist/wire/user/update-user.command.js +18 -31
- package/dist/wire/user/user.utils.d.ts +2 -0
- package/dist/wire/user/user.utils.js +5 -3
- package/dist/wire/vsr/header.d.ts +23 -8
- package/dist/wire/vsr/header.js +32 -10
- package/dist/wire/vsr/header.test.js +5 -1
- package/dist/wire/vsr/index.d.ts +8 -2
- package/dist/wire/vsr/index.js +44 -11
- package/dist/wire/vsr/operation.d.ts +18 -6
- package/dist/wire/vsr/operation.js +46 -16
- package/dist/wire/vsr/operation.test.js +32 -8
- package/dist/wire/vsr/register.d.ts +10 -6
- package/dist/wire/vsr/register.js +24 -7
- package/dist/wire/vsr/register.test.js +28 -8
- package/dist/wire/vsr/reply.d.ts +4 -1
- package/dist/wire/vsr/reply.js +42 -7
- package/dist/wire/vsr/reply.test.js +11 -1
- package/dist/wire/vsr/session.d.ts +9 -10
- package/dist/wire/vsr/session.js +32 -23
- package/dist/wire/vsr/session.test.js +33 -7
- package/dist/wire/vsr/vsr.test.js +57 -6
- package/package.json +6 -7
- package/dist/wire/message/flush-unsaved-buffers.command.d.ts +0 -26
- package/dist/wire/message/flush-unsaved-buffers.command.js +0 -36
- package/dist/wire/stream/purge-stream.command.d.ts +0 -22
- package/dist/wire/topic/purge-topic.command.d.ts +0 -24
- package/dist/wire/topic/purge-topic.command.js +0 -39
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
// or more contributor license agreements. See the NOTICE file
|
|
3
|
+
// distributed with this work for additional information
|
|
4
|
+
// regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
// to you under the Apache License, Version 2.0 (the
|
|
6
|
+
// "License"); you may not use this file except in compliance
|
|
7
|
+
// with the License. You may obtain a copy of the License at
|
|
8
|
+
//
|
|
9
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
//
|
|
11
|
+
// Unless required by applicable law or agreed to in writing,
|
|
12
|
+
// software distributed under the License is distributed on an
|
|
13
|
+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
// KIND, either express or implied. See the License for the
|
|
15
|
+
// specific language governing permissions and limitations
|
|
16
|
+
// under the License.
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { describe, it } from 'node:test';
|
|
19
|
+
import { COMMAND_CODE } from '../wire/command.code.js';
|
|
20
|
+
import { VsrSession } from '../wire/vsr/index.js';
|
|
21
|
+
import { HEADER_SIZE, REQUEST_OFFSET, EMPTY_PARTITION_CONTEXT } from '../wire/vsr/header.js';
|
|
22
|
+
import { MutationLanes, LaneBlockedError, OperationNotRetainedError } from './client.operation.js';
|
|
23
|
+
import { ResponseError } from '../wire/error.utils.js';
|
|
24
|
+
const body = Buffer.from('frozen body');
|
|
25
|
+
const frameSize = HEADER_SIZE + body.length;
|
|
26
|
+
const session = () => { const state = new VsrSession(1n); state.bind(42n); return state; };
|
|
27
|
+
const enter = (lanes, state, lane, signal = new AbortController().signal, deadline = Date.now() + 1_000) => lanes.enter(lane, COMMAND_CODE.CreateStream, body, EMPTY_PARTITION_CONTEXT, true, state, deadline, signal);
|
|
28
|
+
describe('mutation admission and retained ownership', () => {
|
|
29
|
+
it('retains the request identifier for a replay and mints one for a restart', async () => {
|
|
30
|
+
const lanes = new MutationLanes(1, frameSize);
|
|
31
|
+
const state = session();
|
|
32
|
+
const operation = await enter(lanes, state, 'metadata');
|
|
33
|
+
const span = [REQUEST_OFFSET.requestChecksum, REQUEST_OFFSET.requestChecksum + 16];
|
|
34
|
+
const first = Buffer.from(operation.frame.buffer.subarray(...span));
|
|
35
|
+
const replay = operation.frame.buffer;
|
|
36
|
+
assert.deepEqual(replay.subarray(...span), first);
|
|
37
|
+
await lanes.restart(operation, body, EMPTY_PARTITION_CONTEXT, state, Date.now() + 1_000, new AbortController().signal);
|
|
38
|
+
assert.notDeepEqual(operation.frame.buffer.subarray(...span), first);
|
|
39
|
+
lanes.complete(operation);
|
|
40
|
+
});
|
|
41
|
+
it('retains one lane while independent groups keep their own order', async () => {
|
|
42
|
+
const lanes = new MutationLanes(3, 3 * frameSize);
|
|
43
|
+
const state = session();
|
|
44
|
+
const first = await enter(lanes, state, 'first');
|
|
45
|
+
first.uncertain = true;
|
|
46
|
+
lanes.retain(first);
|
|
47
|
+
await assert.rejects(enter(lanes, state, 'first'), (error) => error instanceof LaneBlockedError && error.operationId === first.id);
|
|
48
|
+
const other = await enter(lanes, state, 'other');
|
|
49
|
+
let admitted = false;
|
|
50
|
+
const next = enter(lanes, state, 'other').then((operation) => { admitted = true; return operation; });
|
|
51
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
52
|
+
assert.equal(admitted, false);
|
|
53
|
+
lanes.complete(other);
|
|
54
|
+
lanes.complete(await next);
|
|
55
|
+
assert.equal(lanes.retained(first.id), first);
|
|
56
|
+
lanes.complete(first);
|
|
57
|
+
});
|
|
58
|
+
it('preserves FIFO when waiters at the head, middle and tail cancel', async () => {
|
|
59
|
+
const count = 512;
|
|
60
|
+
const lanes = new MutationLanes(1, frameSize);
|
|
61
|
+
const state = session();
|
|
62
|
+
const active = await enter(lanes, state, 'queued');
|
|
63
|
+
const controllers = Array.from({ length: count }, () => new AbortController());
|
|
64
|
+
const admitted = [];
|
|
65
|
+
const pending = controllers.map((controller, index) => enter(lanes, state, 'queued', controller.signal, Date.now() + 5_000).then((operation) => {
|
|
66
|
+
admitted.push(index);
|
|
67
|
+
lanes.complete(operation);
|
|
68
|
+
}, (error) => { assert.equal(controller.signal.aborted, true, String(error)); }));
|
|
69
|
+
const cancelled = new Set([0, Math.floor(count / 2), count - 1]);
|
|
70
|
+
for (const index of cancelled)
|
|
71
|
+
controllers[index].abort();
|
|
72
|
+
lanes.complete(active);
|
|
73
|
+
await Promise.all(pending);
|
|
74
|
+
assert.deepEqual(admitted, controllers.map((_, index) => index).filter((index) => !cancelled.has(index)));
|
|
75
|
+
});
|
|
76
|
+
it('rejects a mutation that cannot fit a positive byte budget', async () => {
|
|
77
|
+
const lanes = new MutationLanes(1, 1);
|
|
78
|
+
await assert.rejects(enter(lanes, session(), 'metadata'), ResponseError);
|
|
79
|
+
});
|
|
80
|
+
for (const limit of ['count', 'bytes']) {
|
|
81
|
+
it(`keeps progress polls ordered without consuming the full mutation ${limit} budget`, async () => {
|
|
82
|
+
const lanes = new MutationLanes(limit === 'count' ? 1 : 3, (limit === 'bytes' ? 1 : 3) * frameSize);
|
|
83
|
+
const state = session();
|
|
84
|
+
const cancel = new AbortController();
|
|
85
|
+
const pending = [];
|
|
86
|
+
const enterProgress = (lane) => lanes.enter(lane, COMMAND_CODE.PollMessages, body, EMPTY_PARTITION_CONTEXT, false, state, Date.now() + 1_000, cancel.signal);
|
|
87
|
+
try {
|
|
88
|
+
const retained = await enter(lanes, state, 'retained');
|
|
89
|
+
retained.uncertain = true;
|
|
90
|
+
lanes.retain(retained);
|
|
91
|
+
await assert.rejects(enterProgress('retained'), (error) => error instanceof LaneBlockedError && error.operationId === retained.id);
|
|
92
|
+
const first = await enterProgress('progress');
|
|
93
|
+
const originalId = first.id;
|
|
94
|
+
await lanes.restart(first, body, EMPTY_PARTITION_CONTEXT, state, Date.now() + 1_000, cancel.signal);
|
|
95
|
+
assert.equal(first.id, originalId + 1n);
|
|
96
|
+
let progressAdmitted = false;
|
|
97
|
+
const progress = enterProgress('progress').then((operation) => { progressAdmitted = true; return operation; });
|
|
98
|
+
pending.push(progress);
|
|
99
|
+
let mutationAdmitted = false;
|
|
100
|
+
const mutation = enter(lanes, state, 'other', cancel.signal).then((operation) => { mutationAdmitted = true; return operation; });
|
|
101
|
+
pending.push(mutation);
|
|
102
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
103
|
+
assert.equal(progressAdmitted, false);
|
|
104
|
+
assert.equal(mutationAdmitted, false);
|
|
105
|
+
lanes.complete(first);
|
|
106
|
+
lanes.complete(await progress);
|
|
107
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
108
|
+
assert.equal(mutationAdmitted, false);
|
|
109
|
+
lanes.complete(retained);
|
|
110
|
+
lanes.complete(await mutation);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
cancel.abort();
|
|
114
|
+
lanes.reset();
|
|
115
|
+
await Promise.allSettled(pending);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
it('admits an independent lane when only a later same-lane waiter fits the free bytes', async () => {
|
|
120
|
+
const sizes = { retained: 900, active: 300, large: 700, small: 500, independent: 500 };
|
|
121
|
+
const lanes = new MutationLanes(2, 1_500);
|
|
122
|
+
const state = session();
|
|
123
|
+
const cancel = new AbortController();
|
|
124
|
+
const pending = [];
|
|
125
|
+
const admitted = [];
|
|
126
|
+
const enterFrame = (lane, size) => lanes.enter(lane, COMMAND_CODE.CreateStream, Buffer.alloc(size - HEADER_SIZE), EMPTY_PARTITION_CONTEXT, true, state, Date.now() + 1_000, cancel.signal);
|
|
127
|
+
try {
|
|
128
|
+
const retained = await enterFrame('retained', sizes.retained);
|
|
129
|
+
retained.uncertain = true;
|
|
130
|
+
lanes.retain(retained);
|
|
131
|
+
const active = await enterFrame('active', sizes.active);
|
|
132
|
+
const large = enterFrame('queued', sizes.large).then((operation) => { admitted.push('large'); return operation; });
|
|
133
|
+
const small = enterFrame('queued', sizes.small).then((operation) => { admitted.push('small'); return operation; });
|
|
134
|
+
const independent = enterFrame('independent', sizes.independent).then((operation) => { admitted.push('independent'); return operation; });
|
|
135
|
+
pending.push(large, small, independent);
|
|
136
|
+
lanes.complete(active);
|
|
137
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
138
|
+
assert.deepEqual(admitted, ['independent']);
|
|
139
|
+
lanes.complete(await independent);
|
|
140
|
+
lanes.complete(retained);
|
|
141
|
+
lanes.complete(await large);
|
|
142
|
+
lanes.complete(await small);
|
|
143
|
+
assert.deepEqual(admitted, ['independent', 'large', 'small']);
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
cancel.abort();
|
|
147
|
+
lanes.reset();
|
|
148
|
+
await Promise.allSettled(pending);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
it('releases logical count immediately but pins bytes through the final writer alias', async () => {
|
|
152
|
+
const lanes = new MutationLanes(1, 2 * frameSize);
|
|
153
|
+
const state = session();
|
|
154
|
+
const first = await enter(lanes, state, 'first');
|
|
155
|
+
first.frame.retain();
|
|
156
|
+
lanes.complete(first);
|
|
157
|
+
const second = await enter(lanes, state, 'second');
|
|
158
|
+
assert.equal(second.id, first.id + 1n);
|
|
159
|
+
second.frame.retain();
|
|
160
|
+
lanes.complete(second);
|
|
161
|
+
let admitted = false;
|
|
162
|
+
const third = enter(lanes, state, 'third').then((operation) => { admitted = true; return operation; });
|
|
163
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
164
|
+
assert.equal(admitted, false);
|
|
165
|
+
first.frame.release();
|
|
166
|
+
lanes.complete(await third);
|
|
167
|
+
second.frame.release();
|
|
168
|
+
});
|
|
169
|
+
it('checks oversize and expired admission before allocating an operation identity', async () => {
|
|
170
|
+
const state = session();
|
|
171
|
+
const lanes = new MutationLanes(1, HEADER_SIZE);
|
|
172
|
+
await assert.rejects(enter(lanes, state, 'oversize'), (error) => error instanceof ResponseError && error.errorCode === 80);
|
|
173
|
+
assert.equal(state.encode(COMMAND_CODE.Ping, Buffer.alloc(0)).readBigUInt64LE(REQUEST_OFFSET.request), 1n);
|
|
174
|
+
const expired = new MutationLanes(1, frameSize);
|
|
175
|
+
await assert.rejects(enter(expired, state, 'expired', undefined, Date.now() - 1), (error) => error instanceof ResponseError && error.errorCode === 58);
|
|
176
|
+
assert.equal(state.encode(COMMAND_CODE.Ping, Buffer.alloc(0)).readBigUInt64LE(REQUEST_OFFSET.request), 2n);
|
|
177
|
+
});
|
|
178
|
+
it('reports a queued admission deadline as not accepted without consuming an identity', async () => {
|
|
179
|
+
const state = session();
|
|
180
|
+
const lanes = new MutationLanes(1, frameSize);
|
|
181
|
+
const first = await enter(lanes, state, 'first');
|
|
182
|
+
await assert.rejects(enter(lanes, state, 'second', undefined, Date.now() + 10), (error) => error instanceof ResponseError && error.errorCode === 58);
|
|
183
|
+
lanes.complete(first);
|
|
184
|
+
const next = await enter(lanes, state, 'second');
|
|
185
|
+
assert.equal(next.id, first.id + 1n);
|
|
186
|
+
lanes.complete(next);
|
|
187
|
+
});
|
|
188
|
+
it('keeps a restarting lane while its old writer pins the entire byte budget', async () => {
|
|
189
|
+
const state = session();
|
|
190
|
+
const lanes = new MutationLanes(1, frameSize);
|
|
191
|
+
const first = await enter(lanes, state, 'first');
|
|
192
|
+
const originalId = first.id;
|
|
193
|
+
const writer = first.frame;
|
|
194
|
+
writer.retain();
|
|
195
|
+
let restarted = false;
|
|
196
|
+
const restart = lanes.restart(first, body, EMPTY_PARTITION_CONTEXT, state, Date.now() + 1_000, first.lifetime.signal)
|
|
197
|
+
.then(() => { restarted = true; });
|
|
198
|
+
let admitted = false;
|
|
199
|
+
const later = enter(lanes, state, 'first').then((operation) => { admitted = true; return operation; });
|
|
200
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
201
|
+
assert.equal(restarted, false);
|
|
202
|
+
assert.equal(admitted, false);
|
|
203
|
+
writer.release();
|
|
204
|
+
await restart;
|
|
205
|
+
assert.equal(first.id, originalId + 1n);
|
|
206
|
+
assert.equal(admitted, false);
|
|
207
|
+
lanes.complete(first);
|
|
208
|
+
const next = await later;
|
|
209
|
+
assert.equal(next.id, first.id + 1n);
|
|
210
|
+
lanes.complete(next);
|
|
211
|
+
lanes.complete(await enter(lanes, state, 'independent'));
|
|
212
|
+
});
|
|
213
|
+
it('keeps a restart through a reset and frees the bytes of its old writer once', async () => {
|
|
214
|
+
const state = session();
|
|
215
|
+
const lanes = new MutationLanes(1, frameSize);
|
|
216
|
+
const first = await enter(lanes, state, 'first');
|
|
217
|
+
const id = first.id;
|
|
218
|
+
const writer = first.frame;
|
|
219
|
+
writer.retain();
|
|
220
|
+
const restart = lanes.restart(first, body, EMPTY_PARTITION_CONTEXT, state, Date.now() + 1_000, first.lifetime.signal);
|
|
221
|
+
lanes.reset();
|
|
222
|
+
writer.release();
|
|
223
|
+
await restart;
|
|
224
|
+
assert.ok(lanes.owns(first));
|
|
225
|
+
assert.notEqual(first.id, id);
|
|
226
|
+
lanes.complete(first);
|
|
227
|
+
lanes.complete(await enter(lanes, state, 'first'));
|
|
228
|
+
});
|
|
229
|
+
it('cancels budget waiters without copying bodies or consuming identities', async () => {
|
|
230
|
+
const state = session();
|
|
231
|
+
const lanes = new MutationLanes(1, frameSize);
|
|
232
|
+
const first = await enter(lanes, state, 'first');
|
|
233
|
+
const cancel = new AbortController();
|
|
234
|
+
const waiting = enter(lanes, state, 'other', cancel.signal);
|
|
235
|
+
cancel.abort();
|
|
236
|
+
await assert.rejects(waiting);
|
|
237
|
+
lanes.complete(first);
|
|
238
|
+
const admitted = await enter(lanes, state, 'other');
|
|
239
|
+
assert.equal(admitted.id, first.id + 1n);
|
|
240
|
+
lanes.complete(admitted);
|
|
241
|
+
});
|
|
242
|
+
it('serializes concurrent resolvers and frees the slot but not the lane of an abandoned running replay', async () => {
|
|
243
|
+
const lanes = new MutationLanes(1, 3 * frameSize);
|
|
244
|
+
const state = session();
|
|
245
|
+
const operation = await enter(lanes, state, 'first');
|
|
246
|
+
operation.uncertain = true;
|
|
247
|
+
lanes.retain(operation);
|
|
248
|
+
const first = await lanes.acquireResolution(operation, Date.now() + 1_000, operation.lifetime.signal);
|
|
249
|
+
const waiting = lanes.acquireResolution(operation, Date.now() + 1_000, operation.lifetime.signal);
|
|
250
|
+
lanes.abandon(operation.id);
|
|
251
|
+
assert.throws(() => lanes.retained(operation.id), OperationNotRetainedError);
|
|
252
|
+
lanes.complete(await enter(lanes, state, 'other', undefined, Date.now() + 200));
|
|
253
|
+
let admitted = false;
|
|
254
|
+
const next = enter(lanes, state, 'first').then((later) => { admitted = true; return later; });
|
|
255
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
256
|
+
assert.equal(admitted, false);
|
|
257
|
+
first();
|
|
258
|
+
await assert.rejects(waiting, OperationNotRetainedError);
|
|
259
|
+
lanes.complete(await next);
|
|
260
|
+
const held = await enter(lanes, state, 'other');
|
|
261
|
+
await assert.rejects(enter(lanes, state, 'last', undefined, Date.now() + 20), (error) => error instanceof ResponseError && error.errorCode === 58);
|
|
262
|
+
lanes.complete(held);
|
|
263
|
+
});
|
|
264
|
+
it('clears retained admission on reset without losing writer byte ownership', async () => {
|
|
265
|
+
const state = session();
|
|
266
|
+
const lanes = new MutationLanes(1, frameSize);
|
|
267
|
+
const old = await enter(lanes, state, 'first');
|
|
268
|
+
old.frame.retain();
|
|
269
|
+
old.uncertain = true;
|
|
270
|
+
lanes.retain(old);
|
|
271
|
+
lanes.reset();
|
|
272
|
+
assert.throws(() => lanes.retained(old.id), OperationNotRetainedError);
|
|
273
|
+
state.reset();
|
|
274
|
+
state.bind(43n);
|
|
275
|
+
let admitted = false;
|
|
276
|
+
const fresh = enter(lanes, state, 'fresh').then((operation) => { admitted = true; return operation; });
|
|
277
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
278
|
+
assert.equal(admitted, false);
|
|
279
|
+
old.frame.release();
|
|
280
|
+
const next = await fresh;
|
|
281
|
+
assert.ok(next.id > old.id);
|
|
282
|
+
lanes.complete(next);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
//# sourceMappingURL=client.operation.test.js.map
|
|
@@ -1,233 +1,116 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import type { ClientConfigOrString, ClientCredentials, CommandResponse, PasswordCredentials, RawClient, SendCommandOptions, TokenCredentials } from '
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Manages command execution and response handling for the Iggy server.
|
|
8
|
-
* Implements command queuing, authentication, and heartbeat functionality.
|
|
9
|
-
*/
|
|
2
|
+
import type { ClientConfigOrString, ClientCredentials, CommandResponse, PasswordCredentials, RawClient, SendCommandOptions, TokenCredentials } from './client.type.js';
|
|
3
|
+
export { LaneBlockedError, NotConnectedError, OperationNotRetainedError, OperationUncertainError, VsrResponseTimeoutError } from './client.operation.js';
|
|
4
|
+
/** One logical session owns every link, mutation lane and retained operation. */
|
|
10
5
|
export declare class CommandResponseStream extends EventEmitter {
|
|
11
|
-
|
|
12
|
-
private
|
|
13
|
-
|
|
14
|
-
private
|
|
15
|
-
|
|
16
|
-
private
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
|
|
6
|
+
private readonly options;
|
|
7
|
+
private readonly vsrSession;
|
|
8
|
+
private readonly lanes;
|
|
9
|
+
private readonly replicas;
|
|
10
|
+
private readonly partitionRoutes;
|
|
11
|
+
private readonly changed;
|
|
12
|
+
private readonly lifetime;
|
|
13
|
+
private sessionLifetime;
|
|
14
|
+
private bootstrap?;
|
|
15
|
+
private bootstrapPromise?;
|
|
16
|
+
/**
|
|
17
|
+
* What the running bootstrap reports when it gives up: the bind failure of
|
|
18
|
+
* the last endpoint that accepted a dial, else NotConnectedError with the
|
|
19
|
+
* last dial failure.
|
|
20
|
+
*/
|
|
21
|
+
private bootstrapFailure?;
|
|
20
22
|
private authenticationPromise?;
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
* The leader re-check a refused request started, shared with every other
|
|
25
|
-
* request refused by the same node so one demotion moves the client once.
|
|
26
|
-
*/
|
|
27
|
-
private leaderMoveInFlight?;
|
|
23
|
+
private sessionAction;
|
|
24
|
+
private recoveryPromise?;
|
|
25
|
+
private rosterPromise?;
|
|
28
26
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* the
|
|
27
|
+
* Whom the client signs in as by itself: the caller's last sign-in, else the
|
|
28
|
+
* configured credentials while the caller never signed in. A logout, a
|
|
29
|
+
* sign-in of the caller that fails and a refused sign-in clear it.
|
|
32
30
|
*/
|
|
33
|
-
private
|
|
31
|
+
private signInCredentials?;
|
|
34
32
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* command re-checks the leader, and a command written in the meantime goes
|
|
38
|
-
* out on the socket that check is about to replace.
|
|
33
|
+
* Raised by each sign-in and sign-out of the caller. A call continues under
|
|
34
|
+
* a new session only while the lineage it started with is unchanged.
|
|
39
35
|
*/
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
36
|
+
private sessionLineage;
|
|
37
|
+
private knownEndpoints;
|
|
38
|
+
private readonly configuredEndpoint;
|
|
39
|
+
private metadataReplica?;
|
|
40
|
+
private metadataWatermark;
|
|
41
|
+
private routingGeneration;
|
|
42
|
+
private activeDials;
|
|
46
43
|
private pendingSubmissions;
|
|
47
|
-
|
|
48
|
-
busy: boolean;
|
|
49
|
-
/** Whether the client has been authenticated */
|
|
44
|
+
private heartbeatIntervalHandler?;
|
|
50
45
|
isAuthenticated: boolean;
|
|
51
|
-
/** Authenticated user ID */
|
|
52
46
|
userId?: number;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
private heartbeatInFlight;
|
|
57
|
-
private rememberedCredentials?;
|
|
58
|
-
private clustered?;
|
|
59
|
-
private metadataWatermark;
|
|
60
|
-
private routingGeneration;
|
|
61
|
-
private pollRoutes;
|
|
62
|
-
private pollConnections;
|
|
63
|
-
/**
|
|
64
|
-
* Creates a new CommandResponseStream.
|
|
65
|
-
*
|
|
66
|
-
* @param options - Client configuration
|
|
67
|
-
*/
|
|
68
|
-
constructor(options: ClientConfigOrString);
|
|
69
|
-
/**
|
|
70
|
-
* Initializes the stream by setting up heartbeat and connection event handlers.
|
|
71
|
-
*/
|
|
72
|
-
_init(): void;
|
|
73
|
-
/**
|
|
74
|
-
* Re-submits queued commands through the full send path, so each one
|
|
75
|
-
* reconnects, re-authenticates and re-checks the leader as if it had just
|
|
76
|
-
* been called.
|
|
77
|
-
*
|
|
78
|
-
* Only for a drop the client caused. Nothing here was written, so there is no
|
|
79
|
-
* outcome in doubt: a command still in the queue when the socket is replaced
|
|
80
|
-
* would otherwise fail with a lost-connection error the caller can do nothing
|
|
81
|
-
* about.
|
|
82
|
-
*/
|
|
83
|
-
private _reissueQueue;
|
|
84
|
-
/**
|
|
85
|
-
* Sends a command to the server.
|
|
86
|
-
* Automatically handles connection, authentication and leader settlement.
|
|
87
|
-
*
|
|
88
|
-
* @param command - Command code to send
|
|
89
|
-
* @param payload - Command payload buffer
|
|
90
|
-
* @param options - Response and queue options
|
|
91
|
-
* @returns Promise resolving to the command response
|
|
92
|
-
*/
|
|
47
|
+
constructor(config: ClientConfigOrString);
|
|
48
|
+
get lineage(): number;
|
|
49
|
+
get busy(): boolean;
|
|
93
50
|
sendCommand(command: number, payload: Buffer, options?: SendCommandOptions): Promise<CommandResponse>;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
private
|
|
110
|
-
|
|
111
|
-
private
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
private
|
|
51
|
+
resolveOperation(operationId: bigint, options?: SendCommandOptions): Promise<void>;
|
|
52
|
+
abandonOperation(operationId: bigint): void;
|
|
53
|
+
private replay;
|
|
54
|
+
private drive;
|
|
55
|
+
private exchange;
|
|
56
|
+
private control;
|
|
57
|
+
private routeFor;
|
|
58
|
+
/**
|
|
59
|
+
* Whether the replica that answered a namespace miss on `link` is the
|
|
60
|
+
* metadata primary. The belief can predate a view change, so one roster read
|
|
61
|
+
* on `link`, without a retry or another link, must still name that replica.
|
|
62
|
+
* No lookup reaches a believed metadata primary without a link, so then that
|
|
63
|
+
* roster also moves the belief.
|
|
64
|
+
*/
|
|
65
|
+
private isMetadataPrimary;
|
|
66
|
+
private rememberRoute;
|
|
67
|
+
private invalidateRoute;
|
|
68
|
+
private primaryLink;
|
|
69
|
+
private readyLink;
|
|
70
|
+
private refreshRoster;
|
|
71
|
+
private installRoster;
|
|
72
|
+
private runReplica;
|
|
73
|
+
/**
|
|
74
|
+
* Only a heartbeat restarts a link that used its budget, so without one the
|
|
75
|
+
* link stays down for good.
|
|
76
|
+
*/
|
|
77
|
+
private budgetFailure;
|
|
78
|
+
private connectionFor;
|
|
79
|
+
private connectBootstrap;
|
|
80
|
+
private serializeSession;
|
|
81
|
+
private login;
|
|
82
|
+
private recoverSession;
|
|
83
|
+
/**
|
|
84
|
+
* Waits for the sign-in that runs or is queued now, and fails unless it binds
|
|
85
|
+
* a session of the call's lineage. Only the client's own sign-in continues a
|
|
86
|
+
* call: after a sign-in or sign-out of the caller, or a refused sign-in, the
|
|
87
|
+
* call ends with 40, and at its deadline with 58.
|
|
88
|
+
*/
|
|
89
|
+
private nextSession;
|
|
90
|
+
/**
|
|
91
|
+
* The `sessionReset` event says whether the reset ended a signed-in session.
|
|
92
|
+
* A reset after a refused sign-in ends none.
|
|
93
|
+
*/
|
|
94
|
+
private resetSession;
|
|
95
|
+
private ensureLineage;
|
|
96
|
+
private ensureGeneration;
|
|
97
|
+
private observeMetadata;
|
|
121
98
|
private _rememberCredentials;
|
|
122
|
-
private _signInCredentials;
|
|
123
|
-
private _clearPollRouting;
|
|
124
|
-
/**
|
|
125
|
-
* Processes queued commands sequentially.
|
|
126
|
-
* Emits 'finishQueue' when all commands are processed.
|
|
127
|
-
*
|
|
128
|
-
*/
|
|
129
|
-
_processQueue(): Promise<void>;
|
|
130
|
-
private _emitFinishQueue;
|
|
131
|
-
/**
|
|
132
|
-
* Processes a single command by writing it to the connection and waiting for response.
|
|
133
|
-
*
|
|
134
|
-
* @param command - Command code
|
|
135
|
-
* @param payload - Command payload
|
|
136
|
-
* @param handleResp - Whether to parse the response
|
|
137
|
-
* @param deadline - When the whole request gives up, shared with the leader
|
|
138
|
-
* re-checks and the re-issues after a move
|
|
139
|
-
* @returns Promise resolving to the command response
|
|
140
|
-
*/
|
|
141
|
-
_processNext(command: number, payload: Buffer, handleResp?: boolean, deadline?: number): Promise<CommandResponse>;
|
|
142
|
-
private _processVsrLogin;
|
|
143
|
-
private _processVsr;
|
|
144
|
-
private _exchange;
|
|
145
|
-
private isUnloggedCommand;
|
|
146
|
-
/**
|
|
147
|
-
* Moves a freshly authenticated session to the cluster leader.
|
|
148
|
-
*
|
|
149
|
-
* Only the leader accepts replicated commands, and the roster read is
|
|
150
|
-
* auth-gated, so the topology cannot be inspected before a login binds a
|
|
151
|
-
* session. The redirect drops that session along with the socket, so the
|
|
152
|
-
* login is replayed on the leader and its answer supersedes the one from the
|
|
153
|
-
* node the client dialed. Leadership can move between the roster read and
|
|
154
|
-
* the replay, so each freshly bound hop rechecks the roster under a bounded
|
|
155
|
-
* redirect budget.
|
|
156
|
-
*
|
|
157
|
-
* @returns The leader's login response, or undefined when the client stays
|
|
158
|
-
*/
|
|
159
|
-
private _settleOnLeader;
|
|
160
|
-
/**
|
|
161
|
-
* Reads the cluster roster and picks the endpoint to settle on.
|
|
162
|
-
*
|
|
163
|
-
* Best effort: an unreadable roster, `Unauthenticated` included (the session
|
|
164
|
-
* died between the login and this read), keeps the client on its current
|
|
165
|
-
* node instead of failing a login that already succeeded.
|
|
166
|
-
*/
|
|
167
|
-
private _readLeaderEndpoint;
|
|
168
|
-
/**
|
|
169
|
-
* Fails all queued commands with the given error.
|
|
170
|
-
*
|
|
171
|
-
* @param err - Error to reject all queued commands with
|
|
172
|
-
*/
|
|
173
|
-
_failQueue(err: Error): void;
|
|
174
|
-
private _resetSession;
|
|
175
99
|
hold(): () => void;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
*/
|
|
182
|
-
authenticate(creds: ClientCredentials): Promise<boolean>;
|
|
183
|
-
private _authenticate;
|
|
184
|
-
/**
|
|
185
|
-
* Authenticates using username and password.
|
|
186
|
-
*
|
|
187
|
-
* @param creds - Password credentials
|
|
188
|
-
* @returns Login response with user ID
|
|
189
|
-
*/
|
|
190
|
-
_authWithPassword(creds: PasswordCredentials): Promise<{
|
|
100
|
+
authenticate(credentials: ClientCredentials): Promise<boolean>;
|
|
101
|
+
private loginByCaller;
|
|
102
|
+
/** Only SDK recovery shares a sign-in; callers always own a fresh attempt. */
|
|
103
|
+
private signIn;
|
|
104
|
+
_authWithPassword(credentials: PasswordCredentials): Promise<{
|
|
191
105
|
userId: number;
|
|
192
106
|
}>;
|
|
193
|
-
|
|
194
|
-
* Authenticates using a token.
|
|
195
|
-
*
|
|
196
|
-
* @param creds - Token credentials
|
|
197
|
-
* @returns Login response with user ID
|
|
198
|
-
*/
|
|
199
|
-
_authWithToken(creds: TokenCredentials): Promise<{
|
|
107
|
+
_authWithToken(credentials: TokenCredentials): Promise<{
|
|
200
108
|
userId: number;
|
|
201
109
|
}>;
|
|
202
|
-
/**
|
|
203
|
-
* Sends a ping command to the server.
|
|
204
|
-
*
|
|
205
|
-
* @returns Ping response
|
|
206
|
-
*/
|
|
207
110
|
ping(): Promise<boolean>;
|
|
208
|
-
/**
|
|
209
|
-
* Starts sending periodic heartbeat pings to keep the connection alive.
|
|
210
|
-
*
|
|
211
|
-
* @param interval - Heartbeat interval in milliseconds
|
|
212
|
-
*/
|
|
213
111
|
heartbeat(interval?: number): void;
|
|
214
|
-
/**
|
|
215
|
-
* Returns the underlying socket as a readable stream.
|
|
216
|
-
*
|
|
217
|
-
* @returns The connection socket
|
|
218
|
-
*/
|
|
219
112
|
getReadStream(): import("node:net").Socket;
|
|
220
|
-
/**
|
|
221
|
-
* Destroys the stream and cleans up resources.
|
|
222
|
-
* Stops heartbeat and destroys the connection.
|
|
223
|
-
*/
|
|
224
113
|
destroy(): void;
|
|
225
114
|
}
|
|
226
|
-
/**
|
|
227
|
-
* Creates a new RawClient instance.
|
|
228
|
-
*
|
|
229
|
-
* @param options - Client configuration
|
|
230
|
-
* @returns RawClient instance
|
|
231
|
-
*/
|
|
232
115
|
export declare function getRawClient(options: ClientConfigOrString): RawClient;
|
|
233
116
|
//# sourceMappingURL=client.socket.d.ts.map
|