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
|
@@ -18,11 +18,14 @@ import assert from 'node:assert/strict';
|
|
|
18
18
|
import { EventEmitter } from 'node:events';
|
|
19
19
|
import { Readable } from 'node:stream';
|
|
20
20
|
import { describe, it } from 'node:test';
|
|
21
|
+
import { EMPTY_PARTITION_CONTEXT } from '../vsr/header.js';
|
|
21
22
|
import { COMMAND_CODE } from '../command.code.js';
|
|
22
23
|
import { ResponseError } from '../error.utils.js';
|
|
24
|
+
import { LaneBlockedError, OPERATION_TIMEOUT_MS, OperationUncertainError } from '../../client/client.operation.js';
|
|
23
25
|
import { Consumer } from '../offset/offset.utils.js';
|
|
24
26
|
import { PollingStrategy } from './poll.utils.js';
|
|
25
27
|
import { NO_ASSIGNED_PARTITION, pollMessages } from './poll-messages.command.js';
|
|
28
|
+
const UNAUTHENTICATED = 40;
|
|
26
29
|
const response = (data) => ({
|
|
27
30
|
status: 0,
|
|
28
31
|
length: data.length,
|
|
@@ -38,7 +41,7 @@ const assignment = (generation, partitions) => {
|
|
|
38
41
|
return response(data);
|
|
39
42
|
};
|
|
40
43
|
const pollResponse = (partitionId, count = 0) => {
|
|
41
|
-
const data = Buffer.alloc(
|
|
44
|
+
const data = Buffer.alloc(40);
|
|
42
45
|
data.writeUInt32LE(partitionId, 0);
|
|
43
46
|
data.writeBigUInt64LE(0n, 4);
|
|
44
47
|
data.writeUInt32LE(count, 12);
|
|
@@ -59,8 +62,8 @@ const stubClient = (responses, onCommand) => {
|
|
|
59
62
|
const client = {
|
|
60
63
|
protocol: 'vsr',
|
|
61
64
|
isAuthenticated: true,
|
|
62
|
-
sendCommand: async (command, payload) => {
|
|
63
|
-
commands.push({ command, payload });
|
|
65
|
+
sendCommand: async (command, payload, options) => {
|
|
66
|
+
commands.push({ command, payload, options });
|
|
64
67
|
onCommand?.(command, emitter);
|
|
65
68
|
const next = responses.shift();
|
|
66
69
|
if (!next)
|
|
@@ -129,12 +132,28 @@ describe('VSR consumer-group polling', () => {
|
|
|
129
132
|
const { client, commands } = stubClient([assignment(1n, [])]);
|
|
130
133
|
assert.deepEqual(await pollMessages(async () => client)(groupRequest), {
|
|
131
134
|
partitionId: NO_ASSIGNED_PARTITION,
|
|
135
|
+
context: EMPTY_PARTITION_CONTEXT,
|
|
132
136
|
currentOffset: 0n,
|
|
133
137
|
count: 0,
|
|
134
138
|
messages: []
|
|
135
139
|
});
|
|
136
140
|
assert.deepEqual(commands.map(({ command }) => command), [COMMAND_CODE.SyncGroup]);
|
|
137
141
|
});
|
|
142
|
+
it('resynchronizes an empty assignment on the next poll', async () => {
|
|
143
|
+
const { client, commands } = stubClient([
|
|
144
|
+
assignment(1n, []),
|
|
145
|
+
assignment(4n, [7]),
|
|
146
|
+
pollResponse(7)
|
|
147
|
+
]);
|
|
148
|
+
const poll = pollMessages(async () => client);
|
|
149
|
+
assert.equal((await poll(groupRequest)).partitionId, NO_ASSIGNED_PARTITION);
|
|
150
|
+
assert.equal((await poll(groupRequest)).partitionId, 7);
|
|
151
|
+
assert.deepEqual(commands.map(({ command }) => command), [
|
|
152
|
+
COMMAND_CODE.SyncGroup,
|
|
153
|
+
COMMAND_CODE.SyncGroup,
|
|
154
|
+
COMMAND_CODE.PollMessages
|
|
155
|
+
]);
|
|
156
|
+
});
|
|
138
157
|
it('caches a cursor and refreshes it after a heartbeat', async () => {
|
|
139
158
|
const responses = [
|
|
140
159
|
assignment(1n, [4, 5]),
|
|
@@ -184,7 +203,7 @@ describe('VSR consumer-group polling', () => {
|
|
|
184
203
|
], (command, emitter) => {
|
|
185
204
|
if (command === COMMAND_CODE.PollMessages && resetPending) {
|
|
186
205
|
resetPending = false;
|
|
187
|
-
emitter.emit('sessionReset');
|
|
206
|
+
emitter.emit('sessionReset', true);
|
|
188
207
|
}
|
|
189
208
|
});
|
|
190
209
|
assert.equal((await pollMessages(async () => client)(groupRequest)).partitionId, 7);
|
|
@@ -197,13 +216,70 @@ describe('VSR consumer-group polling', () => {
|
|
|
197
216
|
COMMAND_CODE.PollMessages
|
|
198
217
|
]);
|
|
199
218
|
});
|
|
219
|
+
it('returns the refused sign-in of a poll that starts signed out', async () => {
|
|
220
|
+
let resetPending = true;
|
|
221
|
+
const refusal = new ResponseError(COMMAND_CODE.SyncGroup, UNAUTHENTICATED);
|
|
222
|
+
const { client, commands } = stubClient([refusal], (_command, emitter) => {
|
|
223
|
+
// The sign-in that the first command waits for resets a session that was not signed in.
|
|
224
|
+
if (resetPending) {
|
|
225
|
+
resetPending = false;
|
|
226
|
+
emitter.emit('sessionReset', false);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
client.isAuthenticated = false;
|
|
230
|
+
await assert.rejects(pollMessages(async () => client)(groupRequest), (error) => error === refusal);
|
|
231
|
+
assert.deepEqual(commands.map(({ command }) => command), [COMMAND_CODE.SyncGroup]);
|
|
232
|
+
});
|
|
233
|
+
it('retries a poll that starts signed out when the session it signed in to ends', async () => {
|
|
234
|
+
let sessionEnded = false;
|
|
235
|
+
const { client, commands } = stubClient([
|
|
236
|
+
assignment(1n, [4]),
|
|
237
|
+
new Error('connection closed while waiting for response'),
|
|
238
|
+
response(Buffer.alloc(0)),
|
|
239
|
+
response(Buffer.alloc(0)),
|
|
240
|
+
assignment(2n, [7]),
|
|
241
|
+
pollResponse(7)
|
|
242
|
+
], (command, emitter) => {
|
|
243
|
+
if (!client.isAuthenticated) {
|
|
244
|
+
// The sign-in that the first command waits for resets a session that was not signed in.
|
|
245
|
+
emitter.emit('sessionReset', false);
|
|
246
|
+
client.isAuthenticated = true;
|
|
247
|
+
}
|
|
248
|
+
else if (command === COMMAND_CODE.PollMessages && !sessionEnded) {
|
|
249
|
+
sessionEnded = true;
|
|
250
|
+
emitter.emit('sessionReset', true);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
client.isAuthenticated = false;
|
|
254
|
+
assert.equal((await pollMessages(async () => client)(groupRequest)).partitionId, 7);
|
|
255
|
+
assert.deepEqual(commands.map(({ command }) => command), [
|
|
256
|
+
COMMAND_CODE.SyncGroup, COMMAND_CODE.PollMessages, COMMAND_CODE.SyncGroup,
|
|
257
|
+
COMMAND_CODE.JoinGroup, COMMAND_CODE.SyncGroup, COMMAND_CODE.PollMessages
|
|
258
|
+
]);
|
|
259
|
+
});
|
|
260
|
+
it('bounds the retries after session ends by one deadline for the whole call', async (context) => {
|
|
261
|
+
// Each attempt takes a third of the deadline, so the third one ends at it.
|
|
262
|
+
const attempts = 3;
|
|
263
|
+
const sessionEnds = attempts + 1;
|
|
264
|
+
context.mock.timers.enable({ apis: ['Date'] });
|
|
265
|
+
const deadline = Date.now() + OPERATION_TIMEOUT_MS;
|
|
266
|
+
const refusal = () => new ResponseError(COMMAND_CODE.SyncGroup, UNAUTHENTICATED);
|
|
267
|
+
const { client, commands } = stubClient(Array.from({ length: sessionEnds }, refusal), (_command, emitter) => {
|
|
268
|
+
if (commands.length <= sessionEnds)
|
|
269
|
+
emitter.emit('sessionReset', true);
|
|
270
|
+
context.mock.timers.tick(OPERATION_TIMEOUT_MS / attempts);
|
|
271
|
+
});
|
|
272
|
+
await assert.rejects(pollMessages(async () => client)(groupRequest), (error) => error instanceof ResponseError && error.errorCode === UNAUTHENTICATED);
|
|
273
|
+
assert.equal(commands.length, attempts);
|
|
274
|
+
assert.ok(commands.every(({ options }) => options?.deadline === deadline));
|
|
275
|
+
});
|
|
200
276
|
it('does not replay an ambiguous auto-commit poll when its coordinator also resets', async () => {
|
|
201
277
|
const { client, commands } = stubClient([
|
|
202
278
|
assignment(1n, [4]),
|
|
203
279
|
new ResponseError(COMMAND_CODE.PollMessages, 57)
|
|
204
280
|
], (command, emitter) => {
|
|
205
281
|
if (command === COMMAND_CODE.PollMessages)
|
|
206
|
-
emitter.emit('sessionReset');
|
|
282
|
+
emitter.emit('sessionReset', true);
|
|
207
283
|
});
|
|
208
284
|
await assert.rejects(pollMessages(async () => client)({ ...groupRequest, autocommit: true }), (error) => error instanceof ResponseError && error.errorCode === 57);
|
|
209
285
|
assert.deepEqual(commands.map(({ command }) => command), [COMMAND_CODE.SyncGroup, COMMAND_CODE.PollMessages]);
|
|
@@ -218,6 +294,7 @@ describe('VSR consumer-group polling', () => {
|
|
|
218
294
|
]);
|
|
219
295
|
assert.deepEqual(await pollMessages(async () => client)(groupRequest), {
|
|
220
296
|
partitionId: NO_ASSIGNED_PARTITION,
|
|
297
|
+
context: EMPTY_PARTITION_CONTEXT,
|
|
221
298
|
currentOffset: 0n,
|
|
222
299
|
count: 0,
|
|
223
300
|
messages: []
|
|
@@ -226,6 +303,37 @@ describe('VSR consumer-group polling', () => {
|
|
|
226
303
|
.filter(({ command }) => command === COMMAND_CODE.PollMessages)
|
|
227
304
|
.map(({ payload }) => payload.readUInt32LE(20)), [1, 2]);
|
|
228
305
|
});
|
|
306
|
+
it('resolves its uncertain join before synchronizing the group again', async () => {
|
|
307
|
+
const operationId = 17n;
|
|
308
|
+
const { client, commands } = stubClient([
|
|
309
|
+
response(Buffer.alloc(0)),
|
|
310
|
+
new OperationUncertainError(operationId, new Error('lost join reply')),
|
|
311
|
+
assignment(2n, [7]),
|
|
312
|
+
pollResponse(7)
|
|
313
|
+
]);
|
|
314
|
+
const resolutions = [];
|
|
315
|
+
client.resolveOperation = async (id) => {
|
|
316
|
+
resolutions.push(id);
|
|
317
|
+
if (resolutions.length === 1)
|
|
318
|
+
throw new OperationUncertainError(id, new Error('lost resolve reply'));
|
|
319
|
+
};
|
|
320
|
+
const poll = pollMessages(async () => client);
|
|
321
|
+
await assert.rejects(poll(groupRequest), OperationUncertainError);
|
|
322
|
+
await assert.rejects(poll(groupRequest), OperationUncertainError);
|
|
323
|
+
assert.equal((await poll(groupRequest)).partitionId, 7);
|
|
324
|
+
assert.deepEqual(resolutions, [operationId, operationId]);
|
|
325
|
+
assert.deepEqual(commands.map(({ command }) => command), [
|
|
326
|
+
COMMAND_CODE.SyncGroup, COMMAND_CODE.JoinGroup,
|
|
327
|
+
COMMAND_CODE.SyncGroup, COMMAND_CODE.PollMessages
|
|
328
|
+
]);
|
|
329
|
+
});
|
|
330
|
+
it('leaves a join blocked by an unrelated operation for the caller to resolve', async () => {
|
|
331
|
+
const { client } = stubClient([
|
|
332
|
+
response(Buffer.alloc(0)), new LaneBlockedError(18n)
|
|
333
|
+
]);
|
|
334
|
+
client.resolveOperation = async () => { assert.fail('must not resolve an unrelated operation'); };
|
|
335
|
+
await assert.rejects(pollMessages(async () => client)(groupRequest), LaneBlockedError);
|
|
336
|
+
});
|
|
229
337
|
it('rejoins when a cached poll reports a missing member', async () => {
|
|
230
338
|
const { client, commands } = stubClient([
|
|
231
339
|
assignment(1n, [4]),
|
|
@@ -308,4 +416,34 @@ describe('VSR consumer-group polling', () => {
|
|
|
308
416
|
assert.equal(held, false);
|
|
309
417
|
});
|
|
310
418
|
});
|
|
419
|
+
describe('VSR poll context', () => {
|
|
420
|
+
const context = { incarnation: 7n, ownerGeneration: 8n, metadataOp: 9n };
|
|
421
|
+
it('passes the caller context with an explicit partition poll', async () => {
|
|
422
|
+
const { client, commands } = stubClient([pollResponse(4)]);
|
|
423
|
+
await pollMessages(async () => client)({ ...groupRequest, partitionId: 4, context });
|
|
424
|
+
assert.deepEqual(commands.map(({ options }) => options?.context), [context]);
|
|
425
|
+
});
|
|
426
|
+
it('passes the caller context with the partition a group poll selects', async () => {
|
|
427
|
+
const { client, commands } = stubClient([assignment(1n, [4]), pollResponse(4)]);
|
|
428
|
+
await pollMessages(async () => client)({ ...groupRequest, context });
|
|
429
|
+
assert.deepEqual(commands.map(({ options }) => options?.context), [undefined, context]);
|
|
430
|
+
});
|
|
431
|
+
it('surfaces stale ownership under a caller context and resynchronizes next time', async () => {
|
|
432
|
+
const { client, commands } = stubClient([
|
|
433
|
+
assignment(1n, [4]),
|
|
434
|
+
new ResponseError(COMMAND_CODE.PollMessages, 5009),
|
|
435
|
+
assignment(2n, [7]),
|
|
436
|
+
pollResponse(7)
|
|
437
|
+
]);
|
|
438
|
+
const poll = pollMessages(async () => client);
|
|
439
|
+
await assert.rejects(poll({ ...groupRequest, context }), (error) => error instanceof ResponseError && error.errorCode === 5009);
|
|
440
|
+
assert.equal((await poll(groupRequest)).partitionId, 7);
|
|
441
|
+
assert.deepEqual(commands.map(({ command }) => command), [
|
|
442
|
+
COMMAND_CODE.SyncGroup,
|
|
443
|
+
COMMAND_CODE.PollMessages,
|
|
444
|
+
COMMAND_CODE.SyncGroup,
|
|
445
|
+
COMMAND_CODE.PollMessages
|
|
446
|
+
]);
|
|
447
|
+
});
|
|
448
|
+
});
|
|
311
449
|
//# sourceMappingURL=poll-messages.command.test.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PartitionContext } from "../vsr/header.js";
|
|
1
2
|
import { type Id } from "../identifier.utils.js";
|
|
2
3
|
import { type ValueOf } from "../../type.utils.js";
|
|
3
4
|
import { type Consumer } from "../offset/offset.utils.js";
|
|
@@ -118,6 +119,8 @@ export type Message = {
|
|
|
118
119
|
* Response from a poll messages command.
|
|
119
120
|
*/
|
|
120
121
|
export type PollMessagesResponse = {
|
|
122
|
+
/** Partition incarnation, owner generation and metadata op the reply carries */
|
|
123
|
+
context: PartitionContext;
|
|
121
124
|
/** Partition the messages came from */
|
|
122
125
|
partitionId: number;
|
|
123
126
|
/** Current offset in the partition */
|
|
@@ -176,6 +179,7 @@ export declare const deserializeMessages: (b: Buffer, pos?: number) => Message[]
|
|
|
176
179
|
*/
|
|
177
180
|
export declare const deserializePollMessages: (r: Buffer, pos?: number) => {
|
|
178
181
|
partitionId: number;
|
|
182
|
+
context: PartitionContext;
|
|
179
183
|
currentOffset: bigint;
|
|
180
184
|
count: number;
|
|
181
185
|
messages: Message[];
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// KIND, either express or implied. See the License for the
|
|
15
15
|
// specific language governing permissions and limitations
|
|
16
16
|
// under the License.
|
|
17
|
+
import { deserializePartitionContext, PARTITION_CONTEXT_SIZE } from "../vsr/header.js";
|
|
17
18
|
import { reverseRecord } from "../../type.utils.js";
|
|
18
19
|
import { toDate } from "../serialize.utils.js";
|
|
19
20
|
import { serializeGetOffset } from "../offset/offset.utils.js";
|
|
@@ -211,9 +212,13 @@ export const deserializePollMessages = (r, pos = 0) => {
|
|
|
211
212
|
const partitionId = r.readUInt32LE(pos);
|
|
212
213
|
const currentOffset = r.readBigUInt64LE(pos + 4);
|
|
213
214
|
const count = r.readUInt32LE(pos + 12);
|
|
214
|
-
const
|
|
215
|
+
const context = deserializePartitionContext(r, pos + 16);
|
|
216
|
+
const messages = deserializeMessages(r, pos + 16 + PARTITION_CONTEXT_SIZE);
|
|
217
|
+
if (messages.length !== count)
|
|
218
|
+
throw new Error("poll response count does not match its messages");
|
|
215
219
|
return {
|
|
216
220
|
partitionId,
|
|
221
|
+
context,
|
|
217
222
|
currentOffset,
|
|
218
223
|
count,
|
|
219
224
|
messages,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Id } from '../identifier.utils.js';
|
|
2
2
|
import { type CreateMessage } from './message.utils.js';
|
|
3
|
-
import
|
|
4
|
-
import type { CommandResponse } from '../../client/client.type.js';
|
|
3
|
+
import { Partitioning } from './partitioning.utils.js';
|
|
4
|
+
import type { ClientProvider, CommandResponse } from '../../client/client.type.js';
|
|
5
5
|
/**
|
|
6
6
|
* Parameters for the send messages command.
|
|
7
7
|
*/
|
|
@@ -54,5 +54,5 @@ export declare const SEND_MESSAGES: {
|
|
|
54
54
|
* Executable send messages command function. Resolves to the commit
|
|
55
55
|
* confirmations of the written partitions, empty against the legacy server.
|
|
56
56
|
*/
|
|
57
|
-
export declare const sendMessages: (getClient:
|
|
57
|
+
export declare const sendMessages: (getClient: ClientProvider) => (request: SendMessages) => Promise<SendMessagesResponse>;
|
|
58
58
|
//# sourceMappingURL=send-messages.command.d.ts.map
|
|
@@ -14,10 +14,14 @@
|
|
|
14
14
|
// KIND, either express or implied. See the License for the
|
|
15
15
|
// specific language governing permissions and limitations
|
|
16
16
|
// under the License.
|
|
17
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
18
|
+
import { xxh32 } from '@node-rs/xxhash';
|
|
19
|
+
import { serializeIdentifier } from '../identifier.utils.js';
|
|
17
20
|
import { serializeSendMessages } from './message.utils.js';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
21
|
+
import { Partitioning, PartitionKind, serializeMessageKey } from './partitioning.utils.js';
|
|
22
|
+
import { DeserializeError, ResponseError, responseError } from '../error.utils.js';
|
|
20
23
|
import { COMMAND_CODE } from '../command.code.js';
|
|
24
|
+
import { deserializePartitionRoute, serializePartitionRoute } from '../partition/partition-route.js';
|
|
21
25
|
/** Size of the confirmation count prefixing the list. */
|
|
22
26
|
const CONFIRMATIONS_COUNT_SIZE = 4;
|
|
23
27
|
/**
|
|
@@ -66,9 +70,105 @@ export const SEND_MESSAGES = {
|
|
|
66
70
|
},
|
|
67
71
|
deserialize: (r) => deserializeSendMessages(r.data)
|
|
68
72
|
};
|
|
73
|
+
/** Rust `calculate_32` hashes message keys with XXH32 under this seed. */
|
|
74
|
+
const MESSAGE_KEY_SEED = 0;
|
|
75
|
+
const TOPIC_ID_NOT_FOUND = 2010;
|
|
76
|
+
const PARTITION_NOT_FOUND = 3007;
|
|
77
|
+
const SEND_TIMEOUT_MS = 30_000;
|
|
78
|
+
const ROUTE_RETRY_INTERVAL_MS = 50;
|
|
79
|
+
const TRANSIENT_NOT_ACCEPTED = 58;
|
|
80
|
+
const NOT_PRIMARY = 89;
|
|
81
|
+
const topicPartitions = new WeakMap();
|
|
82
|
+
const getTopicPartitions = (client, streamId, topicId) => {
|
|
83
|
+
let topics = topicPartitions.get(client);
|
|
84
|
+
if (!topics) {
|
|
85
|
+
const created = new Map();
|
|
86
|
+
topicPartitions.set(client, created);
|
|
87
|
+
// The client's own stream, topic or partition change can alter any
|
|
88
|
+
// count, as it drops the Rust SDK's topic discovery.
|
|
89
|
+
client.on('topicDiscoveryReset', () => {
|
|
90
|
+
for (const topic of created.values())
|
|
91
|
+
topic.count = undefined;
|
|
92
|
+
});
|
|
93
|
+
client.on('partitionRoute', (event) => {
|
|
94
|
+
const { topic, count } = event;
|
|
95
|
+
const cached = created.get(topic);
|
|
96
|
+
if (cached)
|
|
97
|
+
cached.count = count;
|
|
98
|
+
});
|
|
99
|
+
topics = created;
|
|
100
|
+
}
|
|
101
|
+
const key = Buffer.concat([serializeIdentifier(streamId), serializeIdentifier(topicId)]).toString('hex');
|
|
102
|
+
let topic = topics.get(key);
|
|
103
|
+
if (!topic) {
|
|
104
|
+
topic = { cursor: 0 };
|
|
105
|
+
topics.set(key, topic);
|
|
106
|
+
}
|
|
107
|
+
return topic;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Resolves Balanced and MessageKey to an explicit partition id as the Rust SDK
|
|
111
|
+
* does, so the send carries that partition's incarnation.
|
|
112
|
+
*/
|
|
113
|
+
const resolvePartitioning = async (client, { streamId, topicId, partition }, call) => {
|
|
114
|
+
if (partition?.kind === PartitionKind.PartitionId)
|
|
115
|
+
return partition;
|
|
116
|
+
const topic = getTopicPartitions(client, streamId, topicId);
|
|
117
|
+
if (!topic.count) {
|
|
118
|
+
for (;;) {
|
|
119
|
+
try {
|
|
120
|
+
const response = await client.sendCommand(COMMAND_CODE.GetPartitionRoute, serializePartitionRoute(streamId, topicId, 0), call);
|
|
121
|
+
topic.count = deserializePartitionRoute(response.data).partitionsCount;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (!(error instanceof ResponseError) ||
|
|
126
|
+
(error.errorCode !== TRANSIENT_NOT_ACCEPTED && error.errorCode !== NOT_PRIMARY) ||
|
|
127
|
+
call.deadline - Date.now() <= ROUTE_RETRY_INTERVAL_MS)
|
|
128
|
+
throw error;
|
|
129
|
+
await delay(ROUTE_RETRY_INTERVAL_MS);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!topic.count)
|
|
134
|
+
throw responseError(COMMAND_CODE.SendMessages, TOPIC_ID_NOT_FOUND);
|
|
135
|
+
if (partition?.kind === PartitionKind.MessageKey)
|
|
136
|
+
return Partitioning.PartitionId(xxh32(serializeMessageKey(partition.value), MESSAGE_KEY_SEED) % topic.count);
|
|
137
|
+
const index = topic.cursor % topic.count;
|
|
138
|
+
topic.cursor += 1;
|
|
139
|
+
return Partitioning.PartitionId(index);
|
|
140
|
+
};
|
|
69
141
|
/**
|
|
70
142
|
* Executable send messages command function. Resolves to the commit
|
|
71
143
|
* confirmations of the written partitions, empty against the legacy server.
|
|
72
144
|
*/
|
|
73
|
-
export const sendMessages =
|
|
145
|
+
export const sendMessages = (getClient) => async (request) => {
|
|
146
|
+
const client = await getClient();
|
|
147
|
+
const release = client.hold?.();
|
|
148
|
+
try {
|
|
149
|
+
// No command of the call runs under a session that the caller opened after it started.
|
|
150
|
+
const call = { deadline: Date.now() + SEND_TIMEOUT_MS, lineage: client.lineage };
|
|
151
|
+
for (let attempt = 0;; attempt += 1) {
|
|
152
|
+
const partition = await resolvePartitioning(client, request, call);
|
|
153
|
+
try {
|
|
154
|
+
return SEND_MESSAGES.deserialize(await client.sendCommand(SEND_MESSAGES.code, SEND_MESSAGES.serialize({ ...request, partition }), call));
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (attempt !== 0 || request.partition?.kind === PartitionKind.PartitionId ||
|
|
158
|
+
!(error instanceof ResponseError) || error.errorCode !== PARTITION_NOT_FOUND)
|
|
159
|
+
throw error;
|
|
160
|
+
getTopicPartitions(client, request.streamId, request.topicId).count = undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
// A refusal can mean the topic changed shape since its count was read.
|
|
166
|
+
if (error instanceof ResponseError)
|
|
167
|
+
getTopicPartitions(client, request.streamId, request.topicId).count = undefined;
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
release?.();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
74
174
|
//# sourceMappingURL=send-messages.command.js.map
|
|
@@ -16,10 +16,14 @@
|
|
|
16
16
|
// under the License.
|
|
17
17
|
import { describe, it } from "node:test";
|
|
18
18
|
import assert from "node:assert/strict";
|
|
19
|
+
import { EventEmitter } from "node:events";
|
|
19
20
|
import { uuidv7, uuidv4 } from "uuidv7";
|
|
20
|
-
import { SEND_MESSAGES, } from "./send-messages.command.js";
|
|
21
|
+
import { SEND_MESSAGES, sendMessages, } from "./send-messages.command.js";
|
|
21
22
|
import { HeaderValue, HeaderKeyFactory } from "./header.utils.js";
|
|
22
|
-
import {
|
|
23
|
+
import { Partitioning, PartitionKind } from "./partitioning.utils.js";
|
|
24
|
+
import { serializePartitionRoute } from '../partition/partition-route.js';
|
|
25
|
+
import { COMMAND_CODE } from "../command.code.js";
|
|
26
|
+
import { DeserializeError, responseError } from "../error.utils.js";
|
|
23
27
|
const SUCCESS = 0;
|
|
24
28
|
const CONFIRMATION_SIZE = 20;
|
|
25
29
|
const confirmation = (partitionId) => ({
|
|
@@ -226,4 +230,111 @@ describe("SendMessages", () => {
|
|
|
226
230
|
});
|
|
227
231
|
});
|
|
228
232
|
});
|
|
233
|
+
const routeBody = (partitionsCount) => {
|
|
234
|
+
const body = Buffer.alloc(25);
|
|
235
|
+
body.writeUInt32LE(partitionsCount, 20);
|
|
236
|
+
return body;
|
|
237
|
+
};
|
|
238
|
+
const sendClient = (partitionsCount, refuseFirstSend = false) => {
|
|
239
|
+
const sent = [];
|
|
240
|
+
const events = new EventEmitter();
|
|
241
|
+
const client = {
|
|
242
|
+
on: events.on.bind(events),
|
|
243
|
+
emit: events.emit.bind(events),
|
|
244
|
+
sendCommand: async (code, payload) => {
|
|
245
|
+
sent.push({ code, payload });
|
|
246
|
+
if (code === COMMAND_CODE.SendMessages && refuseFirstSend) {
|
|
247
|
+
refuseFirstSend = false;
|
|
248
|
+
throw responseError(code, 87);
|
|
249
|
+
}
|
|
250
|
+
return response(code === COMMAND_CODE.GetPartitionRoute ? routeBody(partitionsCount) : Buffer.alloc(0));
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
return { client, sent };
|
|
254
|
+
};
|
|
255
|
+
const sentPartitions = (sent) => sent.filter(({ code }) => code === COMMAND_CODE.SendMessages).map(({ payload }) => {
|
|
256
|
+
assert.equal(payload[16], PartitionKind.PartitionId);
|
|
257
|
+
return payload.readUInt32LE(18);
|
|
258
|
+
});
|
|
259
|
+
const countOf = (sent, code) => sent.filter((entry) => entry.code === code).length;
|
|
260
|
+
const request = { streamId: 1, topicId: 2, messages: [{ payload: "m" }] };
|
|
261
|
+
describe("SendMessages partition resolution", () => {
|
|
262
|
+
it("resolves Balanced sends round-robin from one route read", async () => {
|
|
263
|
+
const { client, sent } = sendClient(3);
|
|
264
|
+
const send = sendMessages(async () => client);
|
|
265
|
+
for (let index = 0; index < 4; index += 1)
|
|
266
|
+
await send(request);
|
|
267
|
+
assert.deepEqual(sentPartitions(sent), [0, 1, 2, 0]);
|
|
268
|
+
assert.equal(countOf(sent, COMMAND_CODE.GetPartitionRoute), 1);
|
|
269
|
+
});
|
|
270
|
+
it("waits for a route to become ready within the original send deadline", async () => {
|
|
271
|
+
const { client } = sendClient(2);
|
|
272
|
+
const sendCommand = client.sendCommand;
|
|
273
|
+
let reads = 0;
|
|
274
|
+
const deadlines = [];
|
|
275
|
+
client.sendCommand = async (code, payload, options) => {
|
|
276
|
+
deadlines.push(options.deadline);
|
|
277
|
+
if (code === COMMAND_CODE.GetPartitionRoute && ++reads === 1)
|
|
278
|
+
throw responseError(code, 58);
|
|
279
|
+
return sendCommand(code, payload, options);
|
|
280
|
+
};
|
|
281
|
+
await sendMessages(async () => client)(request);
|
|
282
|
+
assert.equal(reads, 2);
|
|
283
|
+
assert.equal(new Set(deadlines).size, 1);
|
|
284
|
+
});
|
|
285
|
+
it("uses refreshed route counts for later balanced sends", async () => {
|
|
286
|
+
const { client, sent } = sendClient(2);
|
|
287
|
+
const send = sendMessages(async () => client);
|
|
288
|
+
await send(request);
|
|
289
|
+
client.emit("partitionRoute", {
|
|
290
|
+
topic: serializePartitionRoute(1, 2, 0).subarray(0, -4).toString('hex'), count: 3
|
|
291
|
+
});
|
|
292
|
+
await send(request);
|
|
293
|
+
await send(request);
|
|
294
|
+
assert.deepEqual(sentPartitions(sent), [0, 1, 2]);
|
|
295
|
+
assert.equal(countOf(sent, COMMAND_CODE.GetPartitionRoute), 1);
|
|
296
|
+
});
|
|
297
|
+
it("hashes MessageKey sends with XXH32 seed 0 like the Rust SDK", async () => {
|
|
298
|
+
const { client, sent } = sendClient(4);
|
|
299
|
+
const send = sendMessages(async () => client);
|
|
300
|
+
await send({ ...request, partition: Partitioning.MessageKey("abc") });
|
|
301
|
+
for (let key = 0; key < 64; key += 1)
|
|
302
|
+
await send({ ...request, partition: Partitioning.MessageKey(`key-${key}`) });
|
|
303
|
+
const [first, ...rest] = sentPartitions(sent);
|
|
304
|
+
assert.equal(first, 0x32D153FF % 4); // XXH32("abc", 0)
|
|
305
|
+
assert.ok(rest.every((partition) => partition >= 0 && partition < 4));
|
|
306
|
+
});
|
|
307
|
+
it("reads the partition count again after a refused send", async () => {
|
|
308
|
+
const { client, sent } = sendClient(2, true);
|
|
309
|
+
const send = sendMessages(async () => client);
|
|
310
|
+
await assert.rejects(send(request));
|
|
311
|
+
await send(request);
|
|
312
|
+
assert.equal(countOf(sent, COMMAND_CODE.GetPartitionRoute), 2);
|
|
313
|
+
});
|
|
314
|
+
it("reads the partition count again after the client changes topics", async () => {
|
|
315
|
+
const { client, sent } = sendClient(2);
|
|
316
|
+
const send = sendMessages(async () => client);
|
|
317
|
+
await send(request);
|
|
318
|
+
client.emit("topicDiscoveryReset");
|
|
319
|
+
await send(request);
|
|
320
|
+
assert.equal(countOf(sent, COMMAND_CODE.GetPartitionRoute), 2);
|
|
321
|
+
assert.deepEqual(sentPartitions(sent), [0, 1], "the round-robin position survives");
|
|
322
|
+
});
|
|
323
|
+
it("holds the raw client across the route read and the send", async () => {
|
|
324
|
+
const { client, sent } = sendClient(2);
|
|
325
|
+
let held = false;
|
|
326
|
+
client.hold = () => {
|
|
327
|
+
held = true;
|
|
328
|
+
return () => { held = false; };
|
|
329
|
+
};
|
|
330
|
+
const sendCommand = client.sendCommand;
|
|
331
|
+
client.sendCommand = (code, payload) => {
|
|
332
|
+
assert.equal(held, true);
|
|
333
|
+
return sendCommand(code, payload);
|
|
334
|
+
};
|
|
335
|
+
await sendMessages(async () => client)(request);
|
|
336
|
+
assert.equal(countOf(sent, COMMAND_CODE.GetPartitionRoute), 1);
|
|
337
|
+
assert.equal(held, false);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
229
340
|
//# sourceMappingURL=send-messages.command.test.js.map
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import type { PartitionContext } from '../vsr/header.js';
|
|
1
2
|
import type { GetOffset } from './get-offset.command.js';
|
|
2
3
|
/**
|
|
3
|
-
* Parameters for the delete offset command
|
|
4
|
-
*
|
|
4
|
+
* Parameters for the delete offset command: the GetOffset parameters and an
|
|
5
|
+
* optional partition context. Without a context the client takes the one its
|
|
6
|
+
* cached route reports: after another client deletes and recreates the
|
|
7
|
+
* partition, one delete can fail with 87 (5009 for a group consumer). The
|
|
8
|
+
* failed route is dropped and the next call routes again.
|
|
5
9
|
*/
|
|
6
|
-
export type DeleteOffset = GetOffset
|
|
10
|
+
export type DeleteOffset = GetOffset & {
|
|
11
|
+
/** Context of the poll the delete belongs to (`PollMessagesResponse.context`) */
|
|
12
|
+
context?: PartitionContext;
|
|
13
|
+
};
|
|
7
14
|
/**
|
|
8
15
|
* Delete offset command definition.
|
|
9
16
|
* Removes a stored consumer offset.
|
|
@@ -16,5 +23,5 @@ export declare const DELETE_OFFSET: {
|
|
|
16
23
|
/**
|
|
17
24
|
* Executable delete offset command function.
|
|
18
25
|
*/
|
|
19
|
-
export declare const deleteOffset: (getClient: import("../../index.js").ClientProvider) => (arg:
|
|
26
|
+
export declare const deleteOffset: (getClient: import("../../index.js").ClientProvider) => (arg: DeleteOffset) => Promise<boolean>;
|
|
20
27
|
//# sourceMappingURL=delete-offset.command.d.ts.map
|
|
@@ -32,5 +32,5 @@ export const DELETE_OFFSET = {
|
|
|
32
32
|
/**
|
|
33
33
|
* Executable delete offset command function.
|
|
34
34
|
*/
|
|
35
|
-
export const deleteOffset = wrapCommand(DELETE_OFFSET);
|
|
35
|
+
export const deleteOffset = wrapCommand(DELETE_OFFSET, ({ context }) => ({ context }));
|
|
36
36
|
//# sourceMappingURL=delete-offset.command.js.map
|
|
@@ -11,7 +11,7 @@ export type GetOffset = {
|
|
|
11
11
|
topicId: Id;
|
|
12
12
|
/** Consumer identifier (single or group) */
|
|
13
13
|
consumer: Consumer;
|
|
14
|
-
/** Partition ID.
|
|
14
|
+
/** Partition ID. A missing partition reads partition zero. */
|
|
15
15
|
partitionId: number | null;
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
@@ -0,0 +1,43 @@
|
|
|
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 '../command.code.js';
|
|
20
|
+
import { Consumer } from './offset.utils.js';
|
|
21
|
+
import { storeOffset } from './store-offset.command.js';
|
|
22
|
+
import { deleteOffset } from './delete-offset.command.js';
|
|
23
|
+
const context = { incarnation: 7n, ownerGeneration: 8n, metadataOp: 9n };
|
|
24
|
+
const target = { streamId: 1, topicId: 2, consumer: Consumer.Single, partitionId: 3 };
|
|
25
|
+
const recordingClient = (sent) => ({
|
|
26
|
+
sendCommand: async (command, _payload, options) => {
|
|
27
|
+
sent.push({ command, options });
|
|
28
|
+
return { status: 0, length: 0, data: Buffer.alloc(0) };
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
describe('offset writes', () => {
|
|
32
|
+
it('pass the caller context to the store and the delete', async () => {
|
|
33
|
+
const sent = [];
|
|
34
|
+
const client = recordingClient(sent);
|
|
35
|
+
await storeOffset(async () => client)({ ...target, offset: 5n, context });
|
|
36
|
+
await deleteOffset(async () => client)({ ...target, context });
|
|
37
|
+
assert.deepEqual(sent, [
|
|
38
|
+
{ command: COMMAND_CODE.StoreOffset, options: { context } },
|
|
39
|
+
{ command: COMMAND_CODE.DeleteConsumerOffset, options: { context } }
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=offset-write.command.test.js.map
|
|
@@ -57,8 +57,6 @@ export const Consumer = {
|
|
|
57
57
|
* @throws Error if partitionId is null for single consumer kind
|
|
58
58
|
*/
|
|
59
59
|
export const serializeGetOffset = (streamId, topicId, consumer, partitionId) => {
|
|
60
|
-
if (consumer.kind === ConsumerKind.Single && partitionId === null)
|
|
61
|
-
throw new Error('getOffset error: partitionId must be provided for single consumer kind');
|
|
62
60
|
const streamIdentifier = serializeIdentifier(streamId);
|
|
63
61
|
const topicIdentifier = serializeIdentifier(topicId);
|
|
64
62
|
const consumerIdentifier = serializeIdentifier(consumer.id);
|
|
@@ -92,6 +90,8 @@ export const serializeGetOffset = (streamId, topicId, consumer, partitionId) =>
|
|
|
92
90
|
* @returns Buffer containing serialized store offset request
|
|
93
91
|
*/
|
|
94
92
|
export const serializeStoreOffset = (streamId, topicId, consumer, partitionId, offset) => {
|
|
93
|
+
if (partitionId === null)
|
|
94
|
+
throw new Error('storeOffset error: partitionId must be provided');
|
|
95
95
|
const b = Buffer.allocUnsafe(9);
|
|
96
96
|
b.writeBigUInt64LE(offset, 0);
|
|
97
97
|
b.writeUInt8(ACK_QUORUM, 8);
|
|
@@ -109,8 +109,12 @@ export const serializeStoreOffset = (streamId, topicId, consumer, partitionId, o
|
|
|
109
109
|
* @param partitionId - Partition ID. VSR requires an explicit partition.
|
|
110
110
|
* @returns Buffer containing serialized delete offset request
|
|
111
111
|
*/
|
|
112
|
-
export const serializeDeleteOffset = (streamId, topicId, consumer, partitionId) =>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
export const serializeDeleteOffset = (streamId, topicId, consumer, partitionId) => {
|
|
113
|
+
if (partitionId === null)
|
|
114
|
+
throw new Error('deleteOffset error: partitionId must be provided');
|
|
115
|
+
return Buffer.concat([
|
|
116
|
+
serializeGetOffset(streamId, topicId, consumer, partitionId),
|
|
117
|
+
uint8ToBuf(ACK_QUORUM)
|
|
118
|
+
]);
|
|
119
|
+
};
|
|
116
120
|
//# sourceMappingURL=offset.utils.js.map
|