blob: 027ead5b2f70a1fb0e2c4eb9e5235fea0c4a35bc [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { u128ToBuf } from "../number.utils.js";
import {
BATCH_HEADER_SIZE,
FRAME_HEADER_SIZE,
serializeBatchHeader,
deserializeBatchHeader,
} from "./iggy-header.utils.js";
import {
encodeMessagesBatch,
serializeSendMessages,
} from "./message.utils.js";
import {
HeaderValue,
HeaderKeyFactory,
serializeHeaders,
} from "./header.utils.js";
import {
deserializeBatchMessages,
deserializePollMessages,
} from "./poll.utils.js";
// Golden vectors generated by the Rust implementation for stream id 1,
// topic id 2, balanced partitioning, and two messages:
// {id=7, originTimestamp=1000, payload="first-payload"} and
// {id=8, originTimestamp=1050, payload="second-payload",
// userHeaders="user-header-bytes"}.
const PRODUCE_BATCH_ONLY =
"000000000000000000000000000000000000000000000000e803000000000000" +
"8c01000000000000a91f38c86307267c02000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"bfd2b9205a759675070000000000000000000000000000000000000000000000" +
"000000000d000000000000000000000066697273742d7061796c6f6164d66b7e" +
"1c758eb7c0080000000000000000000000000000000100000032000000110000" +
"000e00000000000000000000007365636f6e642d7061796c6f6164757365722d" +
"6865616465722d6279746573";
const POLL_BODY =
"0300000065000000000000000200000003000000000000006400000000000000" +
"8813000000000000e8030000000000008c01000000000000c96826b38a8feed2" +
"0200000000000000000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"00000000000000000000000000000000bfd2b9205a7596750700000000000000" +
"00000000000000000000000000000000000000000d0000000000000000000000" +
"66697273742d7061796c6f6164d66b7e1c758eb7c00800000000000000000000" +
"00000000000100000032000000110000000e0000000000000000000000736563" +
"6f6e642d7061796c6f6164757365722d6865616465722d6279746573";
const goldenMessages = () => [
{
id: u128ToBuf(7n),
payload: Buffer.from("first-payload"),
userHeaders: Buffer.alloc(0),
originTimestamp: 1000n,
},
{
id: u128ToBuf(8n),
payload: Buffer.from("second-payload"),
userHeaders: Buffer.from("user-header-bytes"),
originTimestamp: 1050n,
},
];
describe("message batch golden vectors", () => {
it("encodes the golden produce batch byte-for-byte", () => {
const batch = encodeMessagesBatch(goldenMessages());
assert.equal(batch.toString("hex"), PRODUCE_BATCH_ONLY);
});
it("prefixes the batch with the golden send metadata", () => {
const body = serializeSendMessages(1, 2, [
{ id: 7n, payload: "first-payload" },
{ id: 8n, payload: "second-payload" },
]);
const metadata = Buffer.from(
"120000000104010000000104020000000100" + "02000000",
"hex",
);
assert.deepEqual(body.subarray(0, metadata.length), metadata);
assert.equal(
body.length,
metadata.length + BATCH_HEADER_SIZE + FRAME_HEADER_SIZE * 2 +
"first-payload".length + "second-payload".length,
);
});
it("decodes the golden poll body", () => {
const body = Buffer.from(POLL_BODY, "hex");
assert.equal(body.readUInt32LE(0), 3);
assert.equal(body.readBigUInt64LE(4), 101n);
assert.equal(body.readUInt32LE(12), 2);
const messages = deserializeBatchMessages(body, 16);
assert.equal(messages.length, 2);
assert.equal(messages[0].id, 7n);
assert.equal(messages[0].offset, 100n);
assert.equal(messages[0].timestamp, 5000n);
assert.equal(messages[0].originTimestamp, 1000n);
assert.equal(messages[0].checksum, 0x7596755a20b9d2bfn);
assert.equal(messages[0].payload.toString(), "first-payload");
assert.equal(messages[0].userHeaders.length, 0);
assert.equal(messages[1].id, 8n);
assert.equal(messages[1].offset, 101n);
assert.equal(messages[1].timestamp, 5000n);
assert.equal(messages[1].originTimestamp, 1050n);
assert.equal(messages[1].checksum, 0xc0b78e751c7e6bd6n);
assert.equal(messages[1].payload.toString(), "second-payload");
assert.equal(messages[1].userHeaders.toString(), "user-header-bytes");
});
it("rejects a non-zero reserved frame field", () => {
const body = Buffer.from(POLL_BODY, "hex");
body.writeUInt8(1, 16 + BATCH_HEADER_SIZE + 40);
assert.throws(
() => deserializeBatchMessages(body, 16),
/reserved/,
);
});
it("rejects an empty batch", () => {
assert.throws(() => encodeMessagesBatch([]));
assert.throws(() => serializeSendMessages(1, 2, []));
});
it("rejects a timestamp delta beyond u32", () => {
const messages = goldenMessages();
messages[1].originTimestamp =
messages[0].originTimestamp + 0x1_0000_0000n;
assert.throws(() => encodeMessagesBatch(messages), /u32/);
});
it("round-trips messages through the public poll decoder", () => {
const userHeaders = [
{
key: HeaderKeyFactory.String("trace"),
value: HeaderValue.String("golden"),
},
];
const batch = encodeMessagesBatch([
{
id: u128ToBuf(7n),
payload: Buffer.from("first-payload"),
userHeaders: Buffer.alloc(0),
originTimestamp: 3_000_000n,
},
{
id: u128ToBuf(8n),
payload: Buffer.from("second-payload"),
userHeaders: serializeHeaders(userHeaders),
originTimestamp: 3_000_500n,
},
]);
const stamped = deserializeBatchHeader(batch);
stamped.partitionId = 3n;
stamped.baseOffset = 100n;
stamped.baseTimestamp = 5_000_000n;
serializeBatchHeader(stamped).copy(batch);
const envelope = Buffer.alloc(16);
envelope.writeUInt32LE(3, 0);
envelope.writeBigUInt64LE(101n, 4);
envelope.writeUInt32LE(2, 12);
const response = deserializePollMessages(
Buffer.concat([envelope, batch]),
);
assert.equal(response.partitionId, 3);
assert.equal(response.currentOffset, 101n);
assert.equal(response.count, 2);
assert.equal(response.messages.length, 2);
const [first, second] = response.messages;
assert.equal(first.headers.id, 7n);
assert.equal(first.headers.offset, 100n);
assert.deepEqual(first.headers.timestamp, new Date(5_000));
assert.deepEqual(first.headers.originTimestamp, new Date(3_000));
assert.equal(first.payload.toString(), "first-payload");
assert.deepEqual(first.userHeaders, []);
assert.equal(second.headers.id, 8n);
assert.equal(second.headers.offset, 101n);
assert.deepEqual(second.headers.originTimestamp, new Date(3_000));
assert.equal(second.payload.toString(), "second-payload");
assert.equal(second.userHeaders.length, 1);
assert.equal(second.userHeaders[0].key.value, "trace");
assert.equal(second.userHeaders[0].value.value, "golden");
});
});