refactor(sdk)!: remove v2 bridges and legacy message wire format (#3904)
Removes the old bridge APIs such as `StoreConsumerOffset2` and
`DeleteConsumerOffset2`, additionally promotes to the client side
encoding the new message wire format, rather than relying on server-side
conversion.
---------
Co-authored-by: Piotr Gankiewicz <piotr.gankiewicz@gmail.com>
diff --git a/Cargo.lock b/Cargo.lock
index e5964e9..a08a49a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1950,7 +1950,7 @@
[[package]]
name = "bench-dashboard-frontend"
-version = "0.8.0-edge.2"
+version = "0.8.0-edge.3"
dependencies = [
"bench-dashboard-shared",
"bench-report",
@@ -1980,7 +1980,7 @@
[[package]]
name = "bench-report"
-version = "0.4.0-edge.2"
+version = "0.4.0-edge.3"
dependencies = [
"charming",
"colored",
@@ -4848,35 +4848,6 @@
]
[[package]]
-name = "ext-trait"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d772df1c1a777963712fb68e014235e80863d6a91a85c4e06ba2d16243a310e5"
-dependencies = [
- "ext-trait-proc_macros",
-]
-
-[[package]]
-name = "ext-trait-proc_macros"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ab7934152eaf26aa5aa9f7371408ad5af4c31357073c9e84c3b9d7f11ad639a"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "extension-traits"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a296e5a895621edf9fa8329c83aa1cb69a964643e36cf54d8d7a69b789089537"
-dependencies = [
- "ext-trait",
-]
-
-[[package]]
name = "fastbloom"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6622,7 +6593,7 @@
[[package]]
name = "iggy"
-version = "0.11.0-edge.3"
+version = "0.11.0-edge.4"
dependencies = [
"async-broadcast",
"async-dropper",
@@ -6656,7 +6627,7 @@
[[package]]
name = "iggy-bench"
-version = "0.6.0-edge.3"
+version = "0.6.0-edge.4"
dependencies = [
"async-trait",
"bench-report",
@@ -6685,7 +6656,7 @@
[[package]]
name = "iggy-bench-dashboard-server"
-version = "0.8.0-edge.2"
+version = "0.8.0-edge.3"
dependencies = [
"actix-cors",
"actix-files",
@@ -6713,7 +6684,7 @@
[[package]]
name = "iggy-cli"
-version = "0.14.0-edge.3"
+version = "0.14.0-edge.4"
dependencies = [
"anyhow",
"apple-native-keyring-store",
@@ -6747,7 +6718,7 @@
[[package]]
name = "iggy-connectors"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
dependencies = [
"async-trait",
"axum",
@@ -6799,7 +6770,7 @@
[[package]]
name = "iggy-mcp"
-version = "0.5.0-edge.2"
+version = "0.5.0-edge.3"
dependencies = [
"axum",
"axum-server",
@@ -6833,7 +6804,7 @@
[[package]]
name = "iggy_binary_protocol"
-version = "0.11.0-edge.3"
+version = "0.11.0-edge.4"
dependencies = [
"aligned-vec",
"bytemuck",
@@ -6846,7 +6817,7 @@
[[package]]
name = "iggy_common"
-version = "0.11.0-edge.3"
+version = "0.11.0-edge.4"
dependencies = [
"aes-gcm",
"async-broadcast",
@@ -6862,7 +6833,6 @@
"derive_more",
"humantime",
"iggy_binary_protocol",
- "lending-iterator",
"papaya",
"ring",
"secrecy",
@@ -6882,7 +6852,7 @@
[[package]]
name = "iggy_connector_clickhouse_sink"
-version = "0.2.0-edge.3"
+version = "0.2.0-edge.4"
dependencies = [
"async-trait",
"bytes",
@@ -6900,7 +6870,7 @@
[[package]]
name = "iggy_connector_delta_sink"
-version = "0.2.0-edge.3"
+version = "0.2.0-edge.4"
dependencies = [
"async-trait",
"chrono",
@@ -6917,7 +6887,7 @@
[[package]]
name = "iggy_connector_doris_sink"
-version = "0.2.0-edge.3"
+version = "0.2.0-edge.4"
dependencies = [
"async-trait",
"base64",
@@ -6937,7 +6907,7 @@
[[package]]
name = "iggy_connector_elasticsearch_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
dependencies = [
"async-trait",
"base64",
@@ -6955,7 +6925,7 @@
[[package]]
name = "iggy_connector_elasticsearch_source"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
dependencies = [
"async-trait",
"dashmap",
@@ -6973,7 +6943,7 @@
[[package]]
name = "iggy_connector_http_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
dependencies = [
"async-trait",
"base64",
@@ -6995,7 +6965,7 @@
[[package]]
name = "iggy_connector_iceberg_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
dependencies = [
"arrow-json 57.3.1",
"async-trait",
@@ -7014,7 +6984,7 @@
[[package]]
name = "iggy_connector_influxdb_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
dependencies = [
"async-trait",
"axum",
@@ -7035,7 +7005,7 @@
[[package]]
name = "iggy_connector_influxdb_source"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
dependencies = [
"ahash 0.8.12",
"async-trait",
@@ -7061,7 +7031,7 @@
[[package]]
name = "iggy_connector_meilisearch_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
dependencies = [
"async-trait",
"base64",
@@ -7079,7 +7049,7 @@
[[package]]
name = "iggy_connector_mongodb_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
dependencies = [
"async-trait",
"humantime",
@@ -7095,7 +7065,7 @@
[[package]]
name = "iggy_connector_postgres_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
dependencies = [
"async-trait",
"dashmap",
@@ -7114,7 +7084,7 @@
[[package]]
name = "iggy_connector_postgres_source"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
dependencies = [
"async-trait",
"base64",
@@ -7136,7 +7106,7 @@
[[package]]
name = "iggy_connector_quickwit_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
dependencies = [
"async-trait",
"dashmap",
@@ -7150,7 +7120,7 @@
[[package]]
name = "iggy_connector_random_source"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
dependencies = [
"async-trait",
"dashmap",
@@ -7167,7 +7137,7 @@
[[package]]
name = "iggy_connector_s3_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
dependencies = [
"async-trait",
"base64",
@@ -7188,7 +7158,7 @@
[[package]]
name = "iggy_connector_sdk"
-version = "0.4.0-edge.2"
+version = "0.4.0-edge.3"
dependencies = [
"anyhow",
"apache-avro",
@@ -7224,7 +7194,7 @@
[[package]]
name = "iggy_connector_stdout_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
dependencies = [
"async-trait",
"dashmap",
@@ -7236,7 +7206,7 @@
[[package]]
name = "iggy_connector_surrealdb_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
dependencies = [
"async-trait",
"base64",
@@ -7851,31 +7821,6 @@
]
[[package]]
-name = "lending-iterator"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc07588c853b50689205fb5c00498aa681d89828e0ce8cbd965ebc7a5d8ae260"
-dependencies = [
- "extension-traits",
- "lending-iterator-proc_macros",
- "macro_rules_attribute",
- "never-say-never",
- "nougat",
- "polonius-the-crab",
-]
-
-[[package]]
-name = "lending-iterator-proc_macros"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5445dd1c0deb1e97b8a16561d17fc686ca83e8411128fb036e9668a72d51b1d"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
name = "lexical-core"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8295,22 +8240,6 @@
]
[[package]]
-name = "macro_rules_attribute"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf0c9b980bf4f3a37fd7b1c066941dd1b1d0152ce6ee6e8fe8c49b9f6810d862"
-dependencies = [
- "macro_rules_attribute-proc_macro",
- "paste",
-]
-
-[[package]]
-name = "macro_rules_attribute-proc_macro"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58093314a45e00c77d5c508f76e77c3396afbbc0d01506e7fae47b018bac2b1d"
-
-[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8730,12 +8659,6 @@
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
[[package]]
-name = "never-say-never"
-version = "6.6.666"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf5a574dadd7941adeaa71823ecba5e28331b8313fb2e1c6a5c7e5981ea53ad6"
-
-[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8844,27 +8767,6 @@
]
[[package]]
-name = "nougat"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97b57b9ced431322f054fc673f1d3c7fa52d80efd9df74ad2fc759f044742510"
-dependencies = [
- "macro_rules_attribute",
- "nougat-proc_macros",
-]
-
-[[package]]
-name = "nougat-proc_macros"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c84f77a45e99a2f9b492695d99e1c23844619caa5f3e57647cffacad773ca257"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
name = "ntapi"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -9885,12 +9787,6 @@
]
[[package]]
-name = "polonius-the-crab"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2a69ee997a6282f8462abf1e0d8c38c965e968799e912b3bed8c9e8a28c2f9f"
-
-[[package]]
name = "polyval"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -11838,7 +11734,7 @@
[[package]]
name = "server"
-version = "0.9.0-edge.4"
+version = "0.9.0-edge.5"
dependencies = [
"ahash 0.8.12",
"argon2",
diff --git a/Cargo.toml b/Cargo.toml
index 784e3ac..eff5c66 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -199,11 +199,11 @@
iceberg = "0.9.1"
iceberg-catalog-rest = "0.9.1"
iceberg-storage-opendal = "0.9.1"
-iggy = { path = "core/sdk", version = "0.11.0-edge.3" }
-iggy-cli = { path = "core/cli", version = "0.14.0-edge.3" }
-iggy_binary_protocol = { path = "core/binary_protocol", version = "0.11.0-edge.3" }
-iggy_common = { path = "core/common", version = "0.11.0-edge.3" }
-iggy_connector_sdk = { path = "core/connectors/sdk", version = "0.4.0-edge.2" }
+iggy = { path = "core/sdk", version = "0.11.0-edge.4" }
+iggy-cli = { path = "core/cli", version = "0.14.0-edge.4" }
+iggy_binary_protocol = { path = "core/binary_protocol", version = "0.11.0-edge.4" }
+iggy_common = { path = "core/common", version = "0.11.0-edge.4" }
+iggy_connector_sdk = { path = "core/connectors/sdk", version = "0.4.0-edge.3" }
indexmap = "2.14.0"
integration = { path = "core/integration" }
ipnet = "2.12.0"
@@ -213,7 +213,6 @@
keyring-core = "1.0.0"
lazy_static = "1.5.0"
left-right = "0.11"
-lending-iterator = "0.1.7"
libc = "0.2.188"
log = "0.4.33"
lz4_flex = "0.14.0"
diff --git a/bdd/go/go.mod b/bdd/go/go.mod
index 8bbd9df..1106967 100644
--- a/bdd/go/go.mod
+++ b/bdd/go/go.mod
@@ -25,7 +25,9 @@
github.com/hashicorp/go-memdb v1.3.5 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/klauspost/compress v1.19.2 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/spf13/pflag v1.0.10 // indirect
+ github.com/zeebo/xxh3 v1.1.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.56.0 // indirect
diff --git a/bdd/go/go.sum b/bdd/go/go.sum
index 4a53ed4..3767365 100644
--- a/bdd/go/go.sum
+++ b/bdd/go/go.sum
@@ -43,6 +43,8 @@
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -71,6 +73,10 @@
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
+github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
+github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
+github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
+github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
diff --git a/bdd/python/uv.lock b/bdd/python/uv.lock
index a5ebae6..e7014a8 100644
--- a/bdd/python/uv.lock
+++ b/bdd/python/uv.lock
@@ -8,7 +8,7 @@
[[package]]
name = "apache-iggy"
-version = "0.9.0.dev3"
+version = "0.9.0.dev4"
source = { directory = "../../foreign/python" }
[package.metadata]
diff --git a/core/ai/mcp/Cargo.toml b/core/ai/mcp/Cargo.toml
index 0d061d1..10e920b 100644
--- a/core/ai/mcp/Cargo.toml
+++ b/core/ai/mcp/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy-mcp"
-version = "0.5.0-edge.2"
+version = "0.5.0-edge.3"
description = "MCP Server for Iggy message streaming platform"
edition = "2024"
license = "Apache-2.0"
diff --git a/core/bench/Cargo.toml b/core/bench/Cargo.toml
index d9e3a57..922af4d 100644
--- a/core/bench/Cargo.toml
+++ b/core/bench/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy-bench"
-version = "0.6.0-edge.3"
+version = "0.6.0-edge.4"
edition = "2024"
license = "Apache-2.0"
repository = "https://github.com/apache/iggy"
diff --git a/core/bench/dashboard/frontend/Cargo.toml b/core/bench/dashboard/frontend/Cargo.toml
index 0cc8969..c76ae74 100644
--- a/core/bench/dashboard/frontend/Cargo.toml
+++ b/core/bench/dashboard/frontend/Cargo.toml
@@ -18,7 +18,7 @@
[package]
name = "bench-dashboard-frontend"
license = "Apache-2.0"
-version = "0.8.0-edge.2"
+version = "0.8.0-edge.3"
edition = "2024"
publish = false
diff --git a/core/bench/dashboard/server/Cargo.toml b/core/bench/dashboard/server/Cargo.toml
index a57782a..82ca242 100644
--- a/core/bench/dashboard/server/Cargo.toml
+++ b/core/bench/dashboard/server/Cargo.toml
@@ -18,7 +18,7 @@
[package]
name = "iggy-bench-dashboard-server"
license = "Apache-2.0"
-version = "0.8.0-edge.2"
+version = "0.8.0-edge.3"
edition = "2024"
publish = false
diff --git a/core/bench/report/Cargo.toml b/core/bench/report/Cargo.toml
index 5aa1065..88b7dac 100644
--- a/core/bench/report/Cargo.toml
+++ b/core/bench/report/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "bench-report"
-version = "0.4.0-edge.2"
+version = "0.4.0-edge.3"
edition = "2024"
description = "Benchmark report and chart generation library for iggy-bench binary and iggy-benchmarks-dashboard web app"
license = "Apache-2.0"
diff --git a/core/binary_protocol/Cargo.toml b/core/binary_protocol/Cargo.toml
index a52840d..09fee0d 100644
--- a/core/binary_protocol/Cargo.toml
+++ b/core/binary_protocol/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_binary_protocol"
-version = "0.11.0-edge.3"
+version = "0.11.0-edge.4"
description = "Wire protocol types and codec for the Iggy binary protocol. Shared between server and SDK."
edition = "2024"
rust-version.workspace = true
diff --git a/core/binary_protocol/src/batch.rs b/core/binary_protocol/src/batch.rs
new file mode 100644
index 0000000..995abd1
--- /dev/null
+++ b/core/binary_protocol/src/batch.rs
@@ -0,0 +1,663 @@
+// 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.
+
+//! The message batch: the one layout a `SendMessages` body, the replicated
+//! prepare, the persisted segment record, and the poll reply all share.
+//!
+//! ```text
+//! [batch header: 256 bytes][blob: message frames]
+//! frame = [header: 48 bytes][payload][user_headers]
+//! ```
+//!
+//! Producers encode this batch directly on the wire (after the routing
+//! metadata section); the server stamps `partition_id`, `base_offset`, and
+//! `base_timestamp` at ingestion/persistence and replicates the bytes
+//! verbatim; polls serve the stored records back. There is no other message
+//! encoding.
+
+use crate::WireError;
+use std::hash::Hasher;
+use twox_hash::XxHash3_64;
+
+/// Size of the batch header. The remainder past the fields below is reserved
+/// and must be zero.
+pub const BATCH_HEADER_SIZE: usize = 256;
+
+/// Size of a message frame header inside the blob.
+pub const BATCH_MESSAGE_HEADER_SIZE: usize = 48;
+
+/// Byte offset of `batch_checksum` inside the batch header.
+pub const BATCH_CHECKSUM_OFFSET: usize = 40;
+
+/// Byte offset of `message_count` inside the batch header.
+pub const BATCH_MESSAGE_COUNT_OFFSET: usize = 48;
+
+/// Byte offset where the reserved region of the batch header starts.
+pub const BATCH_RESERVED_OFFSET: usize = 52;
+
+/// Upper bound on a message's `timestamp_delta`: the field is a `u32`
+/// microsecond delta against the batch `origin_timestamp`, so a single batch
+/// spans at most ~71.6 minutes of producer clock.
+pub const MAX_TIMESTAMP_DELTA_MICROS: u64 = u32::MAX as u64;
+
+/// The batch header.
+///
+/// A producer encodes it with `partition_id`, `base_offset`, and
+/// `base_timestamp` zero; the server owns those three fields and stamps them
+/// at ingestion (`partition_id`) and persistence (`base_offset`,
+/// `base_timestamp`, plus the `batch_checksum` recompute).
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct BatchHeader {
+ pub partition_id: u64,
+ pub base_offset: u64,
+ pub base_timestamp: u64,
+ pub origin_timestamp: u64,
+ /// Total batch size: `BATCH_HEADER_SIZE` + blob length.
+ pub batch_length: u64,
+ pub batch_checksum: u64,
+ pub message_count: u32,
+}
+
+impl BatchHeader {
+ #[must_use]
+ pub const fn new(
+ partition_id: u64,
+ origin_timestamp: u64,
+ batch_length: u64,
+ message_count: u32,
+ ) -> Self {
+ Self {
+ partition_id,
+ base_offset: 0,
+ base_timestamp: 0,
+ origin_timestamp,
+ batch_length,
+ batch_checksum: 0,
+ message_count,
+ }
+ }
+
+ /// # Errors
+ /// [`WireError::UnexpectedEof`] on a short buffer;
+ /// [`WireError::Validation`] on a `batch_length` smaller than the header
+ /// or nonzero reserved bytes.
+ pub fn decode(bytes: &[u8]) -> Result<Self, WireError> {
+ if bytes.len() < BATCH_HEADER_SIZE {
+ return Err(WireError::UnexpectedEof {
+ offset: 0,
+ need: BATCH_HEADER_SIZE,
+ have: bytes.len(),
+ });
+ }
+
+ let batch_length = read_u64(bytes, 32);
+ if batch_length < BATCH_HEADER_SIZE as u64 {
+ return Err(WireError::Validation(std::borrow::Cow::Borrowed(
+ "batch length must cover the batch header",
+ )));
+ }
+
+ // Every encoder zeroes the reserved region. Admitting nonzero bytes
+ // would let unchecksummed data ride the header to disk and replicas.
+ if bytes[BATCH_RESERVED_OFFSET..BATCH_HEADER_SIZE]
+ .iter()
+ .any(|&reserved_byte| reserved_byte != 0)
+ {
+ return Err(WireError::Validation(std::borrow::Cow::Borrowed(
+ "batch header reserved bytes must be zero",
+ )));
+ }
+
+ Ok(Self {
+ partition_id: read_u64(bytes, 0),
+ base_offset: read_u64(bytes, 8),
+ base_timestamp: read_u64(bytes, 16),
+ origin_timestamp: read_u64(bytes, 24),
+ batch_length,
+ batch_checksum: read_u64(bytes, BATCH_CHECKSUM_OFFSET),
+ message_count: read_u32(bytes, BATCH_MESSAGE_COUNT_OFFSET),
+ })
+ }
+
+ /// # Panics
+ /// Panics if `bytes` is shorter than [`BATCH_HEADER_SIZE`].
+ pub fn encode_into(&self, bytes: &mut [u8]) {
+ assert!(bytes.len() >= BATCH_HEADER_SIZE);
+ bytes[..BATCH_HEADER_SIZE].fill(0);
+ bytes[0..8].copy_from_slice(&self.partition_id.to_le_bytes());
+ bytes[8..16].copy_from_slice(&self.base_offset.to_le_bytes());
+ bytes[16..24].copy_from_slice(&self.base_timestamp.to_le_bytes());
+ bytes[24..32].copy_from_slice(&self.origin_timestamp.to_le_bytes());
+ bytes[32..40].copy_from_slice(&self.batch_length.to_le_bytes());
+ bytes[BATCH_CHECKSUM_OFFSET..BATCH_CHECKSUM_OFFSET + 8]
+ .copy_from_slice(&self.batch_checksum.to_le_bytes());
+ bytes[BATCH_MESSAGE_COUNT_OFFSET..BATCH_MESSAGE_COUNT_OFFSET + 4]
+ .copy_from_slice(&self.message_count.to_le_bytes());
+ }
+
+ /// Total batch size in bytes (header + blob).
+ ///
+ /// # Panics
+ /// Panics if `batch_length` exceeds `usize::MAX`.
+ #[must_use]
+ pub fn total_size(&self) -> usize {
+ usize::try_from(self.batch_length).expect("batch length exceeds usize::MAX")
+ }
+
+ /// # Errors
+ /// [`WireError::Validation`] if `batch_length` does not cover the header
+ /// or exceeds `usize`.
+ pub fn blob_len(&self) -> Result<usize, WireError> {
+ self.batch_length
+ .checked_sub(BATCH_HEADER_SIZE as u64)
+ .and_then(|len| usize::try_from(len).ok())
+ .ok_or(WireError::Validation(std::borrow::Cow::Borrowed(
+ "batch length must cover the batch header",
+ )))
+ }
+
+ #[must_use]
+ pub fn checksum_for_blob(&self, blob: &[u8]) -> u64 {
+ calculate_batch_checksum(self, blob)
+ }
+}
+
+/// A decoded batch borrowing its blob.
+#[derive(Debug, Clone, Copy)]
+pub struct BatchRef<'a> {
+ pub header: BatchHeader,
+ blob: &'a [u8],
+}
+
+impl<'a> BatchRef<'a> {
+ #[must_use]
+ pub const fn new(header: BatchHeader, blob: &'a [u8]) -> Self {
+ Self { header, blob }
+ }
+
+ #[must_use]
+ pub const fn iter(&self) -> BatchIterator<'a> {
+ BatchIterator {
+ blob: self.blob,
+ position: 0,
+ }
+ }
+
+ #[must_use]
+ pub const fn iter_with_offsets(&self) -> BatchIteratorWithOffsets<'a> {
+ BatchIteratorWithOffsets {
+ blob: self.blob,
+ position: 0,
+ }
+ }
+
+ #[must_use]
+ pub const fn blob(&self) -> &'a [u8] {
+ self.blob
+ }
+
+ #[must_use]
+ pub const fn message_count(&self) -> u32 {
+ self.header.message_count
+ }
+}
+
+impl<'a> IntoIterator for &BatchRef<'a> {
+ type Item = BatchMessageView<'a>;
+ type IntoIter = BatchIterator<'a>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter()
+ }
+}
+
+/// A message frame header inside the blob.
+#[derive(Debug, Clone, Copy)]
+pub struct BatchMessageHeader {
+ /// `XxHash3_64` over `frame[8..48] || payload || user_headers`.
+ pub checksum: u64,
+ pub id: u128,
+ pub offset_delta: u32,
+ /// Microsecond delta from [`BatchHeader::origin_timestamp`].
+ pub timestamp_delta: u32,
+ pub user_headers_length: u32,
+ pub payload_length: u32,
+}
+
+impl BatchMessageHeader {
+ /// # Errors
+ /// [`WireError::UnexpectedEof`] on a short buffer; [`WireError::Validation`]
+ /// on nonzero reserved bytes.
+ pub fn decode(bytes: &[u8]) -> Result<Self, WireError> {
+ if bytes.len() < BATCH_MESSAGE_HEADER_SIZE {
+ return Err(WireError::UnexpectedEof {
+ offset: 0,
+ need: BATCH_MESSAGE_HEADER_SIZE,
+ have: bytes.len(),
+ });
+ }
+
+ if read_u64(bytes, 40) != 0 {
+ return Err(WireError::Validation(std::borrow::Cow::Borrowed(
+ "message frame reserved bytes must be zero",
+ )));
+ }
+
+ Ok(Self {
+ checksum: read_u64(bytes, 0),
+ id: read_u128(bytes, 8),
+ offset_delta: read_u32(bytes, 24),
+ timestamp_delta: read_u32(bytes, 28),
+ user_headers_length: read_u32(bytes, 32),
+ payload_length: read_u32(bytes, 36),
+ })
+ }
+
+ /// Frame size: header + payload + user headers.
+ #[must_use]
+ pub const fn total_size(&self) -> usize {
+ BATCH_MESSAGE_HEADER_SIZE + self.user_headers_length as usize + self.payload_length as usize
+ }
+}
+
+/// A message frame view borrowing payload and user headers from the blob.
+#[derive(Debug, Clone, Copy)]
+pub struct BatchMessageView<'a> {
+ pub header: BatchMessageHeader,
+ pub user_headers: &'a [u8],
+ pub payload: &'a [u8],
+}
+
+/// Infallible frame walk over a blob whose layout has already been proven by
+/// [`decode_batch_slice_with`]: on an unvalidated blob it stops at the first
+/// malformed frame instead of erroring.
+pub struct BatchIterator<'a> {
+ blob: &'a [u8],
+ position: usize,
+}
+
+impl<'a> Iterator for BatchIterator<'a> {
+ type Item = BatchMessageView<'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.position >= self.blob.len() {
+ return None;
+ }
+
+ let header = BatchMessageHeader::decode(&self.blob[self.position..]).ok()?;
+ let start = self.position + BATCH_MESSAGE_HEADER_SIZE;
+ let payload_end = start + header.payload_length as usize;
+ let headers_end = payload_end + header.user_headers_length as usize;
+ let payload = self.blob.get(start..payload_end)?;
+ let user_headers = self.blob.get(payload_end..headers_end)?;
+ self.position += header.total_size();
+ Some(BatchMessageView {
+ header,
+ user_headers,
+ payload,
+ })
+ }
+}
+
+/// A frame view plus its byte range inside the blob.
+#[derive(Debug, Clone, Copy)]
+pub struct BatchMessageViewWithOffsets<'a> {
+ pub message: BatchMessageView<'a>,
+ pub start: usize,
+ pub end: usize,
+}
+
+pub struct BatchIteratorWithOffsets<'a> {
+ blob: &'a [u8],
+ position: usize,
+}
+
+impl<'a> Iterator for BatchIteratorWithOffsets<'a> {
+ type Item = BatchMessageViewWithOffsets<'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.position >= self.blob.len() {
+ return None;
+ }
+
+ let start = self.position;
+ let header = BatchMessageHeader::decode(&self.blob[self.position..]).ok()?;
+ let message_start = self.position + BATCH_MESSAGE_HEADER_SIZE;
+ let payload_end = message_start + header.payload_length as usize;
+ let headers_end = payload_end + header.user_headers_length as usize;
+ let payload = self.blob.get(message_start..payload_end)?;
+ let user_headers = self.blob.get(payload_end..headers_end)?;
+ self.position += header.total_size();
+ Some(BatchMessageViewWithOffsets {
+ message: BatchMessageView {
+ header,
+ user_headers,
+ payload,
+ },
+ start,
+ end: self.position,
+ })
+ }
+}
+
+/// How much of a batch record [`decode_batch_slice_with`] proves before returning it.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum BatchIntegrity {
+ /// Re-hash the batch and reject it unless it matches its own `batch_checksum`.
+ Verify,
+ /// Check the framing only, and hand back whatever it describes. The caller is
+ /// accepting bytes that may not be the ones written.
+ LayoutOnly,
+}
+
+/// Decode one batch record (`[256B header][blob]`), verifying the batch
+/// checksum and every per-message checksum.
+///
+/// `body` may extend past the batch: readers walking a stream of records hand
+/// in the rest of the buffer and step by `batch_length`. Callers whose buffer
+/// is meant to BE the batch must reject the surplus themselves.
+///
+/// # Errors
+/// [`WireError`] on a short or self-inconsistent record, and
+/// [`WireError::InvalidBatchChecksum`] / [`WireError::InvalidMessageChecksum`]
+/// on an integrity mismatch.
+pub fn decode_batch_slice(body: &[u8]) -> Result<BatchRef<'_>, WireError> {
+ decode_batch_slice_with(body, BatchIntegrity::Verify)
+}
+
+/// [`decode_batch_slice`] with the integrity level chosen by the caller.
+///
+/// Layout checks are not optional either way: a short or self-inconsistent
+/// record is rejected regardless, because the caller would otherwise index
+/// past it.
+///
+/// # Errors
+/// See [`decode_batch_slice`]; [`BatchIntegrity::LayoutOnly`] skips only the
+/// checksum comparison.
+pub fn decode_batch_slice_with(
+ body: &[u8],
+ integrity: BatchIntegrity,
+) -> Result<BatchRef<'_>, WireError> {
+ let header = BatchHeader::decode(body)?;
+ let blob_len = header.blob_len()?;
+ if body.len() < header.total_size() {
+ return Err(WireError::UnexpectedEof {
+ offset: 0,
+ need: header.total_size(),
+ have: body.len(),
+ });
+ }
+
+ let blob = &body[BATCH_HEADER_SIZE..BATCH_HEADER_SIZE + blob_len];
+ let batch = BatchRef { header, blob };
+ match integrity {
+ BatchIntegrity::Verify => {
+ let expected_checksum = verify_and_recompute_batch_checksum(&batch)?;
+ if header.batch_checksum != expected_checksum {
+ return Err(WireError::InvalidBatchChecksum {
+ stored: header.batch_checksum,
+ computed: expected_checksum,
+ base_offset: header.base_offset,
+ });
+ }
+ }
+ BatchIntegrity::LayoutOnly => validate_batch_layout(&batch)?,
+ }
+
+ Ok(batch)
+}
+
+/// Batch checksum: streaming `XxHash3_64` over the six batch header meta
+/// fields followed by each message's stored 8-byte checksum field in message
+/// order - NOT the message bodies.
+///
+/// Bodies are bound only transitively: each per-message checksum already covers
+/// `frame[8..48] || payload || user_headers`, so hashing the checksum fields
+/// binds every body byte IFF a reader also re-verifies the per-message
+/// checksums. Stamping hashes `N * 8` bytes instead of the whole blob;
+/// validating decoders pay the one body pass as the per-message verify in
+/// [`verify_and_recompute_batch_checksum`], which hashes the checksum-field
+/// bytes in the same order so its recompute matches a compute here.
+///
+/// Assumes a well-formed blob whose frames tile exactly; every compute site
+/// builds the blob and satisfies this.
+#[must_use]
+pub fn calculate_batch_checksum(header: &BatchHeader, blob: &[u8]) -> u64 {
+ let mut hasher = XxHash3_64::new();
+ write_batch_header_fields(&mut hasher, header);
+ let batch = BatchRef {
+ header: *header,
+ blob,
+ };
+ for framed in batch.iter_with_offsets() {
+ hasher.write(&blob[framed.start..framed.start + 8]);
+ }
+ hasher.finish()
+}
+
+fn write_batch_header_fields(hasher: &mut XxHash3_64, header: &BatchHeader) {
+ hasher.write(&header.partition_id.to_le_bytes());
+ hasher.write(&header.base_offset.to_le_bytes());
+ hasher.write(&header.base_timestamp.to_le_bytes());
+ hasher.write(&header.origin_timestamp.to_le_bytes());
+ hasher.write(&header.batch_length.to_le_bytes());
+ hasher.write(&header.message_count.to_le_bytes());
+}
+
+/// Verify every per-message checksum in `batch` and return the recomputed
+/// batch checksum (see [`calculate_batch_checksum`]) from a single frame walk.
+///
+/// The per-message pass is the equal-integrity half of the scheme: the batch
+/// value binds bodies only through the checksum fields, so a validating decode
+/// must re-verify each message here or body corruption that leaves the
+/// checksum field intact would pass. This is the one full-body pass a
+/// validating decode pays; the caller then compares the returned value against
+/// the stored `batch_checksum`.
+///
+/// # Errors
+/// [`WireError::InvalidMessageChecksum`] on the first per-message mismatch;
+/// [`WireError::Validation`] if the frames do not tile `message_count` exactly.
+pub fn verify_and_recompute_batch_checksum(batch: &BatchRef<'_>) -> Result<u64, WireError> {
+ let blob = batch.blob();
+ let mut hasher = XxHash3_64::new();
+ write_batch_header_fields(&mut hasher, &batch.header);
+ let mut verified = 0u32;
+ let mut covered = 0usize;
+ for framed in batch.iter_with_offsets() {
+ // Cover (`frame[8..48] || payload || user_headers`) hashed raw from the
+ // blob, byte-exact with the encoder's, so a flipped body byte fails even
+ // when the stored checksum field is left intact.
+ let stored = framed.message.header.checksum;
+ let expected = XxHash3_64::oneshot(&blob[framed.start + 8..framed.end]);
+ if expected != stored {
+ return Err(WireError::InvalidMessageChecksum {
+ stored,
+ computed: expected,
+ offset: batch
+ .header
+ .base_offset
+ .saturating_add(u64::from(framed.message.header.offset_delta)),
+ });
+ }
+ hasher.write(&blob[framed.start..framed.start + 8]);
+ verified += 1;
+ covered = framed.end;
+ }
+ if verified != batch.message_count() || covered != blob.len() {
+ return Err(WireError::Validation(std::borrow::Cow::Borrowed(
+ "batch frames do not tile message_count exactly",
+ )));
+ }
+ Ok(hasher.finish())
+}
+
+/// Layout-only twin of [`verify_and_recompute_batch_checksum`]: prove the
+/// frames tile `message_count` exactly without touching any checksum.
+///
+/// # Errors
+/// [`WireError::Validation`] if the frames do not tile `message_count` exactly.
+fn validate_batch_layout(batch: &BatchRef<'_>) -> Result<(), WireError> {
+ let blob = batch.blob();
+ let mut counted = 0u32;
+ let mut covered = 0usize;
+ for framed in batch.iter_with_offsets() {
+ counted += 1;
+ covered = framed.end;
+ }
+ if counted != batch.message_count() || covered != blob.len() {
+ return Err(WireError::Validation(std::borrow::Cow::Borrowed(
+ "batch frames do not tile message_count exactly",
+ )));
+ }
+ Ok(())
+}
+
+fn read_u32(bytes: &[u8], offset: usize) -> u32 {
+ u32::from_le_bytes(bytes[offset..offset + 4].try_into().expect("4-byte slice"))
+}
+
+fn read_u64(bytes: &[u8], offset: usize) -> u64 {
+ u64::from_le_bytes(bytes[offset..offset + 8].try_into().expect("8-byte slice"))
+}
+
+fn read_u128(bytes: &[u8], offset: usize) -> u128 {
+ u128::from_le_bytes(
+ bytes[offset..offset + 16]
+ .try_into()
+ .expect("16-byte slice"),
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[allow(clippy::cast_possible_truncation)]
+ fn frame(id: u128, offset_delta: u32, timestamp_delta: u32, payload: &[u8]) -> Vec<u8> {
+ let mut bytes = vec![0u8; BATCH_MESSAGE_HEADER_SIZE];
+ bytes[8..24].copy_from_slice(&id.to_le_bytes());
+ bytes[24..28].copy_from_slice(&offset_delta.to_le_bytes());
+ bytes[28..32].copy_from_slice(×tamp_delta.to_le_bytes());
+ bytes[36..40].copy_from_slice(&(payload.len() as u32).to_le_bytes());
+ bytes.extend_from_slice(payload);
+ let checksum = XxHash3_64::oneshot(&bytes[8..]);
+ bytes[0..8].copy_from_slice(&checksum.to_le_bytes());
+ bytes
+ }
+
+ #[allow(clippy::cast_possible_truncation)]
+ fn batch_bytes(frames: &[Vec<u8>]) -> Vec<u8> {
+ let blob: Vec<u8> = frames.concat();
+ let mut header = BatchHeader::new(
+ 7,
+ 1_000,
+ (BATCH_HEADER_SIZE + blob.len()) as u64,
+ frames.len() as u32,
+ );
+ header.batch_checksum = calculate_batch_checksum(&header, &blob);
+ let mut bytes = vec![0u8; BATCH_HEADER_SIZE];
+ header.encode_into(&mut bytes);
+ bytes.extend_from_slice(&blob);
+ bytes
+ }
+
+ #[test]
+ fn header_roundtrip() {
+ let mut header = BatchHeader::new(1, 2, 300, 4);
+ header.base_offset = 10;
+ header.base_timestamp = 20;
+ header.batch_checksum = 30;
+ let mut bytes = vec![0u8; BATCH_HEADER_SIZE];
+ header.encode_into(&mut bytes);
+ let decoded = BatchHeader::decode(&bytes).unwrap();
+ assert_eq!(decoded, header);
+ }
+
+ #[test]
+ fn decode_verifies_checksums() {
+ let bytes = batch_bytes(&[frame(1, 0, 0, b"first"), frame(2, 1, 5, b"second")]);
+ let batch = decode_batch_slice(&bytes).unwrap();
+ assert_eq!(batch.message_count(), 2);
+ let views: Vec<_> = batch.iter().collect();
+ assert_eq!(views[0].payload, b"first");
+ assert_eq!(views[1].payload, b"second");
+ assert_eq!(views[1].header.offset_delta, 1);
+ }
+
+ #[test]
+ fn decode_rejects_flipped_body_byte() {
+ let mut bytes = batch_bytes(&[frame(1, 0, 0, b"payload")]);
+ let last = bytes.len() - 1;
+ bytes[last] ^= 0xFF;
+ assert!(matches!(
+ decode_batch_slice(&bytes),
+ Err(WireError::InvalidMessageChecksum { .. })
+ ));
+ }
+
+ #[test]
+ fn decode_rejects_flipped_batch_checksum() {
+ let mut bytes = batch_bytes(&[frame(1, 0, 0, b"payload")]);
+ bytes[BATCH_CHECKSUM_OFFSET] ^= 0xFF;
+ assert!(matches!(
+ decode_batch_slice(&bytes),
+ Err(WireError::InvalidBatchChecksum { .. })
+ ));
+ }
+
+ #[test]
+ fn layout_only_accepts_zero_checksums() {
+ let mut bytes = batch_bytes(&[frame(1, 0, 0, b"payload")]);
+ bytes[BATCH_CHECKSUM_OFFSET..BATCH_CHECKSUM_OFFSET + 8].fill(0);
+ let batch = decode_batch_slice_with(&bytes, BatchIntegrity::LayoutOnly).unwrap();
+ assert_eq!(batch.message_count(), 1);
+ }
+
+ #[test]
+ fn decode_rejects_miscounted_batch() {
+ let mut bytes = batch_bytes(&[frame(1, 0, 0, b"payload")]);
+ bytes[BATCH_MESSAGE_COUNT_OFFSET..BATCH_MESSAGE_COUNT_OFFSET + 4]
+ .copy_from_slice(&2u32.to_le_bytes());
+ assert!(decode_batch_slice_with(&bytes, BatchIntegrity::LayoutOnly).is_err());
+ }
+
+ #[test]
+ fn decode_rejects_truncated_batch() {
+ let bytes = batch_bytes(&[frame(1, 0, 0, b"payload")]);
+ assert!(decode_batch_slice(&bytes[..bytes.len() - 1]).is_err());
+ }
+
+ #[test]
+ fn decode_rejects_nonzero_reserved() {
+ let mut bytes = batch_bytes(&[frame(1, 0, 0, b"payload")]);
+ bytes[BATCH_HEADER_SIZE + 40] = 1;
+ assert!(decode_batch_slice_with(&bytes, BatchIntegrity::LayoutOnly).is_err());
+ }
+
+ #[test]
+ fn decode_rejects_nonzero_header_reserved() {
+ let mut bytes = batch_bytes(&[frame(1, 0, 0, b"payload")]);
+ bytes[BATCH_RESERVED_OFFSET] = 1;
+ assert!(matches!(
+ BatchHeader::decode(&bytes),
+ Err(WireError::Validation(_))
+ ));
+ bytes[BATCH_RESERVED_OFFSET] = 0;
+ bytes[BATCH_HEADER_SIZE - 1] = 1;
+ assert!(BatchHeader::decode(&bytes).is_err());
+ }
+}
diff --git a/core/binary_protocol/src/codes.rs b/core/binary_protocol/src/codes.rs
index 9042029..85f0b97 100644
--- a/core/binary_protocol/src/codes.rs
+++ b/core/binary_protocol/src/codes.rs
@@ -58,8 +58,6 @@
pub const GET_CONSUMER_OFFSET_CODE: u32 = 120;
pub const STORE_CONSUMER_OFFSET_CODE: u32 = 121;
pub const DELETE_CONSUMER_OFFSET_CODE: u32 = 122;
-pub const STORE_CONSUMER_OFFSET_2_CODE: u32 = 123;
-pub const DELETE_CONSUMER_OFFSET_2_CODE: u32 = 124;
// -- Streams --
pub const GET_STREAM_CODE: u32 = 200;
@@ -137,8 +135,6 @@
GET_CONSUMER_OFFSET_CODE,
STORE_CONSUMER_OFFSET_CODE,
DELETE_CONSUMER_OFFSET_CODE,
- STORE_CONSUMER_OFFSET_2_CODE,
- DELETE_CONSUMER_OFFSET_2_CODE,
GET_STREAM_CODE,
GET_STREAMS_CODE,
CREATE_STREAM_CODE,
diff --git a/core/binary_protocol/src/consensus/command.rs b/core/binary_protocol/src/consensus/command.rs
index f358c3c..81e8033 100644
--- a/core/binary_protocol/src/consensus/command.rs
+++ b/core/binary_protocol/src/consensus/command.rs
@@ -21,7 +21,7 @@
/// VSR message type discriminant.
#[derive(Default, Debug, EnumSetType)]
#[repr(u8)]
-pub enum Command2 {
+pub enum Command {
#[default]
Reserved = 0,
@@ -85,11 +85,11 @@
ForwardLogoutResult = 29,
}
-// SAFETY: Command2 is #[repr(u8)] with no padding bytes.
-unsafe impl NoUninit for Command2 {}
+// SAFETY: Command is #[repr(u8)] with no padding bytes.
+unsafe impl NoUninit for Command {}
-// SAFETY: Command2 is #[repr(u8)]; is_valid_bit_pattern matches all defined discriminants.
-unsafe impl CheckedBitPattern for Command2 {
+// SAFETY: Command is #[repr(u8)]; is_valid_bit_pattern matches all defined discriminants.
+unsafe impl CheckedBitPattern for Command {
type Bits = u8;
fn is_valid_bit_pattern(bits: &u8) -> bool {
diff --git a/core/binary_protocol/src/consensus/error.rs b/core/binary_protocol/src/consensus/error.rs
index b424660..02fb044 100644
--- a/core/binary_protocol/src/consensus/error.rs
+++ b/core/binary_protocol/src/consensus/error.rs
@@ -15,13 +15,13 @@
// specific language governing permissions and limitations
// under the License.
-use super::command::Command2;
+use super::command::Command;
use thiserror::Error;
#[derive(Debug, Clone, Error, PartialEq, Eq)]
pub enum ConsensusError {
#[error("invalid command: expected {expected:?}, found {found:?}")]
- InvalidCommand { expected: Command2, found: Command2 },
+ InvalidCommand { expected: Command, found: Command },
#[error("invalid size: expected {expected:?}, found {found:?}")]
InvalidSize { expected: u32, found: u32 },
@@ -41,7 +41,7 @@
}
)]
FrameChecksumMismatch {
- command: Command2,
+ command: Command,
expected: u128,
found: u128,
},
@@ -62,13 +62,13 @@
PrepareRequestChecksumPaddingNonZero,
#[error("command must be Commit")]
- CommitInvalidCommand2,
+ CommitInvalidCommand,
#[error("size must be 256, found {0}")]
CommitInvalidSize(u32),
#[error("command must be Reply")]
- ReplyInvalidCommand2,
+ ReplyInvalidCommand,
#[error("request_checksum_padding must be 0")]
ReplyRequestChecksumPaddingNonZero,
@@ -80,5 +80,5 @@
InvalidBitPattern,
#[error("client-bound command {0:?} cannot be dispatched on inbound path")]
- ClientBoundCommand(Command2),
+ ClientBoundCommand(Command),
}
diff --git a/core/binary_protocol/src/consensus/header.rs b/core/binary_protocol/src/consensus/header.rs
index 9dd4d7d..2f205df 100644
--- a/core/binary_protocol/src/consensus/header.rs
+++ b/core/binary_protocol/src/consensus/header.rs
@@ -42,7 +42,7 @@
//! `checksum` as their view-independent identity, and the three client-facing
//! headers are sealed on neither side, so SDKs are untouched.
-use super::{Command2, ConsensusError, Operation};
+use super::{Command, ConsensusError, Operation};
use bytemuck::{CheckedBitPattern, NoUninit};
use std::mem::offset_of;
@@ -101,13 +101,13 @@
/// strict allocators (Miri, jemalloc, arenas). Use
/// `aligned_vec::AVec<u8, ConstAlign<16>>` for explicit alignment.
pub trait ConsensusHeader: Sized + CheckedBitPattern + NoUninit {
- const COMMAND: Command2;
+ const COMMAND: Command;
/// Whether a frame carrying `command` may be typed as this header.
/// Defaults to an exact match; a header that serves several commands
/// with one layout (e.g. `RepairDone` / `RangeEvicted`) widens it.
#[must_use]
- fn accepts(command: Command2) -> bool {
+ fn accepts(command: Command) -> bool {
command == Self::COMMAND
}
@@ -135,7 +135,7 @@
/// Returns `ConsensusError` if the header fields are inconsistent.
fn validate(&self) -> Result<(), ConsensusError>;
fn operation(&self) -> Operation;
- fn command(&self) -> Command2;
+ fn command(&self) -> Command;
fn size(&self) -> u32;
/// The `checksum` field, whatever this header spends it on.
@@ -205,7 +205,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
pub reserved_command: [u8; RESERVED_COMMAND_LEN],
@@ -227,7 +227,7 @@
};
impl ConsensusHeader for GenericHeader {
- const COMMAND: Command2 = Command2::Reserved;
+ const COMMAND: Command = Command::Reserved;
const FRAME_SEALED: bool = false;
fn checksum(&self) -> u128 {
@@ -240,7 +240,7 @@
fn operation(&self) -> Operation {
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn validate(&self) -> Result<(), ConsensusError> {
@@ -263,7 +263,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -333,7 +333,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -374,7 +374,7 @@
size: 0,
view: 0,
release: 0,
- command: Command2::Reserved,
+ command: Command::Reserved,
replica: 0,
reserved_frame: [0; 66],
client: 0,
@@ -400,7 +400,7 @@
size: 0,
view: 0,
release: 0,
- command: Command2::Reserved,
+ command: Command::Reserved,
replica: 0,
reserved_frame: [0; 66],
client: 0,
@@ -477,7 +477,7 @@
}
impl ConsensusHeader for RoutedRequestHeader {
- const COMMAND: Command2 = Command2::Request;
+ const COMMAND: Command = Command::Request;
/// The client-wire [`RequestHeader`] this is promoted from is unsealed, and the
/// promotion copies `checksum` verbatim, so there is nothing here to verify.
const FRAME_SEALED: bool = false;
@@ -492,7 +492,7 @@
fn operation(&self) -> Operation {
self.operation
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -500,9 +500,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::Request {
+ if self.command != Command::Request {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::Request,
+ expected: Command::Request,
found: self.command,
});
}
@@ -511,7 +511,7 @@
}
impl ConsensusHeader for RequestHeader {
- const COMMAND: Command2 = Command2::Request;
+ const COMMAND: Command = Command::Request;
const FRAME_SEALED: bool = false;
fn checksum(&self) -> u128 {
@@ -524,7 +524,7 @@
fn operation(&self) -> Operation {
self.operation
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -532,9 +532,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::Request {
+ if self.command != Command::Request {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::Request,
+ expected: Command::Request,
found: self.command,
});
}
@@ -554,7 +554,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -602,7 +602,7 @@
size: 0,
view: 0,
release: 0,
- command: Command2::Reserved,
+ command: Command::Reserved,
replica: 0,
reserved_frame: [0; 66],
request_checksum: 0,
@@ -621,7 +621,7 @@
}
impl ConsensusHeader for ReplyHeader {
- const COMMAND: Command2 = Command2::Reply;
+ const COMMAND: Command = Command::Reply;
const FRAME_SEALED: bool = false;
fn checksum(&self) -> u128 {
@@ -634,7 +634,7 @@
fn operation(&self) -> Operation {
self.operation
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -642,8 +642,8 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::Reply {
- return Err(ConsensusError::ReplyInvalidCommand2);
+ if self.command != Command::Reply {
+ return Err(ConsensusError::ReplyInvalidCommand);
}
Ok(())
}
@@ -713,7 +713,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -781,7 +781,7 @@
size: HEADER_SIZE as u32,
view,
release: 0,
- command: Command2::Eviction,
+ command: Command::Eviction,
replica,
reserved_frame: [0; 66],
client,
@@ -812,7 +812,7 @@
}
impl ConsensusHeader for EvictionHeader {
- const COMMAND: Command2 = Command2::Eviction;
+ const COMMAND: Command = Command::Eviction;
const FRAME_SEALED: bool = false;
fn checksum(&self) -> u128 {
@@ -826,7 +826,7 @@
fn operation(&self) -> Operation {
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -835,9 +835,9 @@
#[allow(clippy::cast_possible_truncation)]
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::Eviction {
+ if self.command != Command::Eviction {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::Eviction,
+ expected: Command::Eviction,
found: self.command,
});
}
@@ -905,7 +905,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -951,7 +951,7 @@
size: 0,
view: 0,
release: 0,
- command: Command2::Reserved,
+ command: Command::Reserved,
replica: 0,
reserved_frame: [0; 66],
client: 0,
@@ -971,7 +971,7 @@
}
impl ConsensusHeader for PrepareHeader {
- const COMMAND: Command2 = Command2::Prepare;
+ const COMMAND: Command = Command::Prepare;
const FRAME_SEALED: bool = false;
fn checksum(&self) -> u128 {
@@ -984,7 +984,7 @@
fn operation(&self) -> Operation {
self.operation
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -992,9 +992,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::Prepare {
+ if self.command != Command::Prepare {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::Prepare,
+ expected: Command::Prepare,
found: self.command,
});
}
@@ -1069,7 +1069,7 @@
pub struct RepairPrepareHeader(pub PrepareHeader);
impl ConsensusHeader for RepairPrepareHeader {
- const COMMAND: Command2 = Command2::RepairPrepare;
+ const COMMAND: Command = Command::RepairPrepare;
const FRAME_SEALED: bool = false;
fn checksum(&self) -> u128 {
@@ -1082,7 +1082,7 @@
fn operation(&self) -> Operation {
self.0.operation
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.0.command
}
fn size(&self) -> u32 {
@@ -1090,9 +1090,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.0.command != Command2::RepairPrepare {
+ if self.0.command != Command::RepairPrepare {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::RepairPrepare,
+ expected: Command::RepairPrepare,
found: self.0.command,
});
}
@@ -1127,7 +1127,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -1160,7 +1160,7 @@
size: 0,
view: 0,
release: 0,
- command: Command2::Reserved,
+ command: Command::Reserved,
replica: 0,
reserved_frame: [0; 66],
parent: 0,
@@ -1180,7 +1180,7 @@
impl ConsensusHeader for PrepareOkHeader {
const FRAME_SEALED: bool = true;
- const COMMAND: Command2 = Command2::PrepareOk;
+ const COMMAND: Command = Command::PrepareOk;
fn checksum(&self) -> u128 {
self.checksum
@@ -1192,7 +1192,7 @@
fn operation(&self) -> Operation {
self.operation
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -1200,9 +1200,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::PrepareOk {
+ if self.command != Command::PrepareOk {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::PrepareOk,
+ expected: Command::PrepareOk,
found: self.command,
});
}
@@ -1222,7 +1222,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -1245,7 +1245,7 @@
impl ConsensusHeader for CommitHeader {
const FRAME_SEALED: bool = true;
- const COMMAND: Command2 = Command2::Commit;
+ const COMMAND: Command = Command::Commit;
fn checksum(&self) -> u128 {
self.checksum
@@ -1257,7 +1257,7 @@
fn operation(&self) -> Operation {
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -1265,8 +1265,8 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::Commit {
- return Err(ConsensusError::CommitInvalidCommand2);
+ if self.command != Command::Commit {
+ return Err(ConsensusError::CommitInvalidCommand);
}
if self.size != 256 {
return Err(ConsensusError::CommitInvalidSize(self.size));
@@ -1287,7 +1287,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -1306,7 +1306,7 @@
impl ConsensusHeader for StartViewChangeHeader {
const FRAME_SEALED: bool = true;
- const COMMAND: Command2 = Command2::StartViewChange;
+ const COMMAND: Command = Command::StartViewChange;
fn checksum(&self) -> u128 {
self.checksum
@@ -1318,7 +1318,7 @@
fn operation(&self) -> Operation {
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -1326,9 +1326,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::StartViewChange {
+ if self.command != Command::StartViewChange {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::StartViewChange,
+ expected: Command::StartViewChange,
found: self.command,
});
}
@@ -1351,7 +1351,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -1414,7 +1414,7 @@
impl ConsensusHeader for DoViewChangeHeader {
const FRAME_SEALED: bool = true;
- const COMMAND: Command2 = Command2::DoViewChange;
+ const COMMAND: Command = Command::DoViewChange;
fn checksum(&self) -> u128 {
self.checksum
@@ -1426,7 +1426,7 @@
fn operation(&self) -> Operation {
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -1434,9 +1434,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::DoViewChange {
+ if self.command != Command::DoViewChange {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::DoViewChange,
+ expected: Command::DoViewChange,
found: self.command,
});
}
@@ -1525,7 +1525,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -1562,7 +1562,7 @@
impl ConsensusHeader for StartViewHeader {
const FRAME_SEALED: bool = true;
- const COMMAND: Command2 = Command2::StartView;
+ const COMMAND: Command = Command::StartView;
fn checksum(&self) -> u128 {
self.checksum
@@ -1574,7 +1574,7 @@
fn operation(&self) -> Operation {
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -1582,9 +1582,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::StartView {
+ if self.command != Command::StartView {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::StartView,
+ expected: Command::StartView,
found: self.command,
});
}
@@ -1637,7 +1637,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -1667,7 +1667,7 @@
impl ConsensusHeader for RequestStartViewHeader {
const FRAME_SEALED: bool = true;
- const COMMAND: Command2 = Command2::RequestStartView;
+ const COMMAND: Command = Command::RequestStartView;
fn checksum(&self) -> u128 {
self.checksum
@@ -1679,7 +1679,7 @@
fn operation(&self) -> Operation {
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -1687,9 +1687,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::RequestStartView {
+ if self.command != Command::RequestStartView {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::RequestStartView,
+ expected: Command::RequestStartView,
found: self.command,
});
}
@@ -1719,7 +1719,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -1741,7 +1741,7 @@
impl ConsensusHeader for RequestPreparesHeader {
const FRAME_SEALED: bool = true;
- const COMMAND: Command2 = Command2::RequestPrepares;
+ const COMMAND: Command = Command::RequestPrepares;
fn checksum(&self) -> u128 {
self.checksum
@@ -1753,7 +1753,7 @@
fn operation(&self) -> Operation {
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -1761,9 +1761,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::RequestPrepares {
+ if self.command != Command::RequestPrepares {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::RequestPrepares,
+ expected: Command::RequestPrepares,
found: self.command,
});
}
@@ -1792,7 +1792,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -1814,7 +1814,7 @@
impl ConsensusHeader for RepairRangeReplyHeader {
const FRAME_SEALED: bool = true;
- const COMMAND: Command2 = Command2::RepairDone;
+ const COMMAND: Command = Command::RepairDone;
fn checksum(&self) -> u128 {
self.checksum
@@ -1826,13 +1826,13 @@
// One layout, two commands: `RepairDone` terminates a stream,
// `RangeEvicted` prefixes it. Without this widening, `try_into_typed`
// rejects `RangeEvicted` frames before `validate` ever sees them.
- fn accepts(command: Command2) -> bool {
- command == Command2::RepairDone || command == Command2::RangeEvicted
+ fn accepts(command: Command) -> bool {
+ command == Command::RepairDone || command == Command::RangeEvicted
}
fn operation(&self) -> Operation {
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -1840,9 +1840,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::RepairDone && self.command != Command2::RangeEvicted {
+ if self.command != Command::RepairDone && self.command != Command::RangeEvicted {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::RepairDone,
+ expected: Command::RepairDone,
found: self.command,
});
}
@@ -1874,7 +1874,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -1896,7 +1896,7 @@
impl ConsensusHeader for RequestStateTransferHeader {
const FRAME_SEALED: bool = true;
- const COMMAND: Command2 = Command2::RequestStateTransfer;
+ const COMMAND: Command = Command::RequestStateTransfer;
fn checksum(&self) -> u128 {
self.checksum
@@ -1908,7 +1908,7 @@
fn operation(&self) -> Operation {
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -1917,9 +1917,9 @@
#[allow(clippy::cast_possible_truncation)]
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::RequestStateTransfer {
+ if self.command != Command::RequestStateTransfer {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::RequestStateTransfer,
+ expected: Command::RequestStateTransfer,
found: self.command,
});
}
@@ -1956,7 +1956,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -2020,7 +2020,7 @@
impl ConsensusHeader for StateTransferTargetHeader {
const FRAME_SEALED: bool = true;
- const COMMAND: Command2 = Command2::StateTransferTarget;
+ const COMMAND: Command = Command::StateTransferTarget;
fn checksum(&self) -> u128 {
self.checksum
@@ -2032,7 +2032,7 @@
fn operation(&self) -> Operation {
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -2040,9 +2040,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::StateTransferTarget {
+ if self.command != Command::StateTransferTarget {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::StateTransferTarget,
+ expected: Command::StateTransferTarget,
found: self.command,
});
}
@@ -2096,7 +2096,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -2121,7 +2121,7 @@
impl ConsensusHeader for RequestStateChunkHeader {
const FRAME_SEALED: bool = true;
- const COMMAND: Command2 = Command2::RequestStateChunk;
+ const COMMAND: Command = Command::RequestStateChunk;
fn checksum(&self) -> u128 {
self.checksum
@@ -2133,7 +2133,7 @@
fn operation(&self) -> Operation {
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -2142,9 +2142,9 @@
#[allow(clippy::cast_possible_truncation)]
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::RequestStateChunk {
+ if self.command != Command::RequestStateChunk {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::RequestStateChunk,
+ expected: Command::RequestStateChunk,
found: self.command,
});
}
@@ -2181,7 +2181,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -2205,7 +2205,7 @@
impl ConsensusHeader for StateChunkHeader {
const FRAME_SEALED: bool = true;
- const COMMAND: Command2 = Command2::StateChunk;
+ const COMMAND: Command = Command::StateChunk;
fn checksum(&self) -> u128 {
self.checksum
@@ -2217,7 +2217,7 @@
fn operation(&self) -> Operation {
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -2225,9 +2225,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::StateChunk {
+ if self.command != Command::StateChunk {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::StateChunk,
+ expected: Command::StateChunk,
found: self.command,
});
}
@@ -2257,7 +2257,7 @@
/// peer could inject a `Request` + `Register` directly. Clients cannot
/// reach this command, because every client frame is typed through
/// [`RequestHeader`], whose `validate` rejects any command but
-/// [`Command2::Request`].
+/// [`Command::Request`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, CheckedBitPattern, NoUninit)]
#[repr(C)]
pub struct ForwardRegisterHeader {
@@ -2267,7 +2267,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -2299,7 +2299,7 @@
// believed: a flipped bit there would commit a register under another user.
const FRAME_SEALED: bool = true;
- const COMMAND: Command2 = Command2::ForwardRegister;
+ const COMMAND: Command = Command::ForwardRegister;
fn checksum(&self) -> u128 {
self.checksum
@@ -2311,7 +2311,7 @@
fn operation(&self) -> Operation {
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -2319,9 +2319,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::ForwardRegister {
+ if self.command != Command::ForwardRegister {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::ForwardRegister,
+ expected: Command::ForwardRegister,
found: self.command,
});
}
@@ -2345,7 +2345,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -2409,7 +2409,7 @@
impl ConsensusHeader for ForwardRegisterResultHeader {
const FRAME_SEALED: bool = true;
- const COMMAND: Command2 = Command2::ForwardRegisterResult;
+ const COMMAND: Command = Command::ForwardRegisterResult;
fn checksum(&self) -> u128 {
self.checksum
@@ -2421,7 +2421,7 @@
fn operation(&self) -> Operation {
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
fn size(&self) -> u32 {
@@ -2429,9 +2429,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::ForwardRegisterResult {
+ if self.command != Command::ForwardRegisterResult {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::ForwardRegisterResult,
+ expected: Command::ForwardRegisterResult,
found: self.command,
});
}
@@ -2476,7 +2476,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -2504,7 +2504,7 @@
impl ConsensusHeader for ForwardLogoutHeader {
const FRAME_SEALED: bool = true;
- const COMMAND: Command2 = Command2::ForwardLogout;
+ const COMMAND: Command = Command::ForwardLogout;
fn checksum(&self) -> u128 {
self.checksum
@@ -2518,7 +2518,7 @@
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
@@ -2527,9 +2527,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::ForwardLogout {
+ if self.command != Command::ForwardLogout {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::ForwardLogout,
+ expected: Command::ForwardLogout,
found: self.command,
});
}
@@ -2556,7 +2556,7 @@
pub size: u32,
pub view: u32,
pub release: u32,
- pub command: Command2,
+ pub command: Command,
pub replica: u8,
pub reserved_frame: [u8; 66],
@@ -2597,7 +2597,7 @@
impl ConsensusHeader for ForwardLogoutResultHeader {
const FRAME_SEALED: bool = true;
- const COMMAND: Command2 = Command2::ForwardLogoutResult;
+ const COMMAND: Command = Command::ForwardLogoutResult;
fn checksum(&self) -> u128 {
self.checksum
@@ -2611,7 +2611,7 @@
Operation::Reserved
}
- fn command(&self) -> Command2 {
+ fn command(&self) -> Command {
self.command
}
@@ -2620,9 +2620,9 @@
}
fn validate(&self) -> Result<(), ConsensusError> {
- if self.command != Command2::ForwardLogoutResult {
+ if self.command != Command::ForwardLogoutResult {
return Err(ConsensusError::InvalidCommand {
- expected: Command2::ForwardLogoutResult,
+ expected: Command::ForwardLogoutResult,
found: self.command,
});
}
@@ -2694,14 +2694,14 @@
#[cfg(test)]
mod tests {
use super::{
- Command2, CommitHeader, ConsensusError, ConsensusHeader, DoViewChangeHeader,
- EvictionHeader, EvictionReason, ForwardLogoutHeader, ForwardLogoutOutcome,
- ForwardLogoutResultHeader, ForwardRegisterHeader, ForwardRegisterOutcome,
- ForwardRegisterResultHeader, GenericHeader, HEADER_SIZE, Operation, PrepareHeader,
- PrepareOkHeader, RepairPrepareHeader, RepairRangeReplyHeader, ReplyHeader, RequestHeader,
- RequestPreparesHeader, RequestStartViewHeader, RequestStateChunkHeader,
- RequestStateTransferHeader, RoutedRequestHeader, StartViewChangeHeader, StartViewHeader,
- StateChunkHeader, StateTransferTargetHeader,
+ Command, CommitHeader, ConsensusError, ConsensusHeader, DoViewChangeHeader, EvictionHeader,
+ EvictionReason, ForwardLogoutHeader, ForwardLogoutOutcome, ForwardLogoutResultHeader,
+ ForwardRegisterHeader, ForwardRegisterOutcome, ForwardRegisterResultHeader, GenericHeader,
+ HEADER_SIZE, Operation, PrepareHeader, PrepareOkHeader, RepairPrepareHeader,
+ RepairRangeReplyHeader, ReplyHeader, RequestHeader, RequestPreparesHeader,
+ RequestStartViewHeader, RequestStateChunkHeader, RequestStateTransferHeader,
+ RoutedRequestHeader, StartViewChangeHeader, StartViewHeader, StateChunkHeader,
+ StateTransferTargetHeader,
};
use aligned_vec::{AVec, ConstAlign};
@@ -2832,7 +2832,7 @@
}
let prepare = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
checksum: 0xdead_beef,
..Default::default()
};
@@ -2856,14 +2856,14 @@
fn generic_header_zero_copy() {
let buf = aligned_zeroed(256);
let header: &GenericHeader = bytemuck::checked::try_from_bytes(&buf).unwrap();
- assert_eq!(header.command, Command2::Reserved);
+ assert_eq!(header.command, Command::Reserved);
assert_eq!(header.size, 0);
}
#[test]
fn request_header_zero_copy() {
let mut buf = aligned_zeroed(256);
- buf[60] = Command2::Request as u8;
+ buf[60] = Command::Request as u8;
// client offset = 60 + 1 (replica) + 66 (reserved_frame) = 128.
// validate rejects client == 0.
buf[128] = 1;
@@ -2871,7 +2871,7 @@
// operation is `Reserved`, which validate rejects.
buf[std::mem::offset_of!(RequestHeader, operation)] = Operation::Register as u8;
let header: &RequestHeader = bytemuck::checked::try_from_bytes(&buf).unwrap();
- assert_eq!(header.command, Command2::Request);
+ assert_eq!(header.command, Command::Request);
assert!(header.validate().is_ok());
}
@@ -2888,7 +2888,7 @@
#[test]
fn request_reserved_operation_rejected() {
let header = RequestHeader {
- command: Command2::Request,
+ command: Command::Request,
client: 1,
operation: Operation::Reserved,
session: 1,
@@ -2901,7 +2901,7 @@
#[test]
fn request_register_nonzero_session_rejected() {
let header = RequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::Register,
session: 5,
request: 0,
@@ -2913,7 +2913,7 @@
#[test]
fn request_register_nonzero_request_rejected() {
let header = RequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::Register,
session: 0,
request: 1,
@@ -2925,7 +2925,7 @@
#[test]
fn request_non_register_valid() {
let header = RequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::SendMessages,
client: 0xCAFE,
session: 10,
@@ -2938,7 +2938,7 @@
#[test]
fn request_non_register_zero_session_rejected() {
let header = RequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::SendMessages,
session: 0,
request: 1,
@@ -2950,7 +2950,7 @@
#[test]
fn request_non_register_zero_request_rejected() {
let header = RequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::SendMessages,
session: 10,
request: 0,
@@ -2962,9 +2962,9 @@
#[test]
fn reply_header_zero_copy() {
let mut buf = aligned_zeroed(256);
- buf[60] = Command2::Reply as u8;
+ buf[60] = Command::Reply as u8;
let header: &ReplyHeader = bytemuck::checked::try_from_bytes(&buf).unwrap();
- assert_eq!(header.command, Command2::Reply);
+ assert_eq!(header.command, Command::Reply);
assert!(header.validate().is_ok());
}
@@ -3011,7 +3011,7 @@
#[test]
fn routed_request_zero_client_rejected() {
let header = RoutedRequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::SendMessages,
session: 10,
request: 1,
@@ -3023,7 +3023,7 @@
#[test]
fn routed_request_reserved_operation_rejected() {
let header = RoutedRequestHeader {
- command: Command2::Request,
+ command: Command::Request,
client: 0xCAFE,
session: 10,
request: 1,
@@ -3040,7 +3040,7 @@
#[test]
fn routed_request_valid_fields_accepted() {
let header = RoutedRequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::SendMessages,
client: 0xCAFE,
session: 10,
@@ -3056,7 +3056,7 @@
#[test]
fn reply_header_nonzero_status_still_validates() {
let header = ReplyHeader {
- command: Command2::Reply,
+ command: Command::Reply,
status: 41,
..ReplyHeader::default()
};
@@ -3154,7 +3154,7 @@
size: u32::try_from(HEADER_SIZE).expect("HEADER_SIZE fits u32"),
view: 3,
release: 0,
- command: Command2::ForwardRegister,
+ command: Command::ForwardRegister,
replica: 2,
reserved_frame: [0; 66],
client: 0xCAFE,
@@ -3175,7 +3175,7 @@
size: u32::try_from(HEADER_SIZE).expect("HEADER_SIZE fits u32"),
view: 3,
release: 0,
- command: Command2::ForwardRegisterResult,
+ command: Command::ForwardRegisterResult,
replica: 0,
reserved_frame: [0; 66],
nonce: 0xF00D,
@@ -3193,11 +3193,11 @@
fn forward_register_round_trips_through_generic_bytes() {
for (command, bytes) in [
(
- Command2::ForwardRegister,
+ Command::ForwardRegister,
bytemuck::bytes_of(&forward_register()).to_vec(),
),
(
- Command2::ForwardRegisterResult,
+ Command::ForwardRegisterResult,
bytemuck::bytes_of(&forward_register_result(ForwardRegisterOutcome::Ok)).to_vec(),
),
] {
@@ -3335,7 +3335,7 @@
size: u32::try_from(HEADER_SIZE).expect("HEADER_SIZE fits u32"),
view: 3,
release: 0,
- command: Command2::ForwardLogout,
+ command: Command::ForwardLogout,
replica: 2,
reserved_frame: [0; 66],
client: 0xCAFE,
@@ -3356,7 +3356,7 @@
size: u32::try_from(HEADER_SIZE).expect("HEADER_SIZE fits u32"),
view: 3,
release: 0,
- command: Command2::ForwardLogoutResult,
+ command: Command::ForwardLogoutResult,
replica: 0,
reserved_frame: [0; 66],
nonce: 0xF00D,
@@ -3393,7 +3393,7 @@
bytes.copy_from_slice(bytemuck::bytes_of(&result));
let generic = bytemuck::checked::try_from_bytes::<GenericHeader>(&bytes)
.expect("forward logout result is a valid generic header");
- assert_eq!(generic.command, Command2::ForwardLogoutResult);
+ assert_eq!(generic.command, Command::ForwardLogoutResult);
}
}
diff --git a/core/binary_protocol/src/consensus/mod.rs b/core/binary_protocol/src/consensus/mod.rs
index bac436e..d33f842 100644
--- a/core/binary_protocol/src/consensus/mod.rs
+++ b/core/binary_protocol/src/consensus/mod.rs
@@ -42,7 +42,7 @@
mod operation;
mod reply_result;
-pub use command::Command2;
+pub use command::Command;
pub use error::ConsensusError;
pub use header::{
CHECKSUM_UNSEALED, CommitHeader, ConsensusHeader, DVC_HEADERS_MAX, DoViewChangeHeader,
diff --git a/core/binary_protocol/src/consensus/operation.rs b/core/binary_protocol/src/consensus/operation.rs
index 026ff9b..691bd9f 100644
--- a/core/binary_protocol/src/consensus/operation.rs
+++ b/core/binary_protocol/src/consensus/operation.rs
@@ -95,8 +95,6 @@
SendMessages = 160,
StoreConsumerOffset = 161,
DeleteConsumerOffset = 162,
- StoreConsumerOffset2 = 164,
- DeleteConsumerOffset2 = 165,
}
impl Operation {
@@ -169,14 +167,7 @@
/// the SDK, the one place that sees Register replies.
#[must_use]
pub const fn is_result_framed(&self) -> bool {
- self.is_metadata()
- || matches!(
- self,
- Self::StoreConsumerOffset
- | Self::StoreConsumerOffset2
- | Self::DeleteConsumerOffset
- | Self::DeleteConsumerOffset2
- )
+ self.is_metadata() || matches!(self, Self::StoreConsumerOffset | Self::DeleteConsumerOffset)
}
/// Data-plane operations routed to the shard owning the partition.
@@ -247,9 +238,7 @@
| Self::LeaveConsumerGroup
| Self::SendMessages
| Self::StoreConsumerOffset
- | Self::DeleteConsumerOffset
- | Self::StoreConsumerOffset2
- | Self::DeleteConsumerOffset2 => match crate::dispatch::lookup_by_operation(*self) {
+ | Self::DeleteConsumerOffset => match crate::dispatch::lookup_by_operation(*self) {
Some(meta) => Some(meta.code),
None => None,
},
@@ -300,8 +289,6 @@
Operation::SendMessages,
Operation::StoreConsumerOffset,
Operation::DeleteConsumerOffset,
- Operation::StoreConsumerOffset2,
- Operation::DeleteConsumerOffset2,
];
for op in ops {
let code = op
@@ -367,8 +354,7 @@
assert!(Operation::TruncatePartition.is_internal());
assert!(Operation::TruncatePartition.is_metadata());
assert!(!Operation::TruncatePartition.is_client_allowed());
+ assert!(Operation::StoreConsumerOffset.is_partition());
assert!(Operation::DeleteConsumerOffset.is_partition());
- assert!(Operation::StoreConsumerOffset2.is_partition());
- assert!(Operation::DeleteConsumerOffset2.is_partition());
}
}
diff --git a/core/binary_protocol/src/dispatch.rs b/core/binary_protocol/src/dispatch.rs
index 576fadb..715fe71 100644
--- a/core/binary_protocol/src/dispatch.rs
+++ b/core/binary_protocol/src/dispatch.rs
@@ -126,16 +126,6 @@
"consumer_offset.delete",
Operation::DeleteConsumerOffset,
),
- CommandMeta::replicated(
- STORE_CONSUMER_OFFSET_2_CODE,
- "consumer_offset.store.v2",
- Operation::StoreConsumerOffset2,
- ),
- CommandMeta::replicated(
- DELETE_CONSUMER_OFFSET_2_CODE,
- "consumer_offset.delete.v2",
- Operation::DeleteConsumerOffset2,
- ),
// Streams
CommandMeta::non_replicated(GET_STREAM_CODE, "stream.get"),
CommandMeta::non_replicated(GET_STREAMS_CODE, "stream.list"),
@@ -233,32 +223,30 @@
GET_CONSUMER_OFFSET_CODE => 24,
STORE_CONSUMER_OFFSET_CODE => 25,
DELETE_CONSUMER_OFFSET_CODE => 26,
- STORE_CONSUMER_OFFSET_2_CODE => 27,
- DELETE_CONSUMER_OFFSET_2_CODE => 28,
- GET_STREAM_CODE => 29,
- GET_STREAMS_CODE => 30,
- CREATE_STREAM_CODE => 31,
- DELETE_STREAM_CODE => 32,
- UPDATE_STREAM_CODE => 33,
- PURGE_STREAM_CODE => 34,
- GET_TOPIC_CODE => 35,
- GET_TOPICS_CODE => 36,
- CREATE_TOPIC_CODE => 37,
- DELETE_TOPIC_CODE => 38,
- UPDATE_TOPIC_CODE => 39,
- PURGE_TOPIC_CODE => 40,
- CREATE_PARTITIONS_CODE => 41,
- DELETE_PARTITIONS_CODE => 42,
- DELETE_SEGMENTS_CODE => 43,
- GET_CONSUMER_GROUP_CODE => 44,
- GET_CONSUMER_GROUPS_CODE => 45,
- CREATE_CONSUMER_GROUP_CODE => 46,
- DELETE_CONSUMER_GROUP_CODE => 47,
- JOIN_CONSUMER_GROUP_CODE => 48,
- LEAVE_CONSUMER_GROUP_CODE => 49,
- SYNC_CONSUMER_GROUP_CODE => 50,
- LOGIN_REGISTER_WITH_PAT_CODE => 51,
- DESCRIBE_OPTIONS_CODE => 52,
+ GET_STREAM_CODE => 27,
+ GET_STREAMS_CODE => 28,
+ CREATE_STREAM_CODE => 29,
+ DELETE_STREAM_CODE => 30,
+ UPDATE_STREAM_CODE => 31,
+ PURGE_STREAM_CODE => 32,
+ GET_TOPIC_CODE => 33,
+ GET_TOPICS_CODE => 34,
+ CREATE_TOPIC_CODE => 35,
+ DELETE_TOPIC_CODE => 36,
+ UPDATE_TOPIC_CODE => 37,
+ PURGE_TOPIC_CODE => 38,
+ CREATE_PARTITIONS_CODE => 39,
+ DELETE_PARTITIONS_CODE => 40,
+ DELETE_SEGMENTS_CODE => 41,
+ GET_CONSUMER_GROUP_CODE => 42,
+ GET_CONSUMER_GROUPS_CODE => 43,
+ CREATE_CONSUMER_GROUP_CODE => 44,
+ DELETE_CONSUMER_GROUP_CODE => 45,
+ JOIN_CONSUMER_GROUP_CODE => 46,
+ LEAVE_CONSUMER_GROUP_CODE => 47,
+ SYNC_CONSUMER_GROUP_CODE => 48,
+ LOGIN_REGISTER_WITH_PAT_CODE => 49,
+ DESCRIBE_OPTIONS_CODE => 50,
_ => return None,
};
Some(&COMMAND_TABLE[idx])
@@ -272,21 +260,21 @@
pub const fn lookup_by_operation(op: Operation) -> Option<&'static CommandMeta> {
// Indices must match the order of entries in COMMAND_TABLE above.
let idx = match op {
- Operation::CreateStream => 31,
- Operation::UpdateStream => 33,
- Operation::DeleteStream => 32,
- Operation::PurgeStream => 34,
- Operation::CreateTopic => 37,
- Operation::UpdateTopic => 39,
- Operation::DeleteTopic => 38,
- Operation::PurgeTopic => 40,
- Operation::CreatePartitions => 41,
- Operation::DeletePartitions => 42,
- Operation::DeleteSegments => 43,
- Operation::CreateConsumerGroup => 46,
- Operation::DeleteConsumerGroup => 47,
- Operation::JoinConsumerGroup => 48,
- Operation::LeaveConsumerGroup => 49,
+ Operation::CreateStream => 29,
+ Operation::UpdateStream => 31,
+ Operation::DeleteStream => 30,
+ Operation::PurgeStream => 32,
+ Operation::CreateTopic => 35,
+ Operation::UpdateTopic => 37,
+ Operation::DeleteTopic => 36,
+ Operation::PurgeTopic => 38,
+ Operation::CreatePartitions => 39,
+ Operation::DeletePartitions => 40,
+ Operation::DeleteSegments => 41,
+ Operation::CreateConsumerGroup => 44,
+ Operation::DeleteConsumerGroup => 45,
+ Operation::JoinConsumerGroup => 46,
+ Operation::LeaveConsumerGroup => 47,
Operation::CreateUser => 9,
Operation::UpdateUser => 11,
Operation::DeleteUser => 10,
@@ -297,8 +285,6 @@
Operation::SendMessages => 22,
Operation::StoreConsumerOffset => 25,
Operation::DeleteConsumerOffset => 26,
- Operation::StoreConsumerOffset2 => 27,
- Operation::DeleteConsumerOffset2 => 28,
Operation::CreateTopicWithAssignments
| Operation::CreatePartitionsWithAssignments
| Operation::RemoveConsumerGroupMember
@@ -347,8 +333,6 @@
GET_CONSUMER_OFFSET_CODE,
STORE_CONSUMER_OFFSET_CODE,
DELETE_CONSUMER_OFFSET_CODE,
- STORE_CONSUMER_OFFSET_2_CODE,
- DELETE_CONSUMER_OFFSET_2_CODE,
GET_STREAM_CODE,
GET_STREAMS_CODE,
CREATE_STREAM_CODE,
@@ -433,8 +417,6 @@
Operation::SendMessages,
Operation::StoreConsumerOffset,
Operation::DeleteConsumerOffset,
- Operation::StoreConsumerOffset2,
- Operation::DeleteConsumerOffset2,
];
for op in replicated_ops {
let meta = lookup_by_operation(op)
diff --git a/core/binary_protocol/src/error.rs b/core/binary_protocol/src/error.rs
index 82d5cb2..208b15c 100644
--- a/core/binary_protocol/src/error.rs
+++ b/core/binary_protocol/src/error.rs
@@ -49,4 +49,23 @@
#[error("validation failed: {0}")]
Validation(Cow<'static, str>),
+
+ #[error(
+ "invalid batch checksum: stored {stored}, computed {computed}, base offset {base_offset}"
+ )]
+ InvalidBatchChecksum {
+ stored: u64,
+ computed: u64,
+ base_offset: u64,
+ },
+
+ #[error("invalid message checksum: stored {stored}, computed {computed}, offset {offset}")]
+ InvalidMessageChecksum {
+ stored: u64,
+ computed: u64,
+ offset: u64,
+ },
+
+ #[error("message timestamp delta {0} exceeds the batch maximum")]
+ InvalidMessageTimestampDelta(u64),
}
diff --git a/core/binary_protocol/src/framing.rs b/core/binary_protocol/src/framing.rs
index 9603834..dc1f5ed 100644
--- a/core/binary_protocol/src/framing.rs
+++ b/core/binary_protocol/src/framing.rs
@@ -20,10 +20,6 @@
//! Encodes and decodes complete request/response frames without any I/O.
//! The transport layer (TCP, QUIC, WebSocket) reads bytes into a buffer,
//! then hands the buffer to these types for zero-copy parsing.
-//!
-//! When VSR consensus replaces this framing, the transport layer will
-//! switch to `consensus::header::GenericHeader` (256-byte fixed header)
-//! while the command payload codec stays the same.
use crate::codec::{read_bytes, read_u32_le, read_u64_le};
use crate::error::WireError;
@@ -34,168 +30,19 @@
/// Status code for a successful response.
pub const STATUS_OK: u32 = 0;
-/// Decoded request frame. Borrows the payload from the input buffer.
-///
-/// Wire format: `[length:4 LE][code:4 LE][payload:N]`
-/// where `length` = 4 (code size) + N (payload size).
-#[derive(Debug)]
-pub struct RequestFrame<'a> {
- pub code: u32,
- pub payload: &'a [u8],
-}
-
-impl<'a> RequestFrame<'a> {
- /// Size of the frame header: `[length:4][code:4]`.
- pub const HEADER_SIZE: usize = 8;
-
- /// Validate a frame length field and return the payload size.
- ///
- /// Transport layers that read the length and code fields incrementally
- /// (e.g. compio completion-based I/O) can use this to validate the length
- /// before reading the payload, without buffering the entire frame.
- ///
- /// # Errors
- /// Returns `WireError::Validation` if `frame_length < 4` (too small to
- /// contain even the command code).
- pub fn payload_length(frame_length: u32) -> Result<u32, WireError> {
- frame_length
- .checked_sub(4)
- .ok_or(WireError::Validation(Cow::Borrowed(
- "request frame length must be at least 4 (code size)",
- )))
- }
-
- /// Construct a frame from pre-parsed header fields and a payload slice.
- ///
- /// Used by transport layers that read the header incrementally (e.g.
- /// compio completion-based I/O) and then read the payload separately.
- #[must_use]
- pub const fn from_parts(code: u32, payload: &'a [u8]) -> Self {
- Self { code, payload }
- }
-
- /// Decode a request frame from a complete buffer.
- ///
- /// # Errors
- /// Returns `WireError::UnexpectedEof` if the buffer is too short.
- pub fn decode(buf: &'a [u8]) -> Result<(Self, usize), WireError> {
- let frame_length = read_u32_le(buf, 0)?;
- let payload_len = Self::payload_length(frame_length)? as usize;
- let code = read_u32_le(buf, 4)?;
- let payload = read_bytes(buf, Self::HEADER_SIZE, payload_len)?;
- let total = Self::HEADER_SIZE + payload_len;
- Ok((Self { code, payload }, total))
- }
-
- /// Encode a request frame into `out`.
- ///
- /// Writes `[length:4 LE][code:4 LE][payload]` where length includes
- /// the 4-byte code field.
- ///
- /// # Errors
- /// Returns `WireError::PayloadTooLarge` if payload exceeds u32 capacity.
- pub fn encode(code: u32, payload: &[u8], out: &mut BytesMut) -> Result<(), WireError> {
- let length = payload
- .len()
- .checked_add(4)
- .and_then(|n| u32::try_from(n).ok())
- .ok_or(WireError::PayloadTooLarge {
- size: payload.len(),
- max: u32::MAX as usize - 4,
- })?;
- out.reserve(Self::HEADER_SIZE + payload.len());
- out.put_u32_le(length);
- out.put_u32_le(code);
- out.put_slice(payload);
- Ok(())
- }
-
- /// Total encoded size for a given payload length.
- ///
- /// Returns `None` if `HEADER_SIZE + payload_len` overflows `usize`.
- #[must_use]
- pub const fn encoded_size(payload_len: usize) -> Option<usize> {
- Self::HEADER_SIZE.checked_add(payload_len)
- }
-}
-
-/// Decoded response frame. Borrows the payload from the input buffer.
-///
-/// Wire format: `[status:4 LE][length:4 LE][payload:N]`
-/// where `status` = 0 for success, non-zero for error code.
-#[derive(Debug)]
-pub struct ResponseFrame<'a> {
- pub status: u32,
- pub payload: &'a [u8],
-}
-
-impl<'a> ResponseFrame<'a> {
- /// Size of the frame header: `[status:4][length:4]`.
- pub const HEADER_SIZE: usize = 8;
-
- /// Decode a response frame from a complete buffer.
- ///
- /// # Errors
- /// Returns `WireError::UnexpectedEof` if the buffer is too short.
- pub fn decode(buf: &'a [u8]) -> Result<(Self, usize), WireError> {
- let status = read_u32_le(buf, 0)?;
- let length = read_u32_le(buf, 4)? as usize;
- let payload = read_bytes(buf, Self::HEADER_SIZE, length)?;
- let total = Self::HEADER_SIZE + length;
- Ok((Self { status, payload }, total))
- }
-
- /// Encode a successful response with payload.
- ///
- /// # Errors
- /// Returns `WireError::PayloadTooLarge` if payload exceeds u32 capacity.
- pub fn encode_ok(payload: &[u8], out: &mut BytesMut) -> Result<(), WireError> {
- let length = u32::try_from(payload.len()).map_err(|_| WireError::PayloadTooLarge {
- size: payload.len(),
- max: u32::MAX as usize,
- })?;
- out.reserve(Self::HEADER_SIZE + payload.len());
- out.put_u32_le(STATUS_OK);
- out.put_u32_le(length);
- out.put_slice(payload);
- Ok(())
- }
-
- /// Encode an error response (status code, empty payload).
- pub fn encode_error(status: NonZeroU32, out: &mut BytesMut) {
- out.reserve(Self::HEADER_SIZE);
- out.put_u32_le(status.get());
- out.put_u32_le(0);
- }
-
- /// Returns `true` if this is a success response.
- #[must_use]
- pub const fn is_ok(&self) -> bool {
- self.status == STATUS_OK
- }
-
- /// Total encoded size for a given payload length.
- ///
- /// Returns `None` if `HEADER_SIZE + payload_len` overflows `usize`.
- #[must_use]
- pub const fn encoded_size(payload_len: usize) -> Option<usize> {
- Self::HEADER_SIZE.checked_add(payload_len)
- }
-}
-
/// Decoded request frame with request ID for request-response correlation
/// and consensus-level duplicate detection (the server framing).
///
/// Wire format: `[length:4 LE][code:4 LE][request_id:8 LE][payload:N]`
/// where `length` = 4 (code) + 8 (`request_id`) + N (payload).
#[derive(Debug)]
-pub struct RequestFrame2<'a> {
+pub struct RequestFrame<'a> {
pub code: u32,
pub request_id: u64,
pub payload: &'a [u8],
}
-impl<'a> RequestFrame2<'a> {
+impl<'a> RequestFrame<'a> {
/// Size of the frame header: `[length:4][code:4][request_id:8]`.
pub const HEADER_SIZE: usize = 16;
@@ -287,13 +134,13 @@
/// Wire format: `[status:4 LE][length:4 LE][request_id:8 LE][payload:N]`
/// where `status` = 0 for success, non-zero for error code.
#[derive(Debug)]
-pub struct ResponseFrame2<'a> {
+pub struct ResponseFrame<'a> {
pub status: u32,
pub request_id: u64,
pub payload: &'a [u8],
}
-impl<'a> ResponseFrame2<'a> {
+impl<'a> ResponseFrame<'a> {
/// Size of the frame header: `[status:4][length:4][request_id:8]`.
pub const HEADER_SIZE: usize = 16;
@@ -376,152 +223,12 @@
use super::*;
#[test]
- fn request_roundtrip() {
- let payload = b"hello world";
- let mut buf = BytesMut::with_capacity(RequestFrame::encoded_size(payload.len()).unwrap());
- RequestFrame::encode(42, payload, &mut buf).unwrap();
-
- let (frame, consumed) = RequestFrame::decode(&buf).unwrap();
- assert_eq!(consumed, buf.len());
- assert_eq!(frame.code, 42);
- assert_eq!(frame.payload, payload);
- }
-
- #[test]
- fn request_empty_payload() {
- let mut buf = BytesMut::with_capacity(RequestFrame::HEADER_SIZE);
- RequestFrame::encode(1, &[], &mut buf).unwrap();
-
- let (frame, consumed) = RequestFrame::decode(&buf).unwrap();
- assert_eq!(consumed, 8);
- assert_eq!(frame.code, 1);
- assert!(frame.payload.is_empty());
- }
-
- #[test]
- fn request_length_field_includes_code() {
- let payload = b"test";
- let mut buf = BytesMut::new();
- RequestFrame::encode(99, payload, &mut buf).unwrap();
-
- let length = u32::from_le_bytes(buf[0..4].try_into().unwrap());
- assert_eq!(length, 4 + 4); // code(4) + payload(4)
- }
-
- #[test]
- fn request_truncated_header() {
- let buf = [0u8; 7]; // less than HEADER_SIZE
- assert!(RequestFrame::decode(&buf).is_err());
- }
-
- #[test]
- fn request_truncated_payload() {
- let mut buf = BytesMut::new();
- buf.put_u32_le(104); // length = 104 (code + 100 bytes payload)
- buf.put_u32_le(1); // code
- buf.put_slice(&[0u8; 50]); // only 50 of 100 bytes
- assert!(RequestFrame::decode(&buf).is_err());
- }
-
- #[test]
- fn request_length_too_small() {
- let mut buf = BytesMut::new();
- buf.put_u32_le(3); // length < 4 (must include code)
- buf.put_u32_le(1);
- assert!(RequestFrame::decode(&buf).is_err());
- }
-
- #[test]
- fn payload_length_valid() {
- assert_eq!(RequestFrame::payload_length(4).unwrap(), 0);
- assert_eq!(RequestFrame::payload_length(104).unwrap(), 100);
- assert_eq!(
- RequestFrame::payload_length(u32::MAX).unwrap(),
- u32::MAX - 4
- );
- }
-
- #[test]
- fn payload_length_too_small() {
- assert!(RequestFrame::payload_length(0).is_err());
- assert!(RequestFrame::payload_length(1).is_err());
- assert!(RequestFrame::payload_length(3).is_err());
- }
-
- #[test]
- fn request_encoded_size() {
- assert_eq!(RequestFrame::encoded_size(0), Some(8));
- assert_eq!(RequestFrame::encoded_size(100), Some(108));
- assert_eq!(RequestFrame::encoded_size(usize::MAX), None);
- }
-
- #[test]
- fn response_ok_roundtrip() {
- let payload = b"response data";
- let mut buf = BytesMut::with_capacity(ResponseFrame::encoded_size(payload.len()).unwrap());
- ResponseFrame::encode_ok(payload, &mut buf).unwrap();
-
- let (frame, consumed) = ResponseFrame::decode(&buf).unwrap();
- assert_eq!(consumed, buf.len());
- assert!(frame.is_ok());
- assert_eq!(frame.status, 0);
- assert_eq!(frame.payload, payload);
- }
-
- #[test]
- fn response_ok_empty_payload() {
- let mut buf = BytesMut::new();
- ResponseFrame::encode_ok(&[], &mut buf).unwrap();
-
- let (frame, consumed) = ResponseFrame::decode(&buf).unwrap();
- assert_eq!(consumed, 8);
- assert!(frame.is_ok());
- assert!(frame.payload.is_empty());
- }
-
- #[test]
- fn response_error_roundtrip() {
- let mut buf = BytesMut::new();
- ResponseFrame::encode_error(NonZeroU32::new(1001).unwrap(), &mut buf);
-
- let (frame, consumed) = ResponseFrame::decode(&buf).unwrap();
- assert_eq!(consumed, 8);
- assert!(!frame.is_ok());
- assert_eq!(frame.status, 1001);
- assert!(frame.payload.is_empty());
- }
-
- #[test]
- fn response_truncated_header() {
- let buf = [0u8; 7];
- assert!(ResponseFrame::decode(&buf).is_err());
- }
-
- #[test]
- fn response_truncated_payload() {
- let mut buf = BytesMut::new();
- buf.put_u32_le(0); // status OK
- buf.put_u32_le(100); // length = 100
- buf.put_slice(&[0u8; 50]); // only 50 bytes
- assert!(ResponseFrame::decode(&buf).is_err());
- }
-
- #[test]
- fn response_encoded_size() {
- assert_eq!(ResponseFrame::encoded_size(0), Some(8));
- assert_eq!(ResponseFrame::encoded_size(256), Some(264));
- assert_eq!(ResponseFrame::encoded_size(usize::MAX), None);
- }
-
- // RequestFrame2 tests
-
- #[test]
fn request2_roundtrip() {
let payload = b"hello world";
- let mut buf = BytesMut::with_capacity(RequestFrame2::encoded_size(payload.len()).unwrap());
- RequestFrame2::encode(42, 7, payload, &mut buf).unwrap();
+ let mut buf = BytesMut::with_capacity(RequestFrame::encoded_size(payload.len()).unwrap());
+ RequestFrame::encode(42, 7, payload, &mut buf).unwrap();
- let (frame, consumed) = RequestFrame2::decode(&buf).unwrap();
+ let (frame, consumed) = RequestFrame::decode(&buf).unwrap();
assert_eq!(consumed, buf.len());
assert_eq!(frame.code, 42);
assert_eq!(frame.request_id, 7);
@@ -530,10 +237,10 @@
#[test]
fn request2_empty_payload() {
- let mut buf = BytesMut::with_capacity(RequestFrame2::HEADER_SIZE);
- RequestFrame2::encode(1, 99, &[], &mut buf).unwrap();
+ let mut buf = BytesMut::with_capacity(RequestFrame::HEADER_SIZE);
+ RequestFrame::encode(1, 99, &[], &mut buf).unwrap();
- let (frame, consumed) = RequestFrame2::decode(&buf).unwrap();
+ let (frame, consumed) = RequestFrame::decode(&buf).unwrap();
assert_eq!(consumed, 16);
assert_eq!(frame.code, 1);
assert_eq!(frame.request_id, 99);
@@ -544,7 +251,7 @@
fn request2_length_field_includes_code_and_request_id() {
let payload = b"test";
let mut buf = BytesMut::new();
- RequestFrame2::encode(99, 1, payload, &mut buf).unwrap();
+ RequestFrame::encode(99, 1, payload, &mut buf).unwrap();
let length = u32::from_le_bytes(buf[0..4].try_into().unwrap());
assert_eq!(length, 4 + 8 + 4); // code(4) + request_id(8) + payload(4)
@@ -553,7 +260,7 @@
#[test]
fn request2_truncated_header() {
let buf = [0u8; 15]; // less than HEADER_SIZE (16)
- assert!(RequestFrame2::decode(&buf).is_err());
+ assert!(RequestFrame::decode(&buf).is_err());
}
#[test]
@@ -563,7 +270,7 @@
buf.put_u32_le(1); // code
buf.put_u64_le(1); // request_id
buf.put_slice(&[0u8; 50]); // only 50 of 100 bytes
- assert!(RequestFrame2::decode(&buf).is_err());
+ assert!(RequestFrame::decode(&buf).is_err());
}
#[test]
@@ -572,46 +279,46 @@
buf.put_u32_le(11); // length < 12 (must include code + request_id)
buf.put_u32_le(1);
buf.put_u64_le(1);
- assert!(RequestFrame2::decode(&buf).is_err());
+ assert!(RequestFrame::decode(&buf).is_err());
}
#[test]
fn request2_payload_length_valid() {
- assert_eq!(RequestFrame2::payload_length(12).unwrap(), 0);
- assert_eq!(RequestFrame2::payload_length(112).unwrap(), 100);
+ assert_eq!(RequestFrame::payload_length(12).unwrap(), 0);
+ assert_eq!(RequestFrame::payload_length(112).unwrap(), 100);
}
#[test]
fn request2_payload_length_too_small() {
- assert!(RequestFrame2::payload_length(0).is_err());
- assert!(RequestFrame2::payload_length(11).is_err());
+ assert!(RequestFrame::payload_length(0).is_err());
+ assert!(RequestFrame::payload_length(11).is_err());
}
#[test]
fn request2_encoded_size() {
- assert_eq!(RequestFrame2::encoded_size(0), Some(16));
- assert_eq!(RequestFrame2::encoded_size(100), Some(116));
- assert_eq!(RequestFrame2::encoded_size(usize::MAX), None);
+ assert_eq!(RequestFrame::encoded_size(0), Some(16));
+ assert_eq!(RequestFrame::encoded_size(100), Some(116));
+ assert_eq!(RequestFrame::encoded_size(usize::MAX), None);
}
#[test]
fn request2_from_parts() {
let payload = b"data";
- let frame = RequestFrame2::from_parts(5, 42, payload);
+ let frame = RequestFrame::from_parts(5, 42, payload);
assert_eq!(frame.code, 5);
assert_eq!(frame.request_id, 42);
assert_eq!(frame.payload, payload);
}
- // ResponseFrame2 tests
+ // ResponseFrame tests
#[test]
fn response2_ok_roundtrip() {
let payload = b"response data";
- let mut buf = BytesMut::with_capacity(ResponseFrame2::encoded_size(payload.len()).unwrap());
- ResponseFrame2::encode_ok(7, payload, &mut buf).unwrap();
+ let mut buf = BytesMut::with_capacity(ResponseFrame::encoded_size(payload.len()).unwrap());
+ ResponseFrame::encode_ok(7, payload, &mut buf).unwrap();
- let (frame, consumed) = ResponseFrame2::decode(&buf).unwrap();
+ let (frame, consumed) = ResponseFrame::decode(&buf).unwrap();
assert_eq!(consumed, buf.len());
assert!(frame.is_ok());
assert_eq!(frame.request_id, 7);
@@ -621,9 +328,9 @@
#[test]
fn response2_ok_empty_payload() {
let mut buf = BytesMut::new();
- ResponseFrame2::encode_ok(42, &[], &mut buf).unwrap();
+ ResponseFrame::encode_ok(42, &[], &mut buf).unwrap();
- let (frame, consumed) = ResponseFrame2::decode(&buf).unwrap();
+ let (frame, consumed) = ResponseFrame::decode(&buf).unwrap();
assert_eq!(consumed, 16);
assert!(frame.is_ok());
assert_eq!(frame.request_id, 42);
@@ -633,9 +340,9 @@
#[test]
fn response2_error_roundtrip() {
let mut buf = BytesMut::new();
- ResponseFrame2::encode_error(NonZeroU32::new(1001).unwrap(), 55, &mut buf);
+ ResponseFrame::encode_error(NonZeroU32::new(1001).unwrap(), 55, &mut buf);
- let (frame, consumed) = ResponseFrame2::decode(&buf).unwrap();
+ let (frame, consumed) = ResponseFrame::decode(&buf).unwrap();
assert_eq!(consumed, 16);
assert!(!frame.is_ok());
assert_eq!(frame.status, 1001);
@@ -646,7 +353,7 @@
#[test]
fn response2_truncated_header() {
let buf = [0u8; 15];
- assert!(ResponseFrame2::decode(&buf).is_err());
+ assert!(ResponseFrame::decode(&buf).is_err());
}
#[test]
@@ -655,13 +362,13 @@
buf.put_u32_le(0); // status
buf.put_u32_le(7); // length < 8 (must include request_id)
buf.put_u64_le(1); // request_id
- assert!(ResponseFrame2::decode(&buf).is_err());
+ assert!(ResponseFrame::decode(&buf).is_err());
}
#[test]
fn response2_encoded_size() {
- assert_eq!(ResponseFrame2::encoded_size(0), Some(16));
- assert_eq!(ResponseFrame2::encoded_size(256), Some(272));
- assert_eq!(ResponseFrame2::encoded_size(usize::MAX), None);
+ assert_eq!(ResponseFrame::encoded_size(0), Some(16));
+ assert_eq!(ResponseFrame::encoded_size(256), Some(272));
+ assert_eq!(ResponseFrame::encoded_size(usize::MAX), None);
}
}
diff --git a/core/binary_protocol/src/lib.rs b/core/binary_protocol/src/lib.rs
index b308e5f..7ec2ddd 100644
--- a/core/binary_protocol/src/lib.rs
+++ b/core/binary_protocol/src/lib.rs
@@ -55,14 +55,13 @@
//! [`StartViewHeader`]
//! - Dispatch: [`GenericHeader`] for type-erased initial parsing
+pub mod batch;
pub mod codec;
pub mod codes;
pub mod consensus;
pub mod dispatch;
pub mod error;
pub mod framing;
-pub mod message_layout;
-pub mod message_view;
pub mod namespace;
pub mod primitives;
pub mod requests;
@@ -71,7 +70,7 @@
pub use codec::{WireDecode, WireEncode};
pub use consensus::{
- CHECKSUM_UNSEALED, Command2, CommitHeader, ConsensusError, ConsensusHeader, DVC_HEADERS_MAX,
+ CHECKSUM_UNSEALED, Command, CommitHeader, ConsensusError, ConsensusHeader, DVC_HEADERS_MAX,
DoViewChangeHeader, EvictionHeader, EvictionReason, ForwardLogoutHeader, ForwardLogoutOutcome,
ForwardLogoutResultHeader, ForwardRegisterHeader, ForwardRegisterOutcome,
ForwardRegisterResultHeader, GenericHeader, HEADER_SIZE, Operation, PrepareHeader,
@@ -83,10 +82,7 @@
};
pub use dispatch::{COMMAND_TABLE, CommandMeta, lookup_by_operation, lookup_command};
pub use error::WireError;
-pub use framing::{RequestFrame, RequestFrame2, ResponseFrame, ResponseFrame2, STATUS_OK};
-pub use message_view::{
- WireMessageIterator, WireMessageIteratorMut, WireMessageView, WireMessageViewMut,
-};
+pub use framing::{RequestFrame, ResponseFrame, STATUS_OK};
pub use primitives::ack_level::AckLevel;
pub use primitives::consumer::{KIND_CONSUMER_GROUP, WireConsumer};
pub use primitives::identifier::{MAX_WIRE_NAME_LENGTH, WireIdentifier, WireName};
diff --git a/core/binary_protocol/src/message_layout.rs b/core/binary_protocol/src/message_layout.rs
deleted file mode 100644
index 2858ec5..0000000
--- a/core/binary_protocol/src/message_layout.rs
+++ /dev/null
@@ -1,56 +0,0 @@
-// 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.
-
-//! 64-byte message frame layout constants.
-//!
-//! This module is the single source of truth for the on-wire message
-//! header layout shared between encoders, decoders, and zero-copy views.
-//!
-//! ```text
-//! [checksum:8][id:16][offset:8][timestamp:8][origin_timestamp:8]
-//! [user_headers_length:4][payload_length:4][reserved:8]
-//! ```
-
-/// Fixed-size message header on the wire (64 bytes).
-pub const WIRE_MESSAGE_HEADER_SIZE: usize = 64;
-
-/// Fixed-size index entry per message (16 bytes).
-///
-/// Layout: `[0x00000000:4][cumulative_size:u32_le:4][0x0000000000000000:8]`
-pub const WIRE_MESSAGE_INDEX_SIZE: usize = 16;
-
-// Field offsets within the 64-byte message header.
-pub const MSG_CHECKSUM_OFFSET: usize = 0;
-pub const MSG_ID_OFFSET: usize = 8;
-pub const MSG_OFFSET_OFFSET: usize = 24;
-pub const MSG_TIMESTAMP_OFFSET: usize = 32;
-pub const MSG_ORIGIN_TIMESTAMP_OFFSET: usize = 40;
-pub const MSG_USER_HEADERS_LEN_OFFSET: usize = 48;
-pub const MSG_PAYLOAD_LEN_OFFSET: usize = 52;
-pub const MSG_RESERVED_OFFSET: usize = 56;
-
-// Compile-time verification that field offsets are contiguous and sum to the header size.
-const _: () = {
- assert!(MSG_ID_OFFSET == MSG_CHECKSUM_OFFSET + 8);
- assert!(MSG_OFFSET_OFFSET == MSG_ID_OFFSET + 16);
- assert!(MSG_TIMESTAMP_OFFSET == MSG_OFFSET_OFFSET + 8);
- assert!(MSG_ORIGIN_TIMESTAMP_OFFSET == MSG_TIMESTAMP_OFFSET + 8);
- assert!(MSG_USER_HEADERS_LEN_OFFSET == MSG_ORIGIN_TIMESTAMP_OFFSET + 8);
- assert!(MSG_PAYLOAD_LEN_OFFSET == MSG_USER_HEADERS_LEN_OFFSET + 4);
- assert!(MSG_RESERVED_OFFSET == MSG_PAYLOAD_LEN_OFFSET + 4);
- assert!(MSG_RESERVED_OFFSET + 8 == WIRE_MESSAGE_HEADER_SIZE);
-};
diff --git a/core/binary_protocol/src/message_view.rs b/core/binary_protocol/src/message_view.rs
deleted file mode 100644
index 3159dac..0000000
--- a/core/binary_protocol/src/message_view.rs
+++ /dev/null
@@ -1,617 +0,0 @@
-// 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.
-
-//! Zero-copy view types for message frames on the wire.
-//!
-//! These types borrow an existing buffer and provide typed access to
-//! message header fields and payload data without copying.
-
-use crate::error::WireError;
-use crate::message_layout::{
- MSG_CHECKSUM_OFFSET, MSG_ID_OFFSET, MSG_OFFSET_OFFSET, MSG_ORIGIN_TIMESTAMP_OFFSET,
- MSG_PAYLOAD_LEN_OFFSET, MSG_TIMESTAMP_OFFSET, MSG_USER_HEADERS_LEN_OFFSET,
- WIRE_MESSAGE_HEADER_SIZE,
-};
-use std::borrow::Cow;
-
-// Private helpers for infallible reads on validated buffers
-
-#[inline]
-fn u32_at(buf: &[u8], offset: usize) -> u32 {
- u32::from_le_bytes(
- buf[offset..offset + 4]
- .try_into()
- .expect("slice is exactly 4 bytes"),
- )
-}
-
-#[inline]
-fn u64_at(buf: &[u8], offset: usize) -> u64 {
- u64::from_le_bytes(
- buf[offset..offset + 8]
- .try_into()
- .expect("slice is exactly 8 bytes"),
- )
-}
-
-#[inline]
-fn u128_at(buf: &[u8], offset: usize) -> u128 {
- u128::from_le_bytes(
- buf[offset..offset + 16]
- .try_into()
- .expect("slice is exactly 16 bytes"),
- )
-}
-
-/// Validate a message frame buffer and return `(total_size, payload_len, user_headers_len)`.
-fn validate_frame(buf: &[u8]) -> Result<(usize, usize, usize), WireError> {
- if buf.len() < WIRE_MESSAGE_HEADER_SIZE {
- return Err(WireError::UnexpectedEof {
- offset: 0,
- need: WIRE_MESSAGE_HEADER_SIZE,
- have: buf.len(),
- });
- }
-
- let user_headers_len = u32_at(buf, MSG_USER_HEADERS_LEN_OFFSET) as usize;
- let payload_len = u32_at(buf, MSG_PAYLOAD_LEN_OFFSET) as usize;
-
- let total = WIRE_MESSAGE_HEADER_SIZE
- .checked_add(payload_len)
- .and_then(|s| s.checked_add(user_headers_len))
- .ok_or(WireError::Validation(Cow::Borrowed(
- "message frame size overflow",
- )))?;
-
- if buf.len() < total {
- return Err(WireError::UnexpectedEof {
- offset: 0,
- need: total,
- have: buf.len(),
- });
- }
-
- Ok((total, payload_len, user_headers_len))
-}
-
-// WireMessageView - immutable zero-copy frame view
-
-/// Borrowed view over a single message frame in a contiguous buffer.
-///
-/// Validates the buffer on construction; all accessors are infallible.
-pub struct WireMessageView<'a> {
- buf: &'a [u8],
- payload_len: usize,
- user_headers_len: usize,
-}
-
-impl<'a> WireMessageView<'a> {
- /// Create a view over the first message frame in `buf`.
- ///
- /// # Errors
- /// Returns `WireError` if the buffer is shorter than the header (64 bytes)
- /// or shorter than the full frame indicated by the length fields.
- pub fn new(buf: &'a [u8]) -> Result<Self, WireError> {
- let (total, payload_len, user_headers_len) = validate_frame(buf)?;
- Ok(Self {
- buf: &buf[..total],
- payload_len,
- user_headers_len,
- })
- }
-
- #[must_use]
- #[inline]
- pub fn checksum(&self) -> u64 {
- u64_at(self.buf, MSG_CHECKSUM_OFFSET)
- }
-
- #[must_use]
- #[inline]
- pub fn id(&self) -> u128 {
- u128_at(self.buf, MSG_ID_OFFSET)
- }
-
- #[must_use]
- #[inline]
- pub fn offset(&self) -> u64 {
- u64_at(self.buf, MSG_OFFSET_OFFSET)
- }
-
- #[must_use]
- #[inline]
- pub fn timestamp(&self) -> u64 {
- u64_at(self.buf, MSG_TIMESTAMP_OFFSET)
- }
-
- #[must_use]
- #[inline]
- pub fn origin_timestamp(&self) -> u64 {
- u64_at(self.buf, MSG_ORIGIN_TIMESTAMP_OFFSET)
- }
-
- #[must_use]
- #[inline]
- pub const fn payload_length(&self) -> usize {
- self.payload_len
- }
-
- #[must_use]
- #[inline]
- pub const fn user_headers_length(&self) -> usize {
- self.user_headers_len
- }
-
- #[must_use]
- pub fn payload(&self) -> &'a [u8] {
- &self.buf[WIRE_MESSAGE_HEADER_SIZE..WIRE_MESSAGE_HEADER_SIZE + self.payload_len]
- }
-
- #[must_use]
- pub fn user_headers(&self) -> &'a [u8] {
- let start = WIRE_MESSAGE_HEADER_SIZE + self.payload_len;
- &self.buf[start..start + self.user_headers_len]
- }
-
- #[must_use]
- #[inline]
- pub const fn total_size(&self) -> usize {
- self.buf.len()
- }
-
- #[must_use]
- pub const fn as_bytes(&self) -> &'a [u8] {
- self.buf
- }
-}
-
-// WireMessageViewMut - mutable zero-copy frame view
-
-/// Mutable view for in-place header patching (offset, timestamp, checksum).
-pub struct WireMessageViewMut<'a> {
- buf: &'a mut [u8],
- payload_len: usize,
- user_headers_len: usize,
-}
-
-impl<'a> WireMessageViewMut<'a> {
- /// Create a mutable view over the first message frame in `buf`.
- ///
- /// # Errors
- /// Same validation as [`WireMessageView::new`].
- pub fn new(buf: &'a mut [u8]) -> Result<Self, WireError> {
- let (total, payload_len, user_headers_len) = validate_frame(buf)?;
- let (frame, _) = buf.split_at_mut(total);
- Ok(Self {
- buf: frame,
- payload_len,
- user_headers_len,
- })
- }
-
- // -- Read accessors (same semantics as WireMessageView) --
-
- #[must_use]
- #[inline]
- pub fn checksum(&self) -> u64 {
- u64_at(self.buf, MSG_CHECKSUM_OFFSET)
- }
-
- #[must_use]
- #[inline]
- pub fn id(&self) -> u128 {
- u128_at(self.buf, MSG_ID_OFFSET)
- }
-
- #[must_use]
- #[inline]
- pub fn offset(&self) -> u64 {
- u64_at(self.buf, MSG_OFFSET_OFFSET)
- }
-
- #[must_use]
- #[inline]
- pub fn timestamp(&self) -> u64 {
- u64_at(self.buf, MSG_TIMESTAMP_OFFSET)
- }
-
- #[must_use]
- #[inline]
- pub fn origin_timestamp(&self) -> u64 {
- u64_at(self.buf, MSG_ORIGIN_TIMESTAMP_OFFSET)
- }
-
- #[must_use]
- #[inline]
- pub const fn payload_length(&self) -> usize {
- self.payload_len
- }
-
- #[must_use]
- #[inline]
- pub const fn user_headers_length(&self) -> usize {
- self.user_headers_len
- }
-
- #[must_use]
- pub fn payload(&self) -> &[u8] {
- &self.buf[WIRE_MESSAGE_HEADER_SIZE..WIRE_MESSAGE_HEADER_SIZE + self.payload_len]
- }
-
- #[must_use]
- pub fn user_headers(&self) -> &[u8] {
- let start = WIRE_MESSAGE_HEADER_SIZE + self.payload_len;
- &self.buf[start..start + self.user_headers_len]
- }
-
- #[must_use]
- #[inline]
- pub const fn total_size(&self) -> usize {
- self.buf.len()
- }
-
- // -- Typed setters --
-
- pub fn set_checksum(&mut self, value: u64) {
- self.buf[MSG_CHECKSUM_OFFSET..MSG_CHECKSUM_OFFSET + 8]
- .copy_from_slice(&value.to_le_bytes());
- }
-
- pub fn set_id(&mut self, value: u128) {
- self.buf[MSG_ID_OFFSET..MSG_ID_OFFSET + 16].copy_from_slice(&value.to_le_bytes());
- }
-
- pub fn set_offset(&mut self, value: u64) {
- self.buf[MSG_OFFSET_OFFSET..MSG_OFFSET_OFFSET + 8].copy_from_slice(&value.to_le_bytes());
- }
-
- pub fn set_timestamp(&mut self, value: u64) {
- self.buf[MSG_TIMESTAMP_OFFSET..MSG_TIMESTAMP_OFFSET + 8]
- .copy_from_slice(&value.to_le_bytes());
- }
-}
-
-// WireMessageIterator - zero-copy frame iterator
-
-/// Iterates over contiguous message frames, yielding borrowed views.
-pub struct WireMessageIterator<'a> {
- buf: &'a [u8],
- pos: usize,
- remaining: u32,
-}
-
-impl<'a> WireMessageIterator<'a> {
- #[must_use]
- pub const fn new(buf: &'a [u8], count: u32) -> Self {
- Self {
- buf,
- pos: 0,
- remaining: count,
- }
- }
-}
-
-impl<'a> Iterator for WireMessageIterator<'a> {
- type Item = Result<WireMessageView<'a>, WireError>;
-
- fn next(&mut self) -> Option<Self::Item> {
- if self.remaining == 0 {
- return None;
- }
- match WireMessageView::new(&self.buf[self.pos..]) {
- Ok(view) => {
- self.pos += view.total_size();
- self.remaining -= 1;
- Some(Ok(view))
- }
- Err(e) => {
- self.remaining = 0;
- Some(Err(e))
- }
- }
- }
-}
-
-// WireMessageIteratorMut - mutable frame iterator (cursor-based)
-
-/// Mutable iterator over contiguous message frames.
-///
-/// Cannot implement standard `Iterator` because the yielded mutable view's
-/// lifetime is tied to `&mut self`. Uses a cursor-based API instead.
-pub struct WireMessageIteratorMut<'a> {
- buf: &'a mut [u8],
- pos: usize,
- remaining: u32,
-}
-
-impl<'a> WireMessageIteratorMut<'a> {
- pub const fn new(buf: &'a mut [u8], count: u32) -> Self {
- Self {
- buf,
- pos: 0,
- remaining: count,
- }
- }
-
- /// Advance to the next frame and return a mutable view over it.
- ///
- /// The returned view borrows `self`, so it must be dropped before
- /// calling this method again.
- pub fn next_view_mut(&mut self) -> Option<Result<WireMessageViewMut<'_>, WireError>> {
- if self.remaining == 0 {
- return None;
- }
-
- // Compute frame size via temporary immutable access (borrow ends at block exit).
- let (total, available) = {
- let rest = &self.buf[self.pos..];
- if rest.len() < WIRE_MESSAGE_HEADER_SIZE {
- self.remaining = 0;
- return Some(Err(WireError::UnexpectedEof {
- offset: self.pos,
- need: WIRE_MESSAGE_HEADER_SIZE,
- have: rest.len(),
- }));
- }
- let user_headers_len = u32_at(rest, MSG_USER_HEADERS_LEN_OFFSET) as usize;
- let payload_len = u32_at(rest, MSG_PAYLOAD_LEN_OFFSET) as usize;
- let Some(total) = WIRE_MESSAGE_HEADER_SIZE
- .checked_add(payload_len)
- .and_then(|s| s.checked_add(user_headers_len))
- else {
- self.remaining = 0;
- return Some(Err(WireError::Validation(Cow::Borrowed(
- "message frame size overflow",
- ))));
- };
- (total, rest.len())
- };
-
- if available < total {
- self.remaining = 0;
- return Some(Err(WireError::UnexpectedEof {
- offset: self.pos,
- need: total,
- have: available,
- }));
- }
-
- let start = self.pos;
- self.pos += total;
- self.remaining -= 1;
-
- Some(WireMessageViewMut::new(&mut self.buf[start..start + total]))
- }
-}
-
-// Tests
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::message_layout::{
- MSG_ID_OFFSET, MSG_ORIGIN_TIMESTAMP_OFFSET, MSG_PAYLOAD_LEN_OFFSET,
- MSG_USER_HEADERS_LEN_OFFSET, WIRE_MESSAGE_HEADER_SIZE,
- };
-
- fn make_frame(payload: &[u8], user_headers: &[u8], id: u128, origin_ts: u64) -> Vec<u8> {
- let total = WIRE_MESSAGE_HEADER_SIZE + payload.len() + user_headers.len();
- let mut frame = vec![0u8; total];
- frame[MSG_ID_OFFSET..MSG_ID_OFFSET + 16].copy_from_slice(&id.to_le_bytes());
- frame[MSG_ORIGIN_TIMESTAMP_OFFSET..MSG_ORIGIN_TIMESTAMP_OFFSET + 8]
- .copy_from_slice(&origin_ts.to_le_bytes());
- #[allow(clippy::cast_possible_truncation)]
- {
- frame[MSG_USER_HEADERS_LEN_OFFSET..MSG_USER_HEADERS_LEN_OFFSET + 4]
- .copy_from_slice(&(user_headers.len() as u32).to_le_bytes());
- frame[MSG_PAYLOAD_LEN_OFFSET..MSG_PAYLOAD_LEN_OFFSET + 4]
- .copy_from_slice(&(payload.len() as u32).to_le_bytes());
- }
- frame[WIRE_MESSAGE_HEADER_SIZE..WIRE_MESSAGE_HEADER_SIZE + payload.len()]
- .copy_from_slice(payload);
- frame[WIRE_MESSAGE_HEADER_SIZE + payload.len()..].copy_from_slice(user_headers);
- frame
- }
-
- // -- WireMessageView --
-
- #[test]
- fn view_rejects_short_buffer() {
- let buf = [0u8; WIRE_MESSAGE_HEADER_SIZE - 1];
- assert!(WireMessageView::new(&buf).is_err());
- }
-
- #[test]
- fn view_rejects_truncated_payload() {
- let mut buf = vec![0u8; WIRE_MESSAGE_HEADER_SIZE + 5];
- buf[MSG_PAYLOAD_LEN_OFFSET..MSG_PAYLOAD_LEN_OFFSET + 4]
- .copy_from_slice(&10u32.to_le_bytes());
- assert!(WireMessageView::new(&buf).is_err());
- }
-
- #[test]
- fn view_accessors() {
- let frame = make_frame(b"hello", b"hdr", 42, 999);
- let view = WireMessageView::new(&frame).unwrap();
- assert_eq!(view.checksum(), 0);
- assert_eq!(view.id(), 42);
- assert_eq!(view.offset(), 0);
- assert_eq!(view.timestamp(), 0);
- assert_eq!(view.origin_timestamp(), 999);
- assert_eq!(view.payload_length(), 5);
- assert_eq!(view.user_headers_length(), 3);
- assert_eq!(view.payload(), b"hello");
- assert_eq!(view.user_headers(), b"hdr");
- assert_eq!(view.total_size(), WIRE_MESSAGE_HEADER_SIZE + 5 + 3);
- assert_eq!(view.as_bytes().len(), view.total_size());
- }
-
- #[test]
- fn view_empty_payload_and_headers() {
- let frame = make_frame(b"", b"", 1, 0);
- let view = WireMessageView::new(&frame).unwrap();
- assert_eq!(view.payload(), b"");
- assert_eq!(view.user_headers(), b"");
- assert_eq!(view.total_size(), WIRE_MESSAGE_HEADER_SIZE);
- }
-
- #[test]
- fn view_from_larger_buffer() {
- let mut buf = make_frame(b"pay", b"", 1, 0);
- buf.extend_from_slice(b"trailing_garbage");
- let view = WireMessageView::new(&buf).unwrap();
- assert_eq!(view.total_size(), WIRE_MESSAGE_HEADER_SIZE + 3);
- assert_eq!(view.as_bytes().len(), WIRE_MESSAGE_HEADER_SIZE + 3);
- }
-
- // -- WireMessageViewMut --
-
- #[test]
- fn view_mut_set_get_roundtrip() {
- let mut frame = make_frame(b"data", b"", 1, 0);
- let mut view = WireMessageViewMut::new(&mut frame).unwrap();
-
- view.set_checksum(0xDEAD_BEEF);
- view.set_id(0x1234);
- view.set_offset(100);
- view.set_timestamp(200);
-
- assert_eq!(view.checksum(), 0xDEAD_BEEF);
- assert_eq!(view.id(), 0x1234);
- assert_eq!(view.offset(), 100);
- assert_eq!(view.timestamp(), 200);
- assert_eq!(view.origin_timestamp(), 0);
- assert_eq!(view.payload(), b"data");
- }
-
- #[test]
- fn view_mut_set_does_not_corrupt_adjacent_fields() {
- let mut frame = make_frame(b"x", b"y", 42, 999);
- {
- let mut view = WireMessageViewMut::new(&mut frame).unwrap();
- view.set_offset(0xFFFF_FFFF_FFFF_FFFF);
- }
- let view = WireMessageView::new(&frame).unwrap();
- assert_eq!(view.id(), 42);
- assert_eq!(view.offset(), 0xFFFF_FFFF_FFFF_FFFF);
- assert_eq!(view.timestamp(), 0);
- assert_eq!(view.origin_timestamp(), 999);
- assert_eq!(view.payload(), b"x");
- assert_eq!(view.user_headers(), b"y");
- }
-
- // -- WireMessageIterator --
-
- #[test]
- fn iterator_zero_count() {
- let buf = [];
- let mut iter = WireMessageIterator::new(&buf, 0);
- assert!(iter.next().is_none());
- }
-
- #[test]
- fn iterator_single_message() {
- let frame = make_frame(b"hello", b"", 1, 100);
- let mut iter = WireMessageIterator::new(&frame, 1);
- let view = iter.next().unwrap().unwrap();
- assert_eq!(view.id(), 1);
- assert_eq!(view.payload(), b"hello");
- assert!(iter.next().is_none());
- }
-
- #[test]
- fn iterator_multiple_messages() {
- let f1 = make_frame(b"aaa", b"", 1, 100);
- let f2 = make_frame(b"bbb", b"hh", 2, 200);
- let f3 = make_frame(b"c", b"", 3, 300);
- let mut buf = Vec::new();
- buf.extend_from_slice(&f1);
- buf.extend_from_slice(&f2);
- buf.extend_from_slice(&f3);
-
- let views: Vec<_> = WireMessageIterator::new(&buf, 3)
- .collect::<Result<Vec<_>, _>>()
- .unwrap();
- assert_eq!(views.len(), 3);
- assert_eq!(views[0].id(), 1);
- assert_eq!(views[0].payload(), b"aaa");
- assert_eq!(views[1].id(), 2);
- assert_eq!(views[1].payload(), b"bbb");
- assert_eq!(views[1].user_headers(), b"hh");
- assert_eq!(views[2].id(), 3);
- assert_eq!(views[2].payload(), b"c");
- }
-
- #[test]
- fn iterator_truncated_mid_frame() {
- let frame = make_frame(b"hello", b"", 1, 0);
- let truncated = &frame[..frame.len() / 2];
- let mut iter = WireMessageIterator::new(truncated, 1);
- assert!(iter.next().unwrap().is_err());
- assert!(iter.next().is_none());
- }
-
- // -- WireMessageIteratorMut --
-
- #[test]
- fn iterator_mut_mutate_then_read() {
- let f1 = make_frame(b"aaa", b"", 1, 100);
- let f2 = make_frame(b"bbb", b"", 2, 200);
- let mut buf = Vec::new();
- buf.extend_from_slice(&f1);
- buf.extend_from_slice(&f2);
-
- let mut iter = WireMessageIteratorMut::new(&mut buf, 2);
- {
- let mut view = iter.next_view_mut().unwrap().unwrap();
- view.set_offset(10);
- view.set_timestamp(1000);
- }
- {
- let mut view = iter.next_view_mut().unwrap().unwrap();
- view.set_offset(20);
- view.set_timestamp(2000);
- }
- assert!(iter.next_view_mut().is_none());
-
- let views: Vec<_> = WireMessageIterator::new(&buf, 2)
- .collect::<Result<Vec<_>, _>>()
- .unwrap();
- assert_eq!(views[0].offset(), 10);
- assert_eq!(views[0].timestamp(), 1000);
- assert_eq!(views[0].payload(), b"aaa");
- assert_eq!(views[1].offset(), 20);
- assert_eq!(views[1].timestamp(), 2000);
- assert_eq!(views[1].payload(), b"bbb");
- }
-
- #[test]
- fn iterator_mut_count_matches() {
- let f1 = make_frame(b"a", b"", 1, 0);
- let f2 = make_frame(b"b", b"", 2, 0);
- let mut buf = Vec::new();
- buf.extend_from_slice(&f1);
- buf.extend_from_slice(&f2);
-
- let mut iter = WireMessageIteratorMut::new(&mut buf, 2);
- let mut count = 0;
- while let Some(Ok(_)) = iter.next_view_mut() {
- count += 1;
- }
- assert_eq!(count, 2);
- }
-}
diff --git a/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset.rs b/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset.rs
index 1c3c480..0140817 100644
--- a/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset.rs
+++ b/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset.rs
@@ -18,14 +18,18 @@
use crate::WireError;
use crate::WireIdentifier;
use crate::codec::{WireDecode, WireEncode, read_u8, read_u32_le};
+use crate::primitives::ack_level::AckLevel;
use crate::primitives::consumer::WireConsumer;
use bytes::{BufMut, BytesMut};
/// `DeleteConsumerOffset` request.
///
+/// Adds an `ack` byte: `NoAck` = leader-local fast path, `Quorum` = VSR
+/// pipeline.
+///
/// Wire format:
/// ```text
-/// [consumer][stream_id][topic_id][partition_flag:1][partition_id:4 LE]
+/// [consumer][stream_id][topic_id][partition_flag:1][partition_id:4 LE][ack:1]
/// ```
///
/// `partition_id` encoding: a u8 flag (1=Some, 0=None) followed by 4 bytes
@@ -36,6 +40,7 @@
pub stream_id: WireIdentifier,
pub topic_id: WireIdentifier,
pub partition_id: Option<u32>,
+ pub ack: AckLevel,
}
impl WireEncode for DeleteConsumerOffsetRequest {
@@ -45,6 +50,7 @@
+ self.topic_id.encoded_size()
+ 1
+ 4
+ + 1
}
fn encode(&self, buf: &mut BytesMut) {
@@ -58,6 +64,7 @@
buf.put_u8(0);
buf.put_u32_le(0);
}
+ buf.put_u8(self.ack.as_u8());
}
}
@@ -79,12 +86,16 @@
} else {
None
};
+ let ack_code = read_u8(buf, pos)?;
+ pos += 1;
+ let ack = AckLevel::from_code(ack_code)?;
Ok((
Self {
consumer,
stream_id,
topic_id,
partition_id,
+ ack,
},
pos,
))
@@ -96,12 +107,13 @@
use super::*;
#[test]
- fn roundtrip_with_partition() {
+ fn roundtrip_with_partition_quorum() {
let req = DeleteConsumerOffsetRequest {
consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
stream_id: WireIdentifier::numeric(10),
topic_id: WireIdentifier::numeric(20),
partition_id: Some(5),
+ ack: AckLevel::Quorum,
};
let bytes = req.to_bytes();
let (decoded, consumed) = DeleteConsumerOffsetRequest::decode(&bytes).unwrap();
@@ -110,12 +122,13 @@
}
#[test]
- fn roundtrip_without_partition() {
+ fn roundtrip_without_partition_no_ack() {
let req = DeleteConsumerOffsetRequest {
consumer: WireConsumer::consumer_group(WireIdentifier::numeric(3)),
stream_id: WireIdentifier::numeric(1),
topic_id: WireIdentifier::numeric(1),
partition_id: None,
+ ack: AckLevel::NoAck,
};
let bytes = req.to_bytes();
let (decoded, consumed) = DeleteConsumerOffsetRequest::decode(&bytes).unwrap();
@@ -130,6 +143,7 @@
stream_id: WireIdentifier::named("stream-1").unwrap(),
topic_id: WireIdentifier::named("topic-1").unwrap(),
partition_id: Some(0),
+ ack: AckLevel::Quorum,
};
let bytes = req.to_bytes();
let (decoded, consumed) = DeleteConsumerOffsetRequest::decode(&bytes).unwrap();
@@ -138,22 +152,31 @@
}
#[test]
- fn partition_none_encodes_zero_bytes() {
+ fn ack_byte_is_last() {
let req = DeleteConsumerOffsetRequest {
consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
stream_id: WireIdentifier::numeric(1),
topic_id: WireIdentifier::numeric(1),
- partition_id: None,
+ partition_id: Some(0),
+ ack: AckLevel::NoAck,
};
let bytes = req.to_bytes();
- let partition_offset = req.consumer.encoded_size()
- + req.stream_id.encoded_size()
- + req.topic_id.encoded_size();
- assert_eq!(bytes[partition_offset], 0);
- assert_eq!(
- &bytes[partition_offset + 1..partition_offset + 5],
- &[0, 0, 0, 0]
- );
+ assert_eq!(*bytes.last().unwrap(), AckLevel::NoAck.as_u8());
+ }
+
+ #[test]
+ fn unknown_ack_rejected() {
+ let req = DeleteConsumerOffsetRequest {
+ consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
+ stream_id: WireIdentifier::numeric(1),
+ topic_id: WireIdentifier::numeric(1),
+ partition_id: Some(0),
+ ack: AckLevel::Quorum,
+ };
+ let mut bytes = req.to_bytes().to_vec();
+ let last = bytes.len() - 1;
+ bytes[last] = 0xFF;
+ assert!(DeleteConsumerOffsetRequest::decode(&bytes).is_err());
}
#[test]
@@ -163,6 +186,7 @@
stream_id: WireIdentifier::numeric(1),
topic_id: WireIdentifier::numeric(1),
partition_id: Some(1),
+ ack: AckLevel::Quorum,
};
let bytes = req.to_bytes();
for i in 0..bytes.len() {
diff --git a/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset_2.rs b/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset_2.rs
deleted file mode 100644
index ccfa104..0000000
--- a/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset_2.rs
+++ /dev/null
@@ -1,199 +0,0 @@
-// 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.
-
-use crate::WireError;
-use crate::WireIdentifier;
-use crate::codec::{WireDecode, WireEncode, read_u8, read_u32_le};
-use crate::primitives::ack_level::AckLevel;
-use crate::primitives::consumer::WireConsumer;
-use bytes::{BufMut, BytesMut};
-
-/// `DeleteConsumerOffset` v2 request.
-///
-/// Adds an `ack` byte: `NoAck` = leader-local fast path, `Quorum` = VSR
-/// pipeline.
-///
-/// Wire format:
-/// ```text
-/// [consumer][stream_id][topic_id][partition_flag:1][partition_id:4 LE][ack:1]
-/// ```
-///
-/// `partition_id` encoding: a u8 flag (1=Some, 0=None) followed by 4 bytes
-/// for the u32 value (0 when None).
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct DeleteConsumerOffset2Request {
- pub consumer: WireConsumer,
- pub stream_id: WireIdentifier,
- pub topic_id: WireIdentifier,
- pub partition_id: Option<u32>,
- pub ack: AckLevel,
-}
-
-impl WireEncode for DeleteConsumerOffset2Request {
- fn encoded_size(&self) -> usize {
- self.consumer.encoded_size()
- + self.stream_id.encoded_size()
- + self.topic_id.encoded_size()
- + 1
- + 4
- + 1
- }
-
- fn encode(&self, buf: &mut BytesMut) {
- self.consumer.encode(buf);
- self.stream_id.encode(buf);
- self.topic_id.encode(buf);
- if let Some(pid) = self.partition_id {
- buf.put_u8(1);
- buf.put_u32_le(pid);
- } else {
- buf.put_u8(0);
- buf.put_u32_le(0);
- }
- buf.put_u8(self.ack.as_u8());
- }
-}
-
-impl WireDecode for DeleteConsumerOffset2Request {
- fn decode(buf: &[u8]) -> Result<(Self, usize), WireError> {
- let mut pos = 0;
- let (consumer, n) = WireConsumer::decode(&buf[pos..])?;
- pos += n;
- let (stream_id, n) = WireIdentifier::decode(&buf[pos..])?;
- pos += n;
- let (topic_id, n) = WireIdentifier::decode(&buf[pos..])?;
- pos += n;
- let partition_flag = read_u8(buf, pos)?;
- pos += 1;
- let partition_raw = read_u32_le(buf, pos)?;
- pos += 4;
- let partition_id = if partition_flag == 1 {
- Some(partition_raw)
- } else {
- None
- };
- let ack_code = read_u8(buf, pos)?;
- pos += 1;
- let ack = AckLevel::from_code(ack_code)?;
- Ok((
- Self {
- consumer,
- stream_id,
- topic_id,
- partition_id,
- ack,
- },
- pos,
- ))
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn roundtrip_with_partition_quorum() {
- let req = DeleteConsumerOffset2Request {
- consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
- stream_id: WireIdentifier::numeric(10),
- topic_id: WireIdentifier::numeric(20),
- partition_id: Some(5),
- ack: AckLevel::Quorum,
- };
- let bytes = req.to_bytes();
- let (decoded, consumed) = DeleteConsumerOffset2Request::decode(&bytes).unwrap();
- assert_eq!(consumed, bytes.len());
- assert_eq!(decoded, req);
- }
-
- #[test]
- fn roundtrip_without_partition_no_ack() {
- let req = DeleteConsumerOffset2Request {
- consumer: WireConsumer::consumer_group(WireIdentifier::numeric(3)),
- stream_id: WireIdentifier::numeric(1),
- topic_id: WireIdentifier::numeric(1),
- partition_id: None,
- ack: AckLevel::NoAck,
- };
- let bytes = req.to_bytes();
- let (decoded, consumed) = DeleteConsumerOffset2Request::decode(&bytes).unwrap();
- assert_eq!(consumed, bytes.len());
- assert_eq!(decoded, req);
- }
-
- #[test]
- fn roundtrip_named_identifiers() {
- let req = DeleteConsumerOffset2Request {
- consumer: WireConsumer::consumer(WireIdentifier::named("my-consumer").unwrap()),
- stream_id: WireIdentifier::named("stream-1").unwrap(),
- topic_id: WireIdentifier::named("topic-1").unwrap(),
- partition_id: Some(0),
- ack: AckLevel::Quorum,
- };
- let bytes = req.to_bytes();
- let (decoded, consumed) = DeleteConsumerOffset2Request::decode(&bytes).unwrap();
- assert_eq!(consumed, bytes.len());
- assert_eq!(decoded, req);
- }
-
- #[test]
- fn ack_byte_is_last() {
- let req = DeleteConsumerOffset2Request {
- consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
- stream_id: WireIdentifier::numeric(1),
- topic_id: WireIdentifier::numeric(1),
- partition_id: Some(0),
- ack: AckLevel::NoAck,
- };
- let bytes = req.to_bytes();
- assert_eq!(*bytes.last().unwrap(), AckLevel::NoAck.as_u8());
- }
-
- #[test]
- fn unknown_ack_rejected() {
- let req = DeleteConsumerOffset2Request {
- consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
- stream_id: WireIdentifier::numeric(1),
- topic_id: WireIdentifier::numeric(1),
- partition_id: Some(0),
- ack: AckLevel::Quorum,
- };
- let mut bytes = req.to_bytes().to_vec();
- let last = bytes.len() - 1;
- bytes[last] = 0xFF;
- assert!(DeleteConsumerOffset2Request::decode(&bytes).is_err());
- }
-
- #[test]
- fn truncated_returns_error() {
- let req = DeleteConsumerOffset2Request {
- consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
- stream_id: WireIdentifier::numeric(1),
- topic_id: WireIdentifier::numeric(1),
- partition_id: Some(1),
- ack: AckLevel::Quorum,
- };
- let bytes = req.to_bytes();
- for i in 0..bytes.len() {
- assert!(
- DeleteConsumerOffset2Request::decode(&bytes[..i]).is_err(),
- "expected error for truncation at byte {i}"
- );
- }
- }
-}
diff --git a/core/binary_protocol/src/requests/consumer_offsets/mod.rs b/core/binary_protocol/src/requests/consumer_offsets/mod.rs
index f8d3cee..bde4d02 100644
--- a/core/binary_protocol/src/requests/consumer_offsets/mod.rs
+++ b/core/binary_protocol/src/requests/consumer_offsets/mod.rs
@@ -16,13 +16,9 @@
// under the License.
pub mod delete_consumer_offset;
-pub mod delete_consumer_offset_2;
pub mod get_consumer_offset;
pub mod store_consumer_offset;
-pub mod store_consumer_offset_2;
pub use delete_consumer_offset::DeleteConsumerOffsetRequest;
-pub use delete_consumer_offset_2::DeleteConsumerOffset2Request;
pub use get_consumer_offset::GetConsumerOffsetRequest;
pub use store_consumer_offset::StoreConsumerOffsetRequest;
-pub use store_consumer_offset_2::StoreConsumerOffset2Request;
diff --git a/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset.rs b/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset.rs
index 10bfd94..5261d5c 100644
--- a/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset.rs
+++ b/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset.rs
@@ -18,14 +18,18 @@
use crate::WireError;
use crate::WireIdentifier;
use crate::codec::{WireDecode, WireEncode, read_u8, read_u32_le, read_u64_le};
+use crate::primitives::ack_level::AckLevel;
use crate::primitives::consumer::WireConsumer;
use bytes::{BufMut, BytesMut};
/// `StoreConsumerOffset` request.
///
+/// Adds an `ack` byte: `NoAck` = leader-local fast path, `Quorum` = VSR
+/// pipeline.
+///
/// Wire format:
/// ```text
-/// [consumer][stream_id][topic_id][partition_flag:1][partition_id:4 LE][offset:8 LE]
+/// [consumer][stream_id][topic_id][partition_flag:1][partition_id:4 LE][offset:8 LE][ack:1]
/// ```
///
/// `partition_id` encoding: a u8 flag (1=Some, 0=None) followed by 4 bytes
@@ -37,6 +41,7 @@
pub topic_id: WireIdentifier,
pub partition_id: Option<u32>,
pub offset: u64,
+ pub ack: AckLevel,
}
impl WireEncode for StoreConsumerOffsetRequest {
@@ -47,6 +52,7 @@
+ 1
+ 4
+ 8
+ + 1
}
fn encode(&self, buf: &mut BytesMut) {
@@ -61,6 +67,7 @@
buf.put_u32_le(0);
}
buf.put_u64_le(self.offset);
+ buf.put_u8(self.ack.as_u8());
}
}
@@ -84,6 +91,9 @@
};
let offset = read_u64_le(buf, pos)?;
pos += 8;
+ let ack_code = read_u8(buf, pos)?;
+ pos += 1;
+ let ack = AckLevel::from_code(ack_code)?;
Ok((
Self {
consumer,
@@ -91,6 +101,7 @@
topic_id,
partition_id,
offset,
+ ack,
},
pos,
))
@@ -102,13 +113,14 @@
use super::*;
#[test]
- fn roundtrip_with_partition() {
+ fn roundtrip_with_partition_quorum() {
let req = StoreConsumerOffsetRequest {
consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
stream_id: WireIdentifier::numeric(10),
topic_id: WireIdentifier::numeric(20),
partition_id: Some(5),
offset: 12345,
+ ack: AckLevel::Quorum,
};
let bytes = req.to_bytes();
let (decoded, consumed) = StoreConsumerOffsetRequest::decode(&bytes).unwrap();
@@ -117,13 +129,14 @@
}
#[test]
- fn roundtrip_without_partition() {
+ fn roundtrip_without_partition_no_ack() {
let req = StoreConsumerOffsetRequest {
consumer: WireConsumer::consumer_group(WireIdentifier::numeric(3)),
stream_id: WireIdentifier::numeric(1),
topic_id: WireIdentifier::numeric(1),
partition_id: None,
offset: u64::MAX,
+ ack: AckLevel::NoAck,
};
let bytes = req.to_bytes();
let (decoded, consumed) = StoreConsumerOffsetRequest::decode(&bytes).unwrap();
@@ -139,6 +152,7 @@
topic_id: WireIdentifier::named("topic-1").unwrap(),
partition_id: Some(0),
offset: 0,
+ ack: AckLevel::Quorum,
};
let bytes = req.to_bytes();
let (decoded, consumed) = StoreConsumerOffsetRequest::decode(&bytes).unwrap();
@@ -147,17 +161,33 @@
}
#[test]
- fn offset_encoding() {
+ fn ack_byte_is_last() {
let req = StoreConsumerOffsetRequest {
consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
stream_id: WireIdentifier::numeric(1),
topic_id: WireIdentifier::numeric(1),
partition_id: Some(0),
- offset: 0x0102_0304_0506_0708,
+ offset: 0,
+ ack: AckLevel::NoAck,
};
let bytes = req.to_bytes();
- let (decoded, _) = StoreConsumerOffsetRequest::decode(&bytes).unwrap();
- assert_eq!(decoded.offset, 0x0102_0304_0506_0708);
+ assert_eq!(*bytes.last().unwrap(), AckLevel::NoAck.as_u8());
+ }
+
+ #[test]
+ fn unknown_ack_rejected() {
+ let req = StoreConsumerOffsetRequest {
+ consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
+ stream_id: WireIdentifier::numeric(1),
+ topic_id: WireIdentifier::numeric(1),
+ partition_id: Some(0),
+ offset: 0,
+ ack: AckLevel::Quorum,
+ };
+ let mut bytes = req.to_bytes().to_vec();
+ let last = bytes.len() - 1;
+ bytes[last] = 0xFF;
+ assert!(StoreConsumerOffsetRequest::decode(&bytes).is_err());
}
#[test]
@@ -168,6 +198,7 @@
topic_id: WireIdentifier::numeric(1),
partition_id: Some(1),
offset: 100,
+ ack: AckLevel::Quorum,
};
let bytes = req.to_bytes();
for i in 0..bytes.len() {
diff --git a/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset_2.rs b/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset_2.rs
deleted file mode 100644
index 37e89fb..0000000
--- a/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset_2.rs
+++ /dev/null
@@ -1,211 +0,0 @@
-// 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.
-
-use crate::WireError;
-use crate::WireIdentifier;
-use crate::codec::{WireDecode, WireEncode, read_u8, read_u32_le, read_u64_le};
-use crate::primitives::ack_level::AckLevel;
-use crate::primitives::consumer::WireConsumer;
-use bytes::{BufMut, BytesMut};
-
-/// `StoreConsumerOffset` v2 request.
-///
-/// Adds an `ack` byte: `NoAck` = leader-local fast path, `Quorum` = VSR
-/// pipeline.
-///
-/// Wire format:
-/// ```text
-/// [consumer][stream_id][topic_id][partition_flag:1][partition_id:4 LE][offset:8 LE][ack:1]
-/// ```
-///
-/// `partition_id` encoding: a u8 flag (1=Some, 0=None) followed by 4 bytes
-/// for the u32 value (0 when None).
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct StoreConsumerOffset2Request {
- pub consumer: WireConsumer,
- pub stream_id: WireIdentifier,
- pub topic_id: WireIdentifier,
- pub partition_id: Option<u32>,
- pub offset: u64,
- pub ack: AckLevel,
-}
-
-impl WireEncode for StoreConsumerOffset2Request {
- fn encoded_size(&self) -> usize {
- self.consumer.encoded_size()
- + self.stream_id.encoded_size()
- + self.topic_id.encoded_size()
- + 1
- + 4
- + 8
- + 1
- }
-
- fn encode(&self, buf: &mut BytesMut) {
- self.consumer.encode(buf);
- self.stream_id.encode(buf);
- self.topic_id.encode(buf);
- if let Some(pid) = self.partition_id {
- buf.put_u8(1);
- buf.put_u32_le(pid);
- } else {
- buf.put_u8(0);
- buf.put_u32_le(0);
- }
- buf.put_u64_le(self.offset);
- buf.put_u8(self.ack.as_u8());
- }
-}
-
-impl WireDecode for StoreConsumerOffset2Request {
- fn decode(buf: &[u8]) -> Result<(Self, usize), WireError> {
- let mut pos = 0;
- let (consumer, n) = WireConsumer::decode(&buf[pos..])?;
- pos += n;
- let (stream_id, n) = WireIdentifier::decode(&buf[pos..])?;
- pos += n;
- let (topic_id, n) = WireIdentifier::decode(&buf[pos..])?;
- pos += n;
- let partition_flag = read_u8(buf, pos)?;
- pos += 1;
- let partition_raw = read_u32_le(buf, pos)?;
- pos += 4;
- let partition_id = if partition_flag == 1 {
- Some(partition_raw)
- } else {
- None
- };
- let offset = read_u64_le(buf, pos)?;
- pos += 8;
- let ack_code = read_u8(buf, pos)?;
- pos += 1;
- let ack = AckLevel::from_code(ack_code)?;
- Ok((
- Self {
- consumer,
- stream_id,
- topic_id,
- partition_id,
- offset,
- ack,
- },
- pos,
- ))
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn roundtrip_with_partition_quorum() {
- let req = StoreConsumerOffset2Request {
- consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
- stream_id: WireIdentifier::numeric(10),
- topic_id: WireIdentifier::numeric(20),
- partition_id: Some(5),
- offset: 12345,
- ack: AckLevel::Quorum,
- };
- let bytes = req.to_bytes();
- let (decoded, consumed) = StoreConsumerOffset2Request::decode(&bytes).unwrap();
- assert_eq!(consumed, bytes.len());
- assert_eq!(decoded, req);
- }
-
- #[test]
- fn roundtrip_without_partition_no_ack() {
- let req = StoreConsumerOffset2Request {
- consumer: WireConsumer::consumer_group(WireIdentifier::numeric(3)),
- stream_id: WireIdentifier::numeric(1),
- topic_id: WireIdentifier::numeric(1),
- partition_id: None,
- offset: u64::MAX,
- ack: AckLevel::NoAck,
- };
- let bytes = req.to_bytes();
- let (decoded, consumed) = StoreConsumerOffset2Request::decode(&bytes).unwrap();
- assert_eq!(consumed, bytes.len());
- assert_eq!(decoded, req);
- }
-
- #[test]
- fn roundtrip_named_identifiers() {
- let req = StoreConsumerOffset2Request {
- consumer: WireConsumer::consumer(WireIdentifier::named("my-consumer").unwrap()),
- stream_id: WireIdentifier::named("stream-1").unwrap(),
- topic_id: WireIdentifier::named("topic-1").unwrap(),
- partition_id: Some(0),
- offset: 0,
- ack: AckLevel::Quorum,
- };
- let bytes = req.to_bytes();
- let (decoded, consumed) = StoreConsumerOffset2Request::decode(&bytes).unwrap();
- assert_eq!(consumed, bytes.len());
- assert_eq!(decoded, req);
- }
-
- #[test]
- fn ack_byte_is_last() {
- let req = StoreConsumerOffset2Request {
- consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
- stream_id: WireIdentifier::numeric(1),
- topic_id: WireIdentifier::numeric(1),
- partition_id: Some(0),
- offset: 0,
- ack: AckLevel::NoAck,
- };
- let bytes = req.to_bytes();
- assert_eq!(*bytes.last().unwrap(), AckLevel::NoAck.as_u8());
- }
-
- #[test]
- fn unknown_ack_rejected() {
- let req = StoreConsumerOffset2Request {
- consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
- stream_id: WireIdentifier::numeric(1),
- topic_id: WireIdentifier::numeric(1),
- partition_id: Some(0),
- offset: 0,
- ack: AckLevel::Quorum,
- };
- let mut bytes = req.to_bytes().to_vec();
- let last = bytes.len() - 1;
- bytes[last] = 0xFF;
- assert!(StoreConsumerOffset2Request::decode(&bytes).is_err());
- }
-
- #[test]
- fn truncated_returns_error() {
- let req = StoreConsumerOffset2Request {
- consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
- stream_id: WireIdentifier::numeric(1),
- topic_id: WireIdentifier::numeric(1),
- partition_id: Some(1),
- offset: 100,
- ack: AckLevel::Quorum,
- };
- let bytes = req.to_bytes();
- for i in 0..bytes.len() {
- assert!(
- StoreConsumerOffset2Request::decode(&bytes[..i]).is_err(),
- "expected error for truncation at byte {i}"
- );
- }
- }
-}
diff --git a/core/binary_protocol/src/requests/messages/mod.rs b/core/binary_protocol/src/requests/messages/mod.rs
index 6b976e0..7658409 100644
--- a/core/binary_protocol/src/requests/messages/mod.rs
+++ b/core/binary_protocol/src/requests/messages/mod.rs
@@ -21,6 +21,4 @@
pub use flush_unsaved_buffer::FlushUnsavedBufferRequest;
pub use poll_messages::PollMessagesRequest;
-pub use send_messages::{
- RawMessage, SendMessagesEncoder, SendMessagesHeader, SendMessagesMetadataEncoder,
-};
+pub use send_messages::{RawMessage, SendMessagesEncoder, SendMessagesHeader};
diff --git a/core/binary_protocol/src/requests/messages/send_messages.rs b/core/binary_protocol/src/requests/messages/send_messages.rs
index 559ce9e..4d4007f 100644
--- a/core/binary_protocol/src/requests/messages/send_messages.rs
+++ b/core/binary_protocol/src/requests/messages/send_messages.rs
@@ -15,16 +15,22 @@
// specific language governing permissions and limitations
// under the License.
-//! Zero-copy encoder for `SendMessages` wire format.
+//! Encoder for the `SendMessages` wire format.
//!
-//! Messages are written directly to the buffer without intermediate allocation.
+//! The body is the routing metadata section followed by one canonical message
+//! batch (see [`crate::batch`]) - the same record the server replicates,
+//! persists, and serves back to polls.
+use crate::batch::{
+ BATCH_HEADER_SIZE, BATCH_MESSAGE_HEADER_SIZE, BatchHeader, MAX_TIMESTAMP_DELTA_MICROS,
+ calculate_batch_checksum,
+};
use crate::codec::{WireDecode, WireEncode, read_u32_le};
use crate::error::WireError;
-use crate::message_layout::{WIRE_MESSAGE_HEADER_SIZE, WIRE_MESSAGE_INDEX_SIZE};
use crate::primitives::identifier::WireIdentifier;
use crate::primitives::partitioning::WirePartitioning;
use bytes::{BufMut, BytesMut};
+use twox_hash::XxHash3_64;
/// Borrowed message data for encoding. No allocation needed - the caller
/// owns the payload and headers buffers.
@@ -36,29 +42,12 @@
}
impl RawMessage<'_> {
- fn wire_size(&self) -> usize {
- WIRE_MESSAGE_HEADER_SIZE + self.payload.len() + self.headers.map_or(0, <[u8]>::len)
- }
-
- /// Write the 64-byte message header to `buf`. Payload and `user_headers` are NOT
- /// written - the caller sends them separately via vectored I/O.
- pub fn encode_header(&self, buf: &mut BytesMut) {
- let headers_len = self.headers.map_or(0, <[u8]>::len);
- buf.put_u64_le(0); // checksum (server-computed)
- buf.put_u128_le(self.id);
- buf.put_u64_le(0); // offset (server-assigned)
- buf.put_u64_le(0); // timestamp (server-assigned)
- buf.put_u64_le(self.origin_timestamp);
- #[allow(clippy::cast_possible_truncation)]
- {
- buf.put_u32_le(headers_len as u32);
- buf.put_u32_le(self.payload.len() as u32);
- }
- buf.put_u64_le(0); // reserved
+ fn frame_size(&self) -> usize {
+ BATCH_MESSAGE_HEADER_SIZE + self.payload.len() + self.headers.map_or(0, <[u8]>::len)
}
}
-/// Zero-copy encoder for the `SendMessages` command payload.
+/// Encoder for the `SendMessages` command payload.
///
/// Wire layout:
/// ```text
@@ -67,9 +56,12 @@
/// [topic_id:variable]
/// [partitioning:variable]
/// [messages_count:u32_le]
-/// [index_array: messages_count * 16 bytes]
-/// [message_data: variable]
+/// [batch: 256-byte batch header + message frames]
/// ```
+///
+/// The producer leaves `partition_id`, `base_offset`, and `base_timestamp`
+/// zero in the batch header; the server stamps them. Every checksum is
+/// producer-computed and verified at admission.
pub struct SendMessagesEncoder;
impl SendMessagesEncoder {
@@ -82,18 +74,31 @@
) -> usize {
let metadata_inner =
stream_id.encoded_size() + topic_id.encoded_size() + partitioning.encoded_size() + 4;
- let index_total = messages.len() * WIRE_MESSAGE_INDEX_SIZE;
- let messages_total: usize = messages.iter().map(RawMessage::wire_size).sum();
- 4 + metadata_inner + index_total + messages_total
+ let blob_total: usize = messages.iter().map(RawMessage::frame_size).sum();
+ 4 + metadata_inner + BATCH_HEADER_SIZE + blob_total
}
+ /// Encode the full `SendMessages` body into `buf`.
+ ///
+ /// # Errors
+ /// [`WireError::Validation`] on an empty message batch;
+ /// [`WireError::InvalidMessageTimestampDelta`] when a message's
+ /// `origin_timestamp` runs more than [`MAX_TIMESTAMP_DELTA_MICROS`] ahead
+ /// of the batch's earliest one; [`WireError::PayloadTooLarge`] when a
+ /// section length or the batch length overflows its wire field.
pub fn encode(
buf: &mut BytesMut,
stream_id: &WireIdentifier,
topic_id: &WireIdentifier,
partitioning: &WirePartitioning,
messages: &[RawMessage<'_>],
- ) {
+ ) -> Result<(), WireError> {
+ if messages.is_empty() {
+ return Err(WireError::Validation(std::borrow::Cow::Borrowed(
+ "cannot encode an empty message batch",
+ )));
+ }
+
let metadata_inner =
stream_id.encoded_size() + topic_id.encoded_size() + partitioning.encoded_size() + 4;
@@ -104,52 +109,83 @@
topic_id.encode(buf);
partitioning.encode(buf);
- #[allow(clippy::cast_possible_truncation)]
- buf.put_u32_le(messages.len() as u32);
+ let message_count =
+ u32::try_from(messages.len()).map_err(|_| WireError::PayloadTooLarge {
+ size: messages.len(),
+ max: u32::MAX as usize,
+ })?;
+ buf.put_u32_le(message_count);
- // Index array: cumulative sizes for each message
- let mut cumulative_size: u32 = 0;
- for msg in messages {
+ let origin_timestamp = messages
+ .iter()
+ .map(|message| message.origin_timestamp)
+ .min()
+ .unwrap_or(0);
+
+ // The batch header depends on the frame checksums written below, so
+ // reserve its slot and backpatch once the blob is in place.
+ let header_start = buf.len();
+ buf.resize(header_start + BATCH_HEADER_SIZE, 0);
+
+ let blob_start = buf.len();
+ for (index, message) in messages.iter().enumerate() {
+ let timestamp_delta = message.origin_timestamp - origin_timestamp;
+ if timestamp_delta > MAX_TIMESTAMP_DELTA_MICROS {
+ return Err(WireError::InvalidMessageTimestampDelta(timestamp_delta));
+ }
+ let headers = message.headers.unwrap_or_default();
+ let user_headers_length =
+ u32::try_from(headers.len()).map_err(|_| WireError::PayloadTooLarge {
+ size: headers.len(),
+ max: u32::MAX as usize,
+ })?;
+ let payload_length =
+ u32::try_from(message.payload.len()).map_err(|_| WireError::PayloadTooLarge {
+ size: message.payload.len(),
+ max: u32::MAX as usize,
+ })?;
+
+ let frame_start = buf.len();
+ buf.put_u64_le(0); // checksum, backpatched below
+ buf.put_u128_le(message.id);
#[allow(clippy::cast_possible_truncation)]
{
- cumulative_size += msg.wire_size() as u32;
+ buf.put_u32_le(index as u32); // offset_delta
+ buf.put_u32_le(timestamp_delta as u32);
}
- // bytes 0-3: zero
- buf.put_u32_le(0);
- // bytes 4-7: cumulative size
- buf.put_u32_le(cumulative_size);
- // bytes 8-15: zero
- buf.put_u64_le(0);
- }
-
- // Message data: header(64) + payload + optional user_headers
- for msg in messages {
- let headers_len = msg.headers.map_or(0, <[u8]>::len);
-
- buf.put_u64_le(0); // checksum (server-computed)
- buf.put_u128_le(msg.id);
- buf.put_u64_le(0); // offset (server-assigned)
- buf.put_u64_le(0); // timestamp (server-assigned)
- buf.put_u64_le(msg.origin_timestamp);
- #[allow(clippy::cast_possible_truncation)]
- {
- buf.put_u32_le(headers_len as u32);
- buf.put_u32_le(msg.payload.len() as u32);
- }
+ buf.put_u32_le(user_headers_length);
+ buf.put_u32_le(payload_length);
buf.put_u64_le(0); // reserved
- buf.put_slice(msg.payload);
- if let Some(headers) = msg.headers {
- buf.put_slice(headers);
- }
+ buf.put_slice(message.payload);
+ buf.put_slice(headers);
+
+ let checksum = XxHash3_64::oneshot(&buf[frame_start + 8..]);
+ buf[frame_start..frame_start + 8].copy_from_slice(&checksum.to_le_bytes());
}
+
+ // Request framing carries the body size in a u32, so a batch past
+ // u32::MAX cannot ride any request frame.
+ let batch_size = BATCH_HEADER_SIZE + (buf.len() - blob_start);
+ if batch_size > u32::MAX as usize {
+ return Err(WireError::PayloadTooLarge {
+ size: batch_size,
+ max: u32::MAX as usize,
+ });
+ }
+ let batch_length = batch_size as u64;
+ let mut batch_header = BatchHeader::new(0, origin_timestamp, batch_length, message_count);
+ batch_header.batch_checksum = calculate_batch_checksum(&batch_header, &buf[blob_start..]);
+ let header_bytes: &mut [u8] = &mut buf[header_start..header_start + BATCH_HEADER_SIZE];
+ batch_header.encode_into(header_bytes);
+ Ok(())
}
}
/// Metadata-only decoder for the `SendMessages` command.
///
/// Parses routing metadata (stream, topic, partitioning, message count)
-/// without touching message payloads. The server uses this to extract
-/// routing info before reading message data into pooled buffers.
+/// without touching the batch. The server uses this to resolve the namespace
+/// before validating the batch bytes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SendMessagesHeader {
pub stream_id: WireIdentifier,
@@ -204,145 +240,75 @@
}
}
-/// Vectored I/O encoder for `SendMessages`.
-///
-/// Writes metadata + index array but stops before message frame data,
-/// enabling the caller to compose `[header_buf | msg_frames...]` via writev.
-pub struct SendMessagesMetadataEncoder;
-
-impl SendMessagesMetadataEncoder {
- /// Exact size of metadata + indexes (everything except message frame data).
- #[must_use]
- pub fn header_size(
- stream_id: &WireIdentifier,
- topic_id: &WireIdentifier,
- partitioning: &WirePartitioning,
- messages_count: usize,
- ) -> usize {
- let metadata_inner =
- stream_id.encoded_size() + topic_id.encoded_size() + partitioning.encoded_size() + 4;
- 4 + metadata_inner + messages_count * WIRE_MESSAGE_INDEX_SIZE
- }
-
- /// Encode `metadata_length` + metadata fields + index array into `buf`.
- ///
- /// Does NOT write message frame data - the caller sends that via vectored I/O.
- pub fn encode_header(
- buf: &mut BytesMut,
- stream_id: &WireIdentifier,
- topic_id: &WireIdentifier,
- partitioning: &WirePartitioning,
- messages: &[RawMessage<'_>],
- ) {
- let metadata_inner =
- stream_id.encoded_size() + topic_id.encoded_size() + partitioning.encoded_size() + 4;
-
- #[allow(clippy::cast_possible_truncation)]
- buf.put_u32_le(metadata_inner as u32);
-
- stream_id.encode(buf);
- topic_id.encode(buf);
- partitioning.encode(buf);
-
- #[allow(clippy::cast_possible_truncation)]
- buf.put_u32_le(messages.len() as u32);
-
- let mut cumulative_size: u32 = 0;
- for msg in messages {
- #[allow(clippy::cast_possible_truncation)]
- {
- cumulative_size += msg.wire_size() as u32;
- }
- buf.put_u32_le(0);
- buf.put_u32_le(cumulative_size);
- buf.put_u64_le(0);
- }
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
+ use crate::batch::decode_batch_slice;
fn numeric_id(id: u32) -> WireIdentifier {
WireIdentifier::numeric(id)
}
+ fn encode(
+ stream_id: &WireIdentifier,
+ topic_id: &WireIdentifier,
+ partitioning: &WirePartitioning,
+ messages: &[RawMessage<'_>],
+ ) -> BytesMut {
+ let size = SendMessagesEncoder::encoded_size(stream_id, topic_id, partitioning, messages);
+ let mut buf = BytesMut::with_capacity(size);
+ SendMessagesEncoder::encode(&mut buf, stream_id, topic_id, partitioning, messages)
+ .expect("encodes");
+ assert_eq!(buf.len(), size);
+ buf
+ }
+
#[test]
- fn encode_single_message_no_headers() {
+ fn encoded_batch_decodes_and_verifies() {
let stream_id = numeric_id(1);
let topic_id = numeric_id(2);
let partitioning = WirePartitioning::Balanced;
- let payload = b"hello";
- let messages = [RawMessage {
- id: 100,
- origin_timestamp: 999,
- headers: None,
- payload: payload.as_slice(),
- }];
-
- let size =
- SendMessagesEncoder::encoded_size(&stream_id, &topic_id, &partitioning, &messages);
- let mut buf = BytesMut::with_capacity(size);
- SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
-
- assert_eq!(buf.len(), size);
- }
-
- #[test]
- fn encode_single_message_with_headers() {
- let stream_id = numeric_id(1);
- let topic_id = numeric_id(2);
- let partitioning = WirePartitioning::PartitionId(5);
- let payload = b"world";
- let headers = b"key:val";
- let messages = [RawMessage {
- id: 200,
- origin_timestamp: 1000,
- headers: Some(headers.as_slice()),
- payload: payload.as_slice(),
- }];
-
- let size =
- SendMessagesEncoder::encoded_size(&stream_id, &topic_id, &partitioning, &messages);
- let mut buf = BytesMut::with_capacity(size);
- SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
-
- assert_eq!(buf.len(), size);
- }
-
- #[test]
- fn encode_multiple_messages() {
- let stream_id = numeric_id(10);
- let topic_id = numeric_id(20);
- let partitioning = WirePartitioning::MessagesKey(b"user-1".to_vec());
let messages = [
RawMessage {
- id: 1,
- origin_timestamp: 100,
+ id: 100,
+ origin_timestamp: 1_000,
headers: None,
- payload: b"msg-1",
+ payload: b"first",
},
RawMessage {
- id: 2,
- origin_timestamp: 200,
- headers: Some(b"h2"),
- payload: b"msg-2",
- },
- RawMessage {
- id: 3,
- origin_timestamp: 300,
- headers: None,
- payload: b"msg-3",
+ id: 200,
+ origin_timestamp: 2_000,
+ headers: Some(b"hdr"),
+ payload: b"second",
},
];
- let size =
- SendMessagesEncoder::encoded_size(&stream_id, &topic_id, &partitioning, &messages);
- let mut buf = BytesMut::with_capacity(size);
- SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
+ let buf = encode(&stream_id, &topic_id, &partitioning, &messages);
- assert_eq!(buf.len(), size);
+ let metadata_len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
+ let (header, consumed) = SendMessagesHeader::decode(&buf[4..4 + metadata_len]).unwrap();
+ assert_eq!(consumed, metadata_len);
+ assert_eq!(header.messages_count, 2);
+
+ let batch = decode_batch_slice(&buf[4 + metadata_len..]).unwrap();
+ assert_eq!(batch.header.partition_id, 0);
+ assert_eq!(batch.header.base_offset, 0);
+ assert_eq!(batch.header.base_timestamp, 0);
+ assert_eq!(batch.header.origin_timestamp, 1_000);
+ assert_eq!(batch.header.total_size(), buf.len() - 4 - metadata_len);
+ assert_eq!(batch.message_count(), 2);
+
+ let views: Vec<_> = batch.iter().collect();
+ assert_eq!(views[0].header.id, 100);
+ assert_eq!(views[0].header.offset_delta, 0);
+ assert_eq!(views[0].header.timestamp_delta, 0);
+ assert_eq!(views[0].payload, b"first");
+ assert_eq!(views[0].user_headers, b"");
+ assert_eq!(views[1].header.id, 200);
+ assert_eq!(views[1].header.offset_delta, 1);
+ assert_eq!(views[1].header.timestamp_delta, 1_000);
+ assert_eq!(views[1].payload, b"second");
+ assert_eq!(views[1].user_headers, b"hdr");
}
#[test]
@@ -357,8 +323,7 @@
payload: b"x",
}];
- let mut buf = BytesMut::with_capacity(256);
- SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
+ let buf = encode(&stream_id, &topic_id, &partitioning, &messages);
let metadata_len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
let expected =
@@ -367,157 +332,50 @@
}
#[test]
- fn verify_index_entries() {
+ fn empty_payload_message() {
let stream_id = numeric_id(1);
- let topic_id = numeric_id(2);
+ let topic_id = numeric_id(1);
+ let partitioning = WirePartitioning::Balanced;
+ let messages = [RawMessage {
+ id: 7,
+ origin_timestamp: 0,
+ headers: None,
+ payload: b"",
+ }];
+
+ let buf = encode(&stream_id, &topic_id, &partitioning, &messages);
+ let metadata_len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
+ let batch = decode_batch_slice(&buf[4 + metadata_len..]).unwrap();
+ assert_eq!(batch.message_count(), 1);
+ }
+
+ #[test]
+ fn timestamp_delta_overflow_rejected() {
+ let stream_id = numeric_id(1);
+ let topic_id = numeric_id(1);
let partitioning = WirePartitioning::Balanced;
let messages = [
RawMessage {
id: 1,
origin_timestamp: 0,
headers: None,
- payload: b"aaaa", // wire_size = 64 + 4 = 68
+ payload: b"a",
},
RawMessage {
id: 2,
- origin_timestamp: 0,
- headers: Some(b"hh"), // wire_size = 64 + 3 + 2 = 69
- payload: b"bbb",
+ origin_timestamp: MAX_TIMESTAMP_DELTA_MICROS + 1,
+ headers: None,
+ payload: b"b",
},
];
- let mut buf = BytesMut::with_capacity(512);
- SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
-
- // Index starts after: 4 (metadata_len) + stream_id(6) + topic_id(6) + partitioning(2) + 4 (msg_count)
- let index_start = 4 + 6 + 6 + 2 + 4;
-
- // First index entry: cumulative = 68
- let first_zero = u32::from_le_bytes(buf[index_start..index_start + 4].try_into().unwrap());
- let first_cum =
- u32::from_le_bytes(buf[index_start + 4..index_start + 8].try_into().unwrap());
- assert_eq!(first_zero, 0);
- assert_eq!(first_cum, 68);
-
- // Second index entry: cumulative = 68 + 69 = 137
- let second_start = index_start + WIRE_MESSAGE_INDEX_SIZE;
- let second_zero =
- u32::from_le_bytes(buf[second_start..second_start + 4].try_into().unwrap());
- let second_cum =
- u32::from_le_bytes(buf[second_start + 4..second_start + 8].try_into().unwrap());
- assert_eq!(second_zero, 0);
- assert_eq!(second_cum, 137);
- }
-
- #[test]
- fn verify_message_header_layout() {
- let stream_id = numeric_id(1);
- let topic_id = numeric_id(2);
- let partitioning = WirePartitioning::Balanced;
- let messages = [RawMessage {
- id: 42,
- origin_timestamp: 777,
- headers: Some(b"hdr"),
- payload: b"pay",
- }];
-
- let mut buf = BytesMut::with_capacity(256);
- SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
-
- // Message data starts after: 4 + 6 + 6 + 2 + 4 + 16 (one index entry)
- let msg_start = 4 + 6 + 6 + 2 + 4 + 16;
- let msg = &buf[msg_start..];
-
- // Header layout matches WIRE_MESSAGE_HEADER_SIZE (64 bytes):
- // checksum(8) + id(16) + offset(8) + timestamp(8) + origin_ts(8) + hdrs_len(4) + payload_len(4) + reserved(8)
- let checksum = u64::from_le_bytes(msg[0..8].try_into().unwrap());
- assert_eq!(checksum, 0);
-
- let id = u128::from_le_bytes(msg[8..24].try_into().unwrap());
- assert_eq!(id, 42);
-
- let offset = u64::from_le_bytes(msg[24..32].try_into().unwrap());
- assert_eq!(offset, 0);
-
- let timestamp = u64::from_le_bytes(msg[32..40].try_into().unwrap());
- assert_eq!(timestamp, 0);
-
- let origin_ts = u64::from_le_bytes(msg[40..48].try_into().unwrap());
- assert_eq!(origin_ts, 777);
-
- let headers_len = u32::from_le_bytes(msg[48..52].try_into().unwrap());
- assert_eq!(headers_len, 3);
-
- let payload_len = u32::from_le_bytes(msg[52..56].try_into().unwrap());
- assert_eq!(payload_len, 3);
-
- let reserved = u64::from_le_bytes(msg[56..64].try_into().unwrap());
- assert_eq!(reserved, 0);
-
- // After the 64-byte header: payload then headers
- assert_eq!(&msg[64..67], b"pay");
- assert_eq!(&msg[67..70], b"hdr");
- }
-
- #[test]
- fn empty_payload_message() {
- let stream_id = numeric_id(1);
- let topic_id = numeric_id(1);
- let partitioning = WirePartitioning::Balanced;
- let messages = [RawMessage {
- id: 0,
- origin_timestamp: 0,
- headers: None,
- payload: b"",
- }];
-
- let size =
- SendMessagesEncoder::encoded_size(&stream_id, &topic_id, &partitioning, &messages);
- let mut buf = BytesMut::with_capacity(size);
- SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
- assert_eq!(buf.len(), size);
- }
-
- // -- RawMessage::encode_header --
-
- #[test]
- fn raw_message_encode_header_is_64_bytes() {
- let msg = RawMessage {
- id: 42,
- origin_timestamp: 999,
- headers: Some(b"hdr"),
- payload: b"pay",
- };
- let mut buf = BytesMut::with_capacity(WIRE_MESSAGE_HEADER_SIZE);
- msg.encode_header(&mut buf);
- assert_eq!(buf.len(), WIRE_MESSAGE_HEADER_SIZE);
- }
-
- #[test]
- fn raw_message_encode_header_field_values() {
- let msg = RawMessage {
- id: 0x1234_5678_9ABC_DEF0_1234_5678_9ABC_DEF0,
- origin_timestamp: 0xCAFE_BABE,
- headers: Some(b"hdr"),
- payload: b"pay",
- };
- let mut buf = BytesMut::with_capacity(WIRE_MESSAGE_HEADER_SIZE);
- msg.encode_header(&mut buf);
-
- assert_eq!(u64::from_le_bytes(buf[0..8].try_into().unwrap()), 0);
- assert_eq!(
- u128::from_le_bytes(buf[8..24].try_into().unwrap()),
- 0x1234_5678_9ABC_DEF0_1234_5678_9ABC_DEF0
- );
- assert_eq!(u64::from_le_bytes(buf[24..32].try_into().unwrap()), 0);
- assert_eq!(u64::from_le_bytes(buf[32..40].try_into().unwrap()), 0);
- assert_eq!(
- u64::from_le_bytes(buf[40..48].try_into().unwrap()),
- 0xCAFE_BABE
- );
- assert_eq!(u32::from_le_bytes(buf[48..52].try_into().unwrap()), 3);
- assert_eq!(u32::from_le_bytes(buf[52..56].try_into().unwrap()), 3);
- assert_eq!(u64::from_le_bytes(buf[56..64].try_into().unwrap()), 0);
+ let mut buf = BytesMut::new();
+ let result =
+ SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
+ assert!(matches!(
+ result,
+ Err(WireError::InvalidMessageTimestampDelta(_))
+ ));
}
// -- SendMessagesHeader --
@@ -580,171 +438,4 @@
assert_eq!(header.metadata_length(), 18);
assert_eq!(header.encoded_size(), 18);
}
-
- #[test]
- fn send_messages_header_cross_validate_with_encoder() {
- let stream_id = numeric_id(1);
- let topic_id = numeric_id(2);
- let partitioning = WirePartitioning::Balanced;
- let messages = [RawMessage {
- id: 42,
- origin_timestamp: 777,
- headers: Some(b"hdr"),
- payload: b"pay",
- }];
-
- let mut buf = BytesMut::with_capacity(256);
- SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
-
- let metadata_len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
- let (header, consumed) = SendMessagesHeader::decode(&buf[4..4 + metadata_len]).unwrap();
- assert_eq!(consumed, metadata_len);
- assert_eq!(header.stream_id, stream_id);
- assert_eq!(header.topic_id, topic_id);
- assert_eq!(header.partitioning, partitioning);
- assert_eq!(header.messages_count, 1);
- }
-
- // -- SendMessagesMetadataEncoder --
-
- #[test]
- fn metadata_encoder_header_size_matches_encoded() {
- let stream_id = numeric_id(1);
- let topic_id = numeric_id(2);
- let partitioning = WirePartitioning::Balanced;
- let messages = [
- RawMessage {
- id: 1,
- origin_timestamp: 100,
- headers: None,
- payload: b"msg-1",
- },
- RawMessage {
- id: 2,
- origin_timestamp: 200,
- headers: Some(b"h"),
- payload: b"msg-2",
- },
- ];
-
- let expected_size = SendMessagesMetadataEncoder::header_size(
- &stream_id,
- &topic_id,
- &partitioning,
- messages.len(),
- );
- let mut buf = BytesMut::with_capacity(expected_size);
- SendMessagesMetadataEncoder::encode_header(
- &mut buf,
- &stream_id,
- &topic_id,
- &partitioning,
- &messages,
- );
- assert_eq!(buf.len(), expected_size);
- }
-
- #[test]
- fn metadata_encoder_concat_matches_full_encoder() {
- let stream_id = numeric_id(1);
- let topic_id = numeric_id(2);
- let partitioning = WirePartitioning::Balanced;
- let messages = [
- RawMessage {
- id: 1,
- origin_timestamp: 100,
- headers: None,
- payload: b"aaa",
- },
- RawMessage {
- id: 2,
- origin_timestamp: 200,
- headers: Some(b"hh"),
- payload: b"bbb",
- },
- ];
-
- let full_size =
- SendMessagesEncoder::encoded_size(&stream_id, &topic_id, &partitioning, &messages);
- let mut full_buf = BytesMut::with_capacity(full_size);
- SendMessagesEncoder::encode(
- &mut full_buf,
- &stream_id,
- &topic_id,
- &partitioning,
- &messages,
- );
-
- let mut vec_buf = BytesMut::with_capacity(full_size);
- SendMessagesMetadataEncoder::encode_header(
- &mut vec_buf,
- &stream_id,
- &topic_id,
- &partitioning,
- &messages,
- );
- for msg in &messages {
- msg.encode_header(&mut vec_buf);
- vec_buf.put_slice(msg.payload);
- if let Some(headers) = msg.headers {
- vec_buf.put_slice(headers);
- }
- }
-
- assert_eq!(vec_buf.len(), full_buf.len());
- assert_eq!(&vec_buf[..], &full_buf[..]);
- }
-
- // -- Cross-validation: encoder -> header decoder -> iterator --
-
- #[test]
- fn cross_validation_encoder_to_iterator() {
- use crate::message_view::WireMessageIterator;
-
- let stream_id = WireIdentifier::numeric(1);
- let topic_id = WireIdentifier::numeric(2);
- let partitioning = WirePartitioning::Balanced;
- let messages = [
- RawMessage {
- id: 100,
- origin_timestamp: 1000,
- headers: None,
- payload: b"first",
- },
- RawMessage {
- id: 200,
- origin_timestamp: 2000,
- headers: Some(b"hdr"),
- payload: b"second",
- },
- ];
-
- let size =
- SendMessagesEncoder::encoded_size(&stream_id, &topic_id, &partitioning, &messages);
- let mut buf = BytesMut::with_capacity(size);
- SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
-
- let metadata_len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
- let (header, _) = SendMessagesHeader::decode(&buf[4..4 + metadata_len]).unwrap();
- assert_eq!(header.messages_count, 2);
-
- let data_offset =
- 4 + metadata_len + (header.messages_count as usize) * WIRE_MESSAGE_INDEX_SIZE;
- let message_data = &buf[data_offset..];
-
- let views: Vec<_> = WireMessageIterator::new(message_data, header.messages_count)
- .collect::<Result<Vec<_>, _>>()
- .unwrap();
- assert_eq!(views.len(), 2);
-
- assert_eq!(views[0].id(), 100);
- assert_eq!(views[0].origin_timestamp(), 1000);
- assert_eq!(views[0].payload(), b"first");
- assert_eq!(views[0].user_headers(), b"");
-
- assert_eq!(views[1].id(), 200);
- assert_eq!(views[1].origin_timestamp(), 2000);
- assert_eq!(views[1].payload(), b"second");
- assert_eq!(views[1].user_headers(), b"hdr");
- }
}
diff --git a/core/binary_protocol/src/responses/consumer_offsets/delete_consumer_offset_2.rs b/core/binary_protocol/src/responses/consumer_offsets/delete_consumer_offset_2.rs
deleted file mode 100644
index a145aab..0000000
--- a/core/binary_protocol/src/responses/consumer_offsets/delete_consumer_offset_2.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-// 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.
-
-/// `DeleteConsumerOffset2` response is empty.
-pub type DeleteConsumerOffset2Response = super::EmptyResponse;
diff --git a/core/binary_protocol/src/responses/consumer_offsets/mod.rs b/core/binary_protocol/src/responses/consumer_offsets/mod.rs
index 6dbaf45..915648c 100644
--- a/core/binary_protocol/src/responses/consumer_offsets/mod.rs
+++ b/core/binary_protocol/src/responses/consumer_offsets/mod.rs
@@ -16,14 +16,10 @@
// under the License.
mod delete_consumer_offset;
-mod delete_consumer_offset_2;
pub mod get_consumer_offset;
mod store_consumer_offset;
-mod store_consumer_offset_2;
pub use super::EmptyResponse;
pub use delete_consumer_offset::DeleteConsumerOffsetResponse;
-pub use delete_consumer_offset_2::DeleteConsumerOffset2Response;
pub use get_consumer_offset::ConsumerOffsetResponse;
pub use store_consumer_offset::StoreConsumerOffsetResponse;
-pub use store_consumer_offset_2::StoreConsumerOffset2Response;
diff --git a/core/binary_protocol/src/responses/consumer_offsets/store_consumer_offset_2.rs b/core/binary_protocol/src/responses/consumer_offsets/store_consumer_offset_2.rs
deleted file mode 100644
index 0f76f60..0000000
--- a/core/binary_protocol/src/responses/consumer_offsets/store_consumer_offset_2.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-// 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.
-
-/// `StoreConsumerOffset2` response is empty.
-pub type StoreConsumerOffset2Response = super::EmptyResponse;
diff --git a/core/binary_protocol/src/responses/messages/poll_messages.rs b/core/binary_protocol/src/responses/messages/poll_messages.rs
index cf31e44..ae7dce7 100644
--- a/core/binary_protocol/src/responses/messages/poll_messages.rs
+++ b/core/binary_protocol/src/responses/messages/poll_messages.rs
@@ -15,9 +15,18 @@
// specific language governing permissions and limitations
// under the License.
+//! Decoder for the `PollMessages` response.
+//!
+//! The body after the 16-byte prefix is a stream of canonical batch records
+//! (see [`crate::batch`]) served as stored: each record's header carries the
+//! stamped `base_offset` / `base_timestamp`, and each frame's deltas resolve
+//! against them. A record may be a server-sliced view of a larger stored
+//! batch, so `base_offset + offset_delta` of the first frame is the first
+//! polled offset, not necessarily `base_offset` itself.
+
+use crate::batch::{BatchHeader, BatchIntegrity, BatchIterator, decode_batch_slice_with};
use crate::codec::{WireDecode, WireEncode, read_u32_le, read_u64_le};
use crate::error::WireError;
-use crate::message_view::WireMessageIterator;
use bytes::{BufMut, BytesMut};
/// Size of the `PollMessages` response header: `partition_id(4) + current_offset(8) + count(4)`.
@@ -61,27 +70,120 @@
}
}
+/// One polled message with its deltas resolved to absolute values.
+#[derive(Debug, Clone, Copy)]
+pub struct PolledMessageView<'a> {
+ /// Stored per-message checksum, passed through as served.
+ pub checksum: u64,
+ pub id: u128,
+ pub offset: u64,
+ /// Broker append time: the flat batch `base_timestamp` (the per-message
+ /// delta applies to `origin_timestamp` only).
+ pub timestamp: u64,
+ pub origin_timestamp: u64,
+ pub payload: &'a [u8],
+ pub user_headers: &'a [u8],
+}
+
+/// Iterator over every message in a stream of served batch records.
+///
+/// Walks records by `batch_length` and flattens their frames. Each record's
+/// layout is proven up front (frames must tile `message_count` exactly), so
+/// framing errors, including frame-level corruption inside a record, surface
+/// as `Some(Err(_))` and end the iteration. Checksums are passed through
+/// unverified, as served.
+pub struct PolledBatchesIterator<'a> {
+ batches: &'a [u8],
+ position: usize,
+ current_header: Option<BatchHeader>,
+ frames: Option<BatchIterator<'a>>,
+ failed: bool,
+}
+
+impl<'a> PolledBatchesIterator<'a> {
+ #[must_use]
+ pub const fn new(batches: &'a [u8]) -> Self {
+ Self {
+ batches,
+ position: 0,
+ current_header: None,
+ frames: None,
+ failed: false,
+ }
+ }
+
+ fn advance_batch(&mut self) -> Result<bool, WireError> {
+ if self.position >= self.batches.len() {
+ return Ok(false);
+ }
+ // Layout-only decode proves the frames tile `message_count` exactly
+ // before any of them are yielded, so a corrupt record errors instead
+ // of silently truncating. Poll replies pass checksums through as
+ // served, so no integrity verification here.
+ let batch =
+ decode_batch_slice_with(&self.batches[self.position..], BatchIntegrity::LayoutOnly)?;
+ self.position += batch.header.total_size();
+ self.frames = Some(batch.iter());
+ self.current_header = Some(batch.header);
+ Ok(true)
+ }
+}
+
+impl<'a> Iterator for PolledBatchesIterator<'a> {
+ type Item = Result<PolledMessageView<'a>, WireError>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.failed {
+ return None;
+ }
+ loop {
+ if let Some(frames) = &mut self.frames
+ && let Some(view) = frames.next()
+ {
+ let header = self.current_header.expect("frames imply a current header");
+ return Some(Ok(PolledMessageView {
+ checksum: view.header.checksum,
+ id: view.header.id,
+ offset: header.base_offset + u64::from(view.header.offset_delta),
+ timestamp: header.base_timestamp,
+ origin_timestamp: header.origin_timestamp
+ + u64::from(view.header.timestamp_delta),
+ payload: view.payload,
+ user_headers: view.user_headers,
+ }));
+ }
+ match self.advance_batch() {
+ Ok(true) => {}
+ Ok(false) => return None,
+ Err(error) => {
+ self.failed = true;
+ return Some(Err(error));
+ }
+ }
+ }
+ }
+}
+
/// Borrowed `PollMessages` response. Does not own message data.
///
/// Does NOT implement `WireDecode` (trait returns owned data, we borrow).
/// Use [`PollMessagesResponse::decode`] instead.
pub struct PollMessagesResponse<'a> {
pub header: PollMessagesResponseHeader,
- pub messages: WireMessageIterator<'a>,
+ pub messages: PolledBatchesIterator<'a>,
}
impl<'a> PollMessagesResponse<'a> {
/// Decode from a response payload buffer. Borrows the buffer.
///
- /// Reads the 16-byte header then creates an iterator over the remaining
- /// message frames. Messages are validated lazily during iteration.
+ /// Reads the 16-byte header then creates an iterator over the batch
+ /// records that follow. Records are validated lazily during iteration.
///
/// # Errors
/// Returns `WireError` if the buffer is too short for the response header.
pub fn decode(buf: &'a [u8]) -> Result<Self, WireError> {
let (header, _) = PollMessagesResponseHeader::decode(buf)?;
- let messages =
- WireMessageIterator::new(&buf[POLL_RESPONSE_HEADER_SIZE..], header.messages_count);
+ let messages = PolledBatchesIterator::new(&buf[POLL_RESPONSE_HEADER_SIZE..]);
Ok(Self { header, messages })
}
}
@@ -89,88 +191,140 @@
#[cfg(test)]
mod tests {
use super::*;
- use crate::message_layout::{
- MSG_ID_OFFSET, MSG_ORIGIN_TIMESTAMP_OFFSET, MSG_PAYLOAD_LEN_OFFSET,
- WIRE_MESSAGE_HEADER_SIZE,
- };
+ use crate::batch::{BATCH_HEADER_SIZE, BATCH_MESSAGE_HEADER_SIZE, calculate_batch_checksum};
+ use twox_hash::XxHash3_64;
- fn make_frame(payload: &[u8], id: u128, origin_ts: u64) -> Vec<u8> {
- let total = WIRE_MESSAGE_HEADER_SIZE + payload.len();
- let mut frame = vec![0u8; total];
- frame[MSG_ID_OFFSET..MSG_ID_OFFSET + 16].copy_from_slice(&id.to_le_bytes());
- frame[MSG_ORIGIN_TIMESTAMP_OFFSET..MSG_ORIGIN_TIMESTAMP_OFFSET + 8]
- .copy_from_slice(&origin_ts.to_le_bytes());
- #[allow(clippy::cast_possible_truncation)]
- frame[MSG_PAYLOAD_LEN_OFFSET..MSG_PAYLOAD_LEN_OFFSET + 4]
- .copy_from_slice(&(payload.len() as u32).to_le_bytes());
- frame[WIRE_MESSAGE_HEADER_SIZE..].copy_from_slice(payload);
- frame
+ #[allow(clippy::cast_possible_truncation)]
+ fn frame(id: u128, offset_delta: u32, timestamp_delta: u32, payload: &[u8]) -> Vec<u8> {
+ let mut bytes = vec![0u8; BATCH_MESSAGE_HEADER_SIZE];
+ bytes[8..24].copy_from_slice(&id.to_le_bytes());
+ bytes[24..28].copy_from_slice(&offset_delta.to_le_bytes());
+ bytes[28..32].copy_from_slice(×tamp_delta.to_le_bytes());
+ bytes[36..40].copy_from_slice(&(payload.len() as u32).to_le_bytes());
+ bytes.extend_from_slice(payload);
+ let checksum = XxHash3_64::oneshot(&bytes[8..]);
+ bytes[0..8].copy_from_slice(&checksum.to_le_bytes());
+ bytes
+ }
+
+ #[allow(clippy::cast_possible_truncation)]
+ fn batch_record(
+ base_offset: u64,
+ base_timestamp: u64,
+ origin_timestamp: u64,
+ frames: &[Vec<u8>],
+ ) -> Vec<u8> {
+ let blob: Vec<u8> = frames.concat();
+ let mut header = BatchHeader::new(
+ 1,
+ origin_timestamp,
+ (BATCH_HEADER_SIZE + blob.len()) as u64,
+ frames.len() as u32,
+ );
+ header.base_offset = base_offset;
+ header.base_timestamp = base_timestamp;
+ header.batch_checksum = calculate_batch_checksum(&header, &blob);
+ let mut bytes = vec![0u8; BATCH_HEADER_SIZE];
+ header.encode_into(&mut bytes);
+ bytes.extend_from_slice(&blob);
+ bytes
+ }
+
+ fn response_body(current_offset: u64, count: u32, batches: &[Vec<u8>]) -> Vec<u8> {
+ let mut body = Vec::new();
+ body.extend_from_slice(&7u32.to_le_bytes());
+ body.extend_from_slice(¤t_offset.to_le_bytes());
+ body.extend_from_slice(&count.to_le_bytes());
+ for batch in batches {
+ body.extend_from_slice(batch);
+ }
+ body
}
#[test]
fn response_header_roundtrip() {
let header = PollMessagesResponseHeader {
- partition_id: 7,
- current_offset: 12345,
- messages_count: 3,
+ partition_id: 3,
+ current_offset: 42,
+ messages_count: 7,
};
let bytes = header.to_bytes();
- assert_eq!(bytes.len(), POLL_RESPONSE_HEADER_SIZE);
let (decoded, consumed) = PollMessagesResponseHeader::decode(&bytes).unwrap();
- assert_eq!(consumed, POLL_RESPONSE_HEADER_SIZE);
+ assert_eq!(consumed, bytes.len());
assert_eq!(decoded, header);
}
#[test]
fn response_header_truncation() {
- let header = PollMessagesResponseHeader {
- partition_id: 1,
- current_offset: 0,
- messages_count: 0,
- };
- let bytes = header.to_bytes();
- for i in 0..bytes.len() {
- assert!(
- PollMessagesResponseHeader::decode(&bytes[..i]).is_err(),
- "expected error for truncation at byte {i}"
- );
- }
+ let bytes = [0u8; POLL_RESPONSE_HEADER_SIZE - 1];
+ assert!(PollMessagesResponseHeader::decode(&bytes).is_err());
}
#[test]
- fn response_decode_with_messages() {
- let frame1 = make_frame(b"hello", 1, 100);
- let frame2 = make_frame(b"world", 2, 200);
+ fn decodes_messages_across_batches() {
+ let first = batch_record(
+ 100,
+ 5_000,
+ 1_000,
+ &[frame(11, 0, 0, b"a"), frame(12, 1, 10, b"b")],
+ );
+ let second = batch_record(102, 6_000, 2_000, &[frame(13, 0, 0, b"c")]);
+ let body = response_body(102, 3, &[first, second]);
- let mut buf = BytesMut::new();
- buf.put_u32_le(42);
- buf.put_u64_le(999);
- buf.put_u32_le(2);
- buf.put_slice(&frame1);
- buf.put_slice(&frame2);
+ let response = PollMessagesResponse::decode(&body).unwrap();
+ assert_eq!(response.header.partition_id, 7);
+ assert_eq!(response.header.current_offset, 102);
+ assert_eq!(response.header.messages_count, 3);
- let resp = PollMessagesResponse::decode(&buf).unwrap();
- assert_eq!(resp.header.partition_id, 42);
- assert_eq!(resp.header.current_offset, 999);
- assert_eq!(resp.header.messages_count, 2);
+ let messages: Vec<_> = response.messages.collect::<Result<Vec<_>, _>>().unwrap();
+ assert_eq!(messages.len(), 3);
- let views: Vec<_> = resp.messages.collect::<Result<Vec<_>, _>>().unwrap();
- assert_eq!(views.len(), 2);
- assert_eq!(views[0].id(), 1);
- assert_eq!(views[0].payload(), b"hello");
- assert_eq!(views[1].id(), 2);
- assert_eq!(views[1].payload(), b"world");
+ assert_eq!(messages[0].id, 11);
+ assert_eq!(messages[0].offset, 100);
+ assert_eq!(messages[0].timestamp, 5_000);
+ assert_eq!(messages[0].origin_timestamp, 1_000);
+ assert_eq!(messages[0].payload, b"a");
+
+ assert_eq!(messages[1].id, 12);
+ assert_eq!(messages[1].offset, 101);
+ assert_eq!(messages[1].timestamp, 5_000);
+ assert_eq!(messages[1].origin_timestamp, 1_010);
+ assert_eq!(messages[1].payload, b"b");
+
+ assert_eq!(messages[2].id, 13);
+ assert_eq!(messages[2].offset, 102);
+ assert_eq!(messages[2].timestamp, 6_000);
+ assert_eq!(messages[2].payload, b"c");
}
#[test]
- fn response_decode_zero_messages() {
- let mut buf = BytesMut::new();
- buf.put_u32_le(1);
- buf.put_u64_le(0);
- buf.put_u32_le(0);
+ fn sliced_record_resolves_leading_delta() {
+ // A server-sliced record keeps the stored base_offset; the first
+ // frame's delta positions it inside the original batch.
+ let record = batch_record(50, 9_000, 0, &[frame(1, 3, 0, b"tail")]);
+ let body = response_body(53, 1, &[record]);
- let resp = PollMessagesResponse::decode(&buf).unwrap();
- assert_eq!(resp.header.messages_count, 0);
- assert_eq!(resp.messages.count(), 0);
+ let response = PollMessagesResponse::decode(&body).unwrap();
+ let messages: Vec<_> = response.messages.collect::<Result<Vec<_>, _>>().unwrap();
+ assert_eq!(messages.len(), 1);
+ assert_eq!(messages[0].offset, 53);
+ }
+
+ #[test]
+ fn empty_body_yields_no_messages() {
+ let body = response_body(0, 0, &[]);
+ let response = PollMessagesResponse::decode(&body).unwrap();
+ assert_eq!(response.messages.count(), 0);
+ }
+
+ #[test]
+ fn truncated_record_surfaces_error() {
+ let record = batch_record(0, 0, 0, &[frame(1, 0, 0, b"payload")]);
+ let mut body = response_body(0, 1, &[record]);
+ body.truncate(body.len() - 1);
+
+ let response = PollMessagesResponse::decode(&body).unwrap();
+ let result: Result<Vec<_>, _> = response.messages.collect();
+ assert!(result.is_err());
}
}
diff --git a/core/binary_protocol/src/version.rs b/core/binary_protocol/src/version.rs
index 670c460..6d26df2 100644
--- a/core/binary_protocol/src/version.rs
+++ b/core/binary_protocol/src/version.rs
@@ -62,7 +62,7 @@
//!
//! `ClientVersionInfo` is the leading bytes of the login-register request
//! *body*, which itself rides inside a 256-byte VSR `RequestHeader` (see
-//! `consensus::header`): `command` = `Command2::Request`, `operation` =
+//! `consensus::header`): `command` = `Command::Request`, `operation` =
//! `Operation::Register`, client id in `RequestHeader.client`. The client
//! sends no group; the server derives it. A foreign SDK emits that header,
//! then the body starting with this prefix, to reach the gate.
diff --git a/core/cli/Cargo.toml b/core/cli/Cargo.toml
index 580ae57..1f9cb09 100644
--- a/core/cli/Cargo.toml
+++ b/core/cli/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy-cli"
-version = "0.14.0-edge.3"
+version = "0.14.0-edge.4"
edition = "2024"
rust-version.workspace = true
authors = ["bartosz.ciesla@gmail.com"]
diff --git a/core/common/Cargo.toml b/core/common/Cargo.toml
index 59dd949..1bb9328 100644
--- a/core/common/Cargo.toml
+++ b/core/common/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_common"
-version = "0.11.0-edge.3"
+version = "0.11.0-edge.4"
description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
edition = "2024"
rust-version.workspace = true
@@ -44,7 +44,6 @@
derive_more = { workspace = true }
humantime = { workspace = true }
iggy_binary_protocol = { workspace = true }
-lending-iterator = { workspace = true }
papaya = { workspace = true }
ring = { workspace = true }
secrecy = { workspace = true }
diff --git a/core/common/src/traits/binary_impls/consumer_offsets.rs b/core/common/src/traits/binary_impls/consumer_offsets.rs
index 1cde6f2..a5d95bd 100644
--- a/core/common/src/traits/binary_impls/consumer_offsets.rs
+++ b/core/common/src/traits/binary_impls/consumer_offsets.rs
@@ -20,6 +20,7 @@
use crate::{
BinaryClient, Consumer, ConsumerOffsetClient, ConsumerOffsetInfo, Identifier, IggyError,
};
+use iggy_binary_protocol::AckLevel;
use iggy_binary_protocol::codec::WireEncode;
use iggy_binary_protocol::codes::{
DELETE_CONSUMER_OFFSET_CODE, GET_CONSUMER_OFFSET_CODE, STORE_CONSUMER_OFFSET_CODE,
@@ -51,6 +52,7 @@
topic_id: wire_topic_id,
partition_id,
offset,
+ ack: AckLevel::Quorum,
}
.to_bytes(),
)
@@ -106,6 +108,7 @@
stream_id: wire_stream_id,
topic_id: wire_topic_id,
partition_id,
+ ack: AckLevel::Quorum,
}
.to_bytes(),
)
diff --git a/core/common/src/traits/binary_impls/messages.rs b/core/common/src/traits/binary_impls/messages.rs
index ab1f260..bf720a1 100644
--- a/core/common/src/traits/binary_impls/messages.rs
+++ b/core/common/src/traits/binary_impls/messages.rs
@@ -337,6 +337,14 @@
let wire_stream_id = identifier_to_wire(stream_id)?;
let wire_topic_id = identifier_to_wire(topic_id)?;
let wire_partitioning = partitioning_to_wire(partitioning)?;
+ // The producer owns message ids now that batches ride the wire
+ // verbatim: a zero id is minted here, before the frame checksum
+ // covers it.
+ for message in messages.iter_mut() {
+ if message.header.id == 0 {
+ message.header.id = crate::utils::random_id::get_uuid();
+ }
+ }
let raw_messages: Vec<RawMessage<'_>> = messages
.iter()
.map(|m| RawMessage {
@@ -359,7 +367,13 @@
&wire_topic_id,
&wire_partitioning,
&raw_messages,
- );
+ )
+ .map_err(|error| match error {
+ iggy_binary_protocol::WireError::InvalidMessageTimestampDelta(delta) => {
+ IggyError::InvalidMessageTimestampDelta(delta)
+ }
+ _ => IggyError::InvalidCommand,
+ })?;
let response = self
.send_raw_with_response(SEND_MESSAGES_CODE, buf.freeze())
.await?;
diff --git a/core/common/src/types/message/message_view_mut.rs b/core/common/src/types/message/message_view_mut.rs
deleted file mode 100644
index 574eb58..0000000
--- a/core/common/src/types/message/message_view_mut.rs
+++ /dev/null
@@ -1,117 +0,0 @@
-// 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.
-
-use crate::{
- IGGY_MESSAGE_HEADER_SIZE, IggyMessageHeaderView, calculate_checksum,
- types::message::message_header_view_mut::IggyMessageHeaderViewMut,
-};
-use lending_iterator::prelude::*;
-
-/// A mutable view of a message for in-place modifications
-#[derive(Debug)]
-pub struct IggyMessageViewMut<'a> {
- /// The buffer containing the message
- buffer: &'a mut [u8],
-}
-
-impl<'a> IggyMessageViewMut<'a> {
- /// Create a new mutable message view from a buffer
- pub fn new(buffer: &'a mut [u8]) -> Self {
- Self { buffer }
- }
-
- /// Get an immutable header view
- pub fn header(&self) -> IggyMessageHeaderView<'_> {
- let hdr_slice = &self.buffer[0..IGGY_MESSAGE_HEADER_SIZE];
- IggyMessageHeaderView::new(hdr_slice)
- }
-
- /// Get an ephemeral mutable header view for reading/writing
- pub fn header_mut(&mut self) -> IggyMessageHeaderViewMut<'_> {
- let hdr_slice = &mut self.buffer[0..IGGY_MESSAGE_HEADER_SIZE];
- IggyMessageHeaderViewMut::new(hdr_slice)
- }
-
- /// Returns the size of the entire message (header + payload + user headers).
- pub fn size(&self) -> usize {
- let hdr_view = self.header();
-
- IGGY_MESSAGE_HEADER_SIZE + hdr_view.payload_length() + hdr_view.user_headers_length()
- }
-
- /// Convenience method to update the checksum field in the header
- pub fn update_checksum(&mut self) {
- let checksum_field_size = size_of::<u64>(); // Skip checksum field for checksum calculation
- let size = self.size() - checksum_field_size;
- let data = &self.buffer[checksum_field_size..checksum_field_size + size];
- let checksum = calculate_checksum(data);
- self.header_mut().set_checksum(checksum);
- }
-}
-
-/// Iterator over mutable message views in a buffer
-pub struct IggyMessageViewMutIterator<'a> {
- buffer: &'a mut [u8],
- position: usize,
-}
-
-impl<'a> IggyMessageViewMutIterator<'a> {
- pub fn new(buffer: &'a mut [u8]) -> Self {
- Self {
- buffer,
- position: 0,
- }
- }
-}
-
-#[gat]
-impl LendingIterator for IggyMessageViewMutIterator<'_> {
- type Item<'next> = IggyMessageViewMut<'next>;
-
- fn next(&mut self) -> Option<Self::Item<'_>> {
- let buffer_len = self.buffer.len();
- if self.position >= buffer_len {
- return None;
- }
-
- if self.position + IGGY_MESSAGE_HEADER_SIZE > self.buffer.len() {
- tracing::error!(
- "Buffer too small for message header at position {}, buffer len: {}",
- self.position,
- self.buffer.len()
- );
- self.position = self.buffer.len();
- return None;
- }
-
- let buffer_slice = &mut self.buffer[self.position..];
- let view = IggyMessageViewMut::new(buffer_slice);
-
- let message_size = view.size();
- if message_size == 0 {
- tracing::error!(
- "Message size is 0 at position {}, preventing infinite loop",
- self.position
- );
- self.position = buffer_len;
- return None;
- }
-
- self.position += message_size;
- Some(view)
- }
-}
diff --git a/core/common/src/types/message/mod.rs b/core/common/src/types/message/mod.rs
index ccba3df..86f2360 100644
--- a/core/common/src/types/message/mod.rs
+++ b/core/common/src/types/message/mod.rs
@@ -24,7 +24,6 @@
mod message_header_view;
mod message_header_view_mut;
mod message_view;
-mod message_view_mut;
mod messages_batch;
pub mod partitioning;
pub mod partitioning_kind;
@@ -55,7 +54,6 @@
pub use message_header_view::IggyMessageHeaderView;
pub use message_header_view_mut::IggyMessageHeaderViewMut;
pub use message_view::{IggyMessageView, IggyMessageViewIterator};
-pub use message_view_mut::{IggyMessageViewMut, IggyMessageViewMutIterator};
pub use messages_batch::IggyMessagesBatch;
pub use partitioning::Partitioning;
pub use partitioning_kind::PartitioningKind;
diff --git a/core/common/src/types/message/polled_messages.rs b/core/common/src/types/message/polled_messages.rs
index 6c45d54..af23d4e 100644
--- a/core/common/src/types/message/polled_messages.rs
+++ b/core/common/src/types/message/polled_messages.rs
@@ -15,8 +15,9 @@
// specific language governing permissions and limitations
// under the License.
-use crate::{IGGY_MESSAGE_HEADER_SIZE, IggyMessage, IggyMessageHeader, error::IggyError};
+use crate::{IggyMessage, IggyMessageHeader, error::IggyError};
use bytes::Bytes;
+use iggy_binary_protocol::batch::{BATCH_HEADER_SIZE, BatchHeader, BatchMessageHeader};
use serde::{Deserialize, Serialize};
use tracing::error;
@@ -50,7 +51,17 @@
}
impl PolledMessages {
+ /// Decode a `PollMessages` response body: the 16-byte prefix followed by
+ /// the served batch records (`[256B batch header][frames]`, deltas
+ /// resolved against the stamped bases).
+ ///
+ /// # Errors
+ /// [`IggyError::InvalidNumberEncoding`] on a short prefix;
+ /// [`IggyError::InvalidMessagePayloadLength`] on a malformed record.
pub fn from_bytes(bytes: Bytes) -> Result<Self, IggyError> {
+ if bytes.len() < 16 {
+ return Err(IggyError::InvalidNumberEncoding);
+ }
let partition_id = u32::from_le_bytes(
bytes[0..4]
.try_into()
@@ -67,7 +78,7 @@
.map_err(|_| IggyError::InvalidNumberEncoding)?,
);
- let messages = messages_from_bytes_and_count(bytes.slice(16..), count)?;
+ let messages = messages_from_batches(bytes.slice(16..), count)?;
Ok(Self {
partition_id,
@@ -78,48 +89,61 @@
}
}
-/// Convert Bytes to messages
-fn messages_from_bytes_and_count(buffer: Bytes, count: u32) -> Result<Vec<IggyMessage>, IggyError> {
+/// Walk the served batch records, resolving each frame's deltas to absolute
+/// values. Payload and user-header `Bytes` are zero-copy slices of the
+/// response buffer.
+fn messages_from_batches(buffer: Bytes, count: u32) -> Result<Vec<IggyMessage>, IggyError> {
let mut messages = Vec::with_capacity(count as usize);
- let mut position = 0;
- let buf_len = buffer.len();
- while position < buf_len {
- if position + IGGY_MESSAGE_HEADER_SIZE > buf_len {
- break;
- }
- let header_bytes = buffer.slice(position..position + IGGY_MESSAGE_HEADER_SIZE);
- let header = match IggyMessageHeader::from_bytes(header_bytes) {
- Ok(h) => h,
- Err(e) => {
- error!("Failed to deserialize message header: {}", e);
- return Err(e);
+ let mut position = 0usize;
+ while position < buffer.len() {
+ let batch = BatchHeader::decode(&buffer[position..]).map_err(|decode_error| {
+ error!("Failed to decode polled batch header: {decode_error}");
+ IggyError::InvalidMessagePayloadLength
+ })?;
+ let batch_end = position
+ .checked_add(batch.total_size())
+ .filter(|end| *end <= buffer.len())
+ .ok_or(IggyError::InvalidMessagePayloadLength)?;
+ let mut cursor = position + BATCH_HEADER_SIZE;
+ while cursor < batch_end {
+ let frame =
+ BatchMessageHeader::decode(&buffer[cursor..batch_end]).map_err(|decode_error| {
+ error!("Failed to decode polled message frame: {decode_error}");
+ IggyError::InvalidMessagePayloadLength
+ })?;
+ let payload_start = cursor + iggy_binary_protocol::batch::BATCH_MESSAGE_HEADER_SIZE;
+ let payload_end = payload_start + frame.payload_length as usize;
+ let user_headers_end = payload_end + frame.user_headers_length as usize;
+ if user_headers_end > batch_end {
+ return Err(IggyError::InvalidMessagePayloadLength);
}
- };
- position += IGGY_MESSAGE_HEADER_SIZE;
- let payload_end = position + header.payload_length as usize;
- if payload_end > buf_len {
- break;
+ let header = IggyMessageHeader {
+ checksum: frame.checksum,
+ id: frame.id,
+ offset: batch.base_offset + u64::from(frame.offset_delta),
+ // Broker append time is stamped once per batch; the
+ // per-message delta applies to `origin_timestamp` only.
+ timestamp: batch.base_timestamp,
+ origin_timestamp: batch.origin_timestamp + u64::from(frame.timestamp_delta),
+ user_headers_length: frame.user_headers_length,
+ payload_length: frame.payload_length,
+ reserved: 0,
+ };
+ let payload = buffer.slice(payload_start..payload_end);
+ let user_headers = if frame.user_headers_length > 0 {
+ Some(buffer.slice(payload_end..user_headers_end))
+ } else {
+ None
+ };
+ messages.push(IggyMessage {
+ header,
+ payload,
+ user_headers,
+ });
+ cursor = user_headers_end;
}
- let payload = buffer.slice(position..payload_end);
- position = payload_end;
-
- let user_headers_end = position + header.user_headers_length as usize;
- if user_headers_end > buf_len {
- break;
- }
- let user_headers = if header.user_headers_length > 0 {
- Some(buffer.slice(position..user_headers_end))
- } else {
- None
- };
- position = user_headers_end;
-
- messages.push(IggyMessage {
- header,
- payload,
- user_headers,
- });
+ position = batch_end;
}
Ok(messages)
diff --git a/core/connectors/runtime/Cargo.toml b/core/connectors/runtime/Cargo.toml
index 448c1a4..dba184d 100644
--- a/core/connectors/runtime/Cargo.toml
+++ b/core/connectors/runtime/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy-connectors"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
description = "Connectors runtime for Iggy message streaming platform"
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sdk/Cargo.toml b/core/connectors/sdk/Cargo.toml
index bfd99e2..70d1919 100644
--- a/core/connectors/sdk/Cargo.toml
+++ b/core/connectors/sdk/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_sdk"
-version = "0.4.0-edge.2"
+version = "0.4.0-edge.3"
description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sinks/clickhouse_sink/Cargo.toml b/core/connectors/sinks/clickhouse_sink/Cargo.toml
index a624d1c..5ca6a29 100644
--- a/core/connectors/sinks/clickhouse_sink/Cargo.toml
+++ b/core/connectors/sinks/clickhouse_sink/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_clickhouse_sink"
-version = "0.2.0-edge.3"
+version = "0.2.0-edge.4"
description = "Iggy ClickHouse sink connector for streaming messages into ClickHouse"
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sinks/delta_sink/Cargo.toml b/core/connectors/sinks/delta_sink/Cargo.toml
index 29b836a..51a2e54 100644
--- a/core/connectors/sinks/delta_sink/Cargo.toml
+++ b/core/connectors/sinks/delta_sink/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_delta_sink"
-version = "0.2.0-edge.3"
+version = "0.2.0-edge.4"
description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sinks/doris_sink/Cargo.toml b/core/connectors/sinks/doris_sink/Cargo.toml
index a9f228a..ca031b5 100644
--- a/core/connectors/sinks/doris_sink/Cargo.toml
+++ b/core/connectors/sinks/doris_sink/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_doris_sink"
-version = "0.2.0-edge.3"
+version = "0.2.0-edge.4"
description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sinks/elasticsearch_sink/Cargo.toml b/core/connectors/sinks/elasticsearch_sink/Cargo.toml
index 3f104c0..8c83d51 100644
--- a/core/connectors/sinks/elasticsearch_sink/Cargo.toml
+++ b/core/connectors/sinks/elasticsearch_sink/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_elasticsearch_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
description = "Iggy Elasticsearch sink connector"
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sinks/http_sink/Cargo.toml b/core/connectors/sinks/http_sink/Cargo.toml
index 5ad0d8f..1ab4fc6 100644
--- a/core/connectors/sinks/http_sink/Cargo.toml
+++ b/core/connectors/sinks/http_sink/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_http_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
description = "Iggy HTTP sink connector for delivering stream messages to any HTTP endpoint via webhooks, REST APIs, or serverless functions."
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sinks/iceberg_sink/Cargo.toml b/core/connectors/sinks/iceberg_sink/Cargo.toml
index 14433a3..1d7f5d3 100644
--- a/core/connectors/sinks/iceberg_sink/Cargo.toml
+++ b/core/connectors/sinks/iceberg_sink/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_iceberg_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
edition = "2024"
license = "Apache-2.0"
keywords = ["iggy", "messaging", "streaming"]
diff --git a/core/connectors/sinks/influxdb_sink/Cargo.toml b/core/connectors/sinks/influxdb_sink/Cargo.toml
index 39e29c7..0b5d567 100644
--- a/core/connectors/sinks/influxdb_sink/Cargo.toml
+++ b/core/connectors/sinks/influxdb_sink/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_influxdb_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
description = "Iggy InfluxDB sink connector for storing stream messages as line protocol"
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sinks/meilisearch_sink/Cargo.toml b/core/connectors/sinks/meilisearch_sink/Cargo.toml
index a21988a..6a12ffb 100644
--- a/core/connectors/sinks/meilisearch_sink/Cargo.toml
+++ b/core/connectors/sinks/meilisearch_sink/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_meilisearch_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
description = "Iggy Meilisearch sink connector"
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sinks/mongodb_sink/Cargo.toml b/core/connectors/sinks/mongodb_sink/Cargo.toml
index bfe2b5d..f705c1d 100644
--- a/core/connectors/sinks/mongodb_sink/Cargo.toml
+++ b/core/connectors/sinks/mongodb_sink/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_mongodb_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
description = "Iggy MongoDB sink connector for storing stream messages into MongoDB database"
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sinks/postgres_sink/Cargo.toml b/core/connectors/sinks/postgres_sink/Cargo.toml
index 374911b..315cbed 100644
--- a/core/connectors/sinks/postgres_sink/Cargo.toml
+++ b/core/connectors/sinks/postgres_sink/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_postgres_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
description = "Iggy PostgreSQL sink connector for storing stream messages into PostgreSQL database"
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sinks/quickwit_sink/Cargo.toml b/core/connectors/sinks/quickwit_sink/Cargo.toml
index 3cca112..b571c6b 100644
--- a/core/connectors/sinks/quickwit_sink/Cargo.toml
+++ b/core/connectors/sinks/quickwit_sink/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_quickwit_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sinks/s3_sink/Cargo.toml b/core/connectors/sinks/s3_sink/Cargo.toml
index 1502439..caa128c 100644
--- a/core/connectors/sinks/s3_sink/Cargo.toml
+++ b/core/connectors/sinks/s3_sink/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_s3_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
description = "Iggy S3 sink connector for writing stream messages to Amazon S3 and S3-compatible stores"
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sinks/stdout_sink/Cargo.toml b/core/connectors/sinks/stdout_sink/Cargo.toml
index f79d5c4..864e776 100644
--- a/core/connectors/sinks/stdout_sink/Cargo.toml
+++ b/core/connectors/sinks/stdout_sink/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_stdout_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sinks/surrealdb_sink/Cargo.toml b/core/connectors/sinks/surrealdb_sink/Cargo.toml
index ed6a2d0..9169207 100644
--- a/core/connectors/sinks/surrealdb_sink/Cargo.toml
+++ b/core/connectors/sinks/surrealdb_sink/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_surrealdb_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
description = "Iggy SurrealDB sink connector for writing stream messages into SurrealDB"
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sources/elasticsearch_source/Cargo.toml b/core/connectors/sources/elasticsearch_source/Cargo.toml
index 75cddf6..be79d85 100644
--- a/core/connectors/sources/elasticsearch_source/Cargo.toml
+++ b/core/connectors/sources/elasticsearch_source/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_elasticsearch_source"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
description = "Iggy Elasticsearch source connector"
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sources/influxdb_source/Cargo.toml b/core/connectors/sources/influxdb_source/Cargo.toml
index 3da5c36..7efb4a8 100644
--- a/core/connectors/sources/influxdb_source/Cargo.toml
+++ b/core/connectors/sources/influxdb_source/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_influxdb_source"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
description = "Iggy InfluxDB source connector for polling Flux query results"
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sources/postgres_source/Cargo.toml b/core/connectors/sources/postgres_source/Cargo.toml
index 48453a8..6999acc 100644
--- a/core/connectors/sources/postgres_source/Cargo.toml
+++ b/core/connectors/sources/postgres_source/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_postgres_source"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
description = "Iggy PostgreSQL source connector supporting CDC and table polling for message streaming platform"
edition = "2024"
license = "Apache-2.0"
diff --git a/core/connectors/sources/random_source/Cargo.toml b/core/connectors/sources/random_source/Cargo.toml
index e3e07c1..ed18d54 100644
--- a/core/connectors/sources/random_source/Cargo.toml
+++ b/core/connectors/sources/random_source/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy_connector_random_source"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
edition = "2024"
license = "Apache-2.0"
diff --git a/core/consensus/src/client_table.rs b/core/consensus/src/client_table.rs
index c1efdd8..18c6d86 100644
--- a/core/consensus/src/client_table.rs
+++ b/core/consensus/src/client_table.rs
@@ -1239,7 +1239,7 @@
#[allow(clippy::cast_possible_truncation)]
mod tests {
use super::*;
- use iggy_binary_protocol::{Command2, Operation};
+ use iggy_binary_protocol::{Command, Operation};
/// Arbitrary non-zero user id for register fixtures; most tests don't
/// assert on it (see `register_stores_user_id` for the accessor check).
@@ -1259,7 +1259,7 @@
commit,
// Real size so codec-roundtripped replies re-parse.
size: header_size as u32,
- command: Command2::Reply,
+ command: Command::Reply,
operation: Operation::Register,
..ReplyHeader::default()
};
@@ -1290,7 +1290,7 @@
request_checksum,
// Real size so codec-roundtripped replies re-parse.
size: header_size as u32,
- command: Command2::Reply,
+ command: Command::Reply,
operation: Operation::SendMessages,
..ReplyHeader::default()
};
diff --git a/core/consensus/src/dvc_merge.rs b/core/consensus/src/dvc_merge.rs
index 19bda15..93387da 100644
--- a/core/consensus/src/dvc_merge.rs
+++ b/core/consensus/src/dvc_merge.rs
@@ -530,7 +530,7 @@
use super::*;
use crate::DVC_HEADERS_MAX;
use crate::view_change_quorum::{dvc_blank, dvc_quorum_array_empty, dvc_record};
- use iggy_binary_protocol::{Command2, Operation};
+ use iggy_binary_protocol::{Command, Operation};
/// Three replicas: replication 2, view-change 2, nack 2.
fn quorums_r3() -> MergeQuorums {
@@ -545,7 +545,7 @@
/// A prepare whose checksum derives from its op, so the hash chain connects.
fn prepare(op: u64, view: u32) -> PrepareHeader {
PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
operation: Operation::CreateStream,
op,
view,
diff --git a/core/consensus/src/impls.rs b/core/consensus/src/impls.rs
index e04aa0d..a481e34 100644
--- a/core/consensus/src/impls.rs
+++ b/core/consensus/src/impls.rs
@@ -27,7 +27,7 @@
use bit_set::BitSet;
use clock::{Clock, IggySystemClock};
use iggy_binary_protocol::{
- Command2, ConsensusHeader, DoViewChangeHeader, GenericHeader, PrepareHeader, PrepareOkHeader,
+ Command, ConsensusHeader, DoViewChangeHeader, GenericHeader, PrepareHeader, PrepareOkHeader,
ReplyHeader, RequestStartViewHeader, RoutedRequestHeader, StartViewChangeHeader,
StartViewHeader, frame_body,
};
@@ -3562,14 +3562,14 @@
/// Caller (`on_ack`) should validate `is_primary` and status before calling.
///
/// # Panics
- /// - If `header.command` is not `Command2::PrepareOk`.
+ /// - If `header.command` is not `Command::PrepareOk`.
/// - If `header.replica >= self.replica_count`.
pub fn handle_prepare_ok(
&self,
plane: PlaneKind,
header: &PrepareOkHeader,
) -> PrepareOkOutcome {
- assert_eq!(header.command, Command2::PrepareOk);
+ assert_eq!(header.command, Command::PrepareOk);
assert!(
header.replica < self.replica_count,
"handle_prepare_ok: invalid replica {}",
@@ -3772,7 +3772,7 @@
size: old.size,
view: consensus.view.get(),
release: old.release,
- command: Command2::Prepare,
+ command: Command::Prepare,
replica: consensus.replica,
client: old.client,
parent: consensus.last_prepare_checksum(),
@@ -3814,7 +3814,7 @@
fn project(self, consensus: &Self::Consensus) -> Message<PrepareOkHeader> {
self.transmute_header(|old, new| {
*new = PrepareOkHeader {
- command: Command2::PrepareOk,
+ command: Command::PrepareOk,
parent: old.parent,
prepare_checksum: old.checksum,
request: old.request,
@@ -3881,7 +3881,7 @@
#[cfg(test)]
mod request_queue_tests {
use super::*;
- use iggy_binary_protocol::{Command2, Operation};
+ use iggy_binary_protocol::{Command, Operation};
fn make_request(client: u128, request_num: u64) -> Message<RoutedRequestHeader> {
let header_size = std::mem::size_of::<RoutedRequestHeader>();
@@ -3891,7 +3891,7 @@
)
.expect("zeroed bytes are valid");
*header = RoutedRequestHeader {
- command: Command2::Request,
+ command: Command::Request,
client,
session: 1,
request: request_num,
@@ -4035,7 +4035,7 @@
//! subscriber `Canceled` even on happy path. Tests pin both halves.
use super::*;
- use iggy_binary_protocol::{Command2, ReplyHeader};
+ use iggy_binary_protocol::{Command, ReplyHeader};
use server_common::Message;
fn make_reply(client: u128, request: u64) -> Message<ReplyHeader> {
@@ -4046,7 +4046,7 @@
)
.expect("zeroed bytes are valid");
*header = ReplyHeader {
- command: Command2::Reply,
+ command: Command::Reply,
client,
request,
..ReplyHeader::default()
@@ -4126,7 +4126,7 @@
for op in 1..=depth as u64 {
let checksum = u128::from(op);
let header = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
size: std::mem::size_of::<PrepareHeader>() as u32,
op,
parent,
@@ -4259,7 +4259,7 @@
&mut msg.as_mut_slice()[..size],
)
.expect("zeroed bytes are a valid StartViewHeader");
- header.command = Command2::StartView;
+ header.command = Command::StartView;
header.cluster = 1;
header.view = view;
header.op = op;
@@ -4361,7 +4361,7 @@
&mut msg.as_mut_slice()[..header_size],
)
.expect("zeroed bytes are a valid RoutedRequestHeader");
- header.command = Command2::Request;
+ header.command = Command::Request;
header.client = 1;
header.request = 1;
header.operation = iggy_binary_protocol::Operation::SendMessages;
@@ -4703,7 +4703,7 @@
fn projected_prepare(op: u64, parent: u128) -> Message<PrepareHeader> {
Message::<PrepareHeader>::new(size_of::<PrepareHeader>()).transmute_header(|_, new| {
*new = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
size: size_of::<PrepareHeader>() as u32,
op,
parent,
@@ -4723,7 +4723,7 @@
) -> Message<PrepareHeader> {
Message::<PrepareHeader>::new(size_of::<PrepareHeader>()).transmute_header(|_, new| {
*new = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
size: size_of::<PrepareHeader>() as u32,
op,
parent,
@@ -4760,7 +4760,7 @@
Message::<RoutedRequestHeader>::new(size_of::<RoutedRequestHeader>()).transmute_header(
|_, new| {
*new = RoutedRequestHeader {
- command: Command2::Request,
+ command: Command::Request,
size: size_of::<RoutedRequestHeader>() as u32,
client,
session: 1,
diff --git a/core/consensus/src/metadata_helpers.rs b/core/consensus/src/metadata_helpers.rs
index 3dd5140..7202227 100644
--- a/core/consensus/src/metadata_helpers.rs
+++ b/core/consensus/src/metadata_helpers.rs
@@ -470,7 +470,7 @@
use super::*;
use crate::client_table::REGISTER_REQUEST_ID;
use crate::{CLIENTS_TABLE_MAX, LocalPipeline};
- use iggy_binary_protocol::{Command2, Operation, ReplyHeader};
+ use iggy_binary_protocol::{Command, Operation, ReplyHeader};
use message_bus::SendError;
/// Acting user for register fixtures; these tests exercise preflight /
@@ -579,7 +579,7 @@
let header =
bytemuck::checked::try_from_bytes::<EvictionHeader>(&frozen.as_slice()[..HEADER_SIZE])
.expect("valid EvictionHeader");
- assert_eq!(header.command, Command2::Eviction);
+ assert_eq!(header.command, Command::Eviction);
assert_eq!(header.reason, EvictionReason::NoSession);
assert_eq!(header.client, client_id);
}
@@ -908,7 +908,7 @@
cluster: consensus.cluster(),
size: header_size as u32,
view: consensus.view(),
- command: Command2::Reply,
+ command: Command::Reply,
replica: consensus.replica(),
client: client_id,
op: register_commit,
@@ -942,7 +942,7 @@
cluster: consensus.cluster(),
size: header_size as u32,
view: consensus.view(),
- command: Command2::Reply,
+ command: Command::Reply,
replica: consensus.replica(),
client: client_id,
op: commit,
diff --git a/core/consensus/src/observability.rs b/core/consensus/src/observability.rs
index e5c5832..4519693 100644
--- a/core/consensus/src/observability.rs
+++ b/core/consensus/src/observability.rs
@@ -671,8 +671,6 @@
Operation::SendMessages => "send_messages",
Operation::StoreConsumerOffset => "store_consumer_offset",
Operation::DeleteConsumerOffset => "delete_consumer_offset",
- Operation::StoreConsumerOffset2 => "store_consumer_offset_2",
- Operation::DeleteConsumerOffset2 => "delete_consumer_offset_2",
}
}
diff --git a/core/consensus/src/plane_helpers.rs b/core/consensus/src/plane_helpers.rs
index 344336b..1c576d0 100644
--- a/core/consensus/src/plane_helpers.rs
+++ b/core/consensus/src/plane_helpers.rs
@@ -20,7 +20,7 @@
Status, VsrConsensus,
};
use iggy_binary_protocol::{
- CHECKSUM_UNSEALED, Command2, ConsensusHeader, GenericHeader, PrepareHeader, PrepareOkHeader,
+ CHECKSUM_UNSEALED, Command, ConsensusHeader, GenericHeader, PrepareHeader, PrepareOkHeader,
ReplyHeader, RoutedRequestHeader, frame_body,
};
use message_bus::{MessageBus, SendError};
@@ -35,7 +35,7 @@
#[non_exhaustive]
pub enum ChainReplicationError {
MalformedPrepare,
- UnexpectedCommand { command: Command2 },
+ UnexpectedCommand { command: Command },
CommittedPrepare { op: u64, commit_min: u64 },
SelfRoute { replica: u8 },
Transport(SendError),
@@ -215,7 +215,7 @@
B: MessageBus,
P: Pipeline<Entry = PipelineEntry>,
{
- if header.command != Command2::Prepare {
+ if header.command != Command::Prepare {
return Err(ChainReplicationError::UnexpectedCommand {
command: header.command,
});
@@ -307,7 +307,7 @@
/// status, or the message's view differs from the replica's.
///
/// # Panics
-/// If `header.command` is not `Command2::Prepare`.
+/// If `header.command` is not `Command::Prepare`.
pub fn replicate_preflight<B, P>(
consensus: &VsrConsensus<B, P>,
header: &PrepareHeader,
@@ -316,7 +316,7 @@
B: MessageBus,
P: Pipeline<Entry = PipelineEntry>,
{
- assert_eq!(header.command, Command2::Prepare);
+ assert_eq!(header.command, Command::Prepare);
if consensus.is_transferring() {
return Err(IgnoreReason::StateTransfer);
@@ -544,7 +544,7 @@
// Commit-time view
view: prepare_header.view,
release: prepare_header.release,
- command: Command2::Reply,
+ command: Command::Reply,
// Original primary's id
replica: prepare_header.replica,
reserved_frame: [0; 66],
@@ -608,7 +608,7 @@
size: total_size as u32,
view: request_header.view,
release: request_header.release,
- command: Command2::Reply,
+ command: Command::Reply,
replica: request_header.replica,
request_checksum: request_header.request_checksum,
client: request_header.client,
@@ -662,7 +662,7 @@
size: total_size as u32,
view: consensus.view(),
release: 0,
- command: Command2::Reply,
+ command: Command::Reply,
replica: consensus.replica(),
reserved_frame: [0; 66],
request_checksum: request_header.request_checksum,
@@ -741,7 +741,7 @@
size: header_size as u32,
view: request_header.view,
release: request_header.release,
- command: Command2::Reply,
+ command: Command::Reply,
replica: request_header.replica,
request_checksum: request_header.request_checksum,
client: request_header.client,
@@ -777,7 +777,7 @@
// TODO: Figure out how to make this check the journal if it contains the prepare.
/// # Panics
-/// - If `header.command` is not `Command2::Prepare`.
+/// - If `header.command` is not `Command::Prepare`.
/// - If `header.view > consensus.view()`.
#[allow(clippy::cast_possible_truncation, clippy::future_not_send)]
pub async fn send_prepare_ok<B, P>(
@@ -788,7 +788,7 @@
B: MessageBus,
P: Pipeline<Entry = PipelineEntry>,
{
- assert_eq!(header.command, Command2::Prepare);
+ assert_eq!(header.command, Command::Prepare);
if consensus.status() != Status::Normal {
return;
@@ -814,7 +814,7 @@
}
let prepare_ok_header = PrepareOkHeader {
- command: Command2::PrepareOk,
+ command: Command::PrepareOk,
cluster: consensus.cluster(),
replica: consensus.replica(),
view: consensus.view(),
@@ -892,7 +892,7 @@
Message::<PrepareHeader>::new(std::mem::size_of::<PrepareHeader>()).transmute_header(
|_, new| {
*new = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
size: std::mem::size_of::<PrepareHeader>() as u32,
op,
parent,
@@ -914,7 +914,7 @@
Message::<PrepareHeader>::new(std::mem::size_of::<PrepareHeader>()).transmute_header(
|_, new| {
*new = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
size: std::mem::size_of::<PrepareHeader>() as u32,
op: 1,
view,
@@ -966,7 +966,7 @@
let svc =
Message::<StartViewChangeHeader>::new(std::mem::size_of::<StartViewChangeHeader>())
.transmute_header(|_, new: &mut StartViewChangeHeader| {
- new.command = Command2::StartViewChange;
+ new.command = Command::StartViewChange;
new.size = std::mem::size_of::<StartViewChangeHeader>() as u32;
new.view = 1;
new.replica = 0;
@@ -1006,7 +1006,7 @@
let svc =
Message::<StartViewChangeHeader>::new(std::mem::size_of::<StartViewChangeHeader>())
.transmute_header(|_, new: &mut StartViewChangeHeader| {
- new.command = Command2::StartViewChange;
+ new.command = Command::StartViewChange;
new.size = std::mem::size_of::<StartViewChangeHeader>() as u32;
new.view = 1;
new.replica = 0;
@@ -1036,7 +1036,7 @@
size: std::mem::size_of::<DoViewChangeHeader>() as u32,
view: 1,
release: 0,
- command: Command2::DoViewChange,
+ command: Command::DoViewChange,
replica: 1,
reserved_frame: [0; 66],
op: dvc_op,
@@ -1060,7 +1060,7 @@
// moved with it, one op would carry different checksums per receiving view
// and the merge would read them as competing prepares nacking each other.
let base = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
operation: iggy_binary_protocol::Operation::CreateStream,
op: 9,
view: 4,
@@ -1160,7 +1160,7 @@
// The distinction the merge depends on: two prepares at one op number are
// told apart, so a canonical header is distinguishable from a stale one.
let first = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
operation: iggy_binary_protocol::Operation::CreateStream,
op: 5,
client: 1,
@@ -1223,7 +1223,7 @@
consensus.init();
let prepare_header = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
cluster: 1,
view: 0,
op: 0,
@@ -1236,13 +1236,13 @@
let mut buf = Vec::new();
consensus.drain_loopback_into(&mut buf);
assert_eq!(buf.len(), 1);
- assert_eq!(buf[0].header().command, Command2::PrepareOk);
+ assert_eq!(buf[0].header().command, Command::PrepareOk);
let typed: Message<PrepareOkHeader> = buf
.remove(0)
.try_into_typed()
.expect("loopback message must be PrepareOk");
- assert_eq!(typed.header().command, Command2::PrepareOk);
+ assert_eq!(typed.header().command, Command::PrepareOk);
}
/// A sender's suffix and matching body bytes for a replica that holds every op
@@ -1291,7 +1291,7 @@
.expect("synthetic DVC frame fits u32"),
view,
release: 0,
- command: Command2::DoViewChange,
+ command: Command::DoViewChange,
replica,
reserved_frame: [0; 66],
op,
@@ -1319,7 +1319,7 @@
let mut ascending = Vec::new();
for op in low.max(1)..=high {
let mut header = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
operation: iggy_binary_protocol::Operation::CreateStream,
op,
view,
@@ -1349,7 +1349,7 @@
.expect("header fits u32"),
view,
release: 0,
- command: Command2::StartViewChange,
+ command: Command::StartViewChange,
replica,
reserved_frame: [0; 66],
group: 0,
@@ -1414,7 +1414,7 @@
fn given_a_sealed_prepare_when_verifying_integrity_should_accept() {
let message = Message::<PrepareHeader>::new(size_of::<PrepareHeader>()).transmute_header(
|_, header: &mut PrepareHeader| {
- header.command = Command2::Prepare;
+ header.command = Command::Prepare;
header.op = 7;
header.size = u32::try_from(size_of::<PrepareHeader>()).expect("header fits u32");
},
@@ -1429,7 +1429,7 @@
// corrupted in transit is journaled and then re-served to peers from the WAL.
let message = Message::<PrepareHeader>::new(size_of::<PrepareHeader>()).transmute_header(
|_, header: &mut PrepareHeader| {
- header.command = Command2::Prepare;
+ header.command = Command::Prepare;
header.op = 7;
header.size = u32::try_from(size_of::<PrepareHeader>()).expect("header fits u32");
},
@@ -1447,7 +1447,7 @@
// The partition plane leaves `checksum` at `CHECKSUM_UNSEALED` and carries a
// verified `batch_checksum` over the same bytes instead.
let header = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
op: 7,
..Default::default()
};
@@ -1470,7 +1470,7 @@
let header = bytemuck::checked::from_bytes_mut::<PrepareHeader>(
&mut bytes[..size_of::<PrepareHeader>()],
);
- header.command = Command2::Prepare;
+ header.command = Command::Prepare;
header.op = 7;
header.size = u32::try_from(size).expect("fits u32");
header.checksum_body = u128::from(calculate_checksum(body));
@@ -1642,7 +1642,7 @@
.expect("synthetic StartView fits u32"),
view,
release: 0,
- command: Command2::StartView,
+ command: Command::StartView,
replica,
reserved_frame: [0; 66],
op,
@@ -1870,7 +1870,7 @@
consensus.init();
let prepare_header = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
cluster: 1,
view: 0,
op: 0,
@@ -1995,8 +1995,8 @@
consensus.advance_commit_max(4);
let request = RoutedRequestHeader {
- command: Command2::Request,
- operation: Operation::DeleteConsumerOffset2,
+ command: Command::Request,
+ operation: Operation::DeleteConsumerOffset,
client: 42,
request: 7,
..Default::default()
@@ -2005,13 +2005,13 @@
let reply = build_deny_reply_from_request(&consensus, &request, status);
let header = reply.header();
- assert_eq!(header.command, Command2::Reply);
+ assert_eq!(header.command, Command::Reply);
assert_eq!(header.status, status);
assert_eq!(header.op, 0, "a deny commits nothing");
assert_eq!(header.commit, 4);
assert_eq!(header.client, 42);
assert_eq!(header.request, 7);
- assert_eq!(header.operation, Operation::DeleteConsumerOffset2);
+ assert_eq!(header.operation, Operation::DeleteConsumerOffset);
assert_eq!(
header.size as usize,
std::mem::size_of::<ReplyHeader>(),
diff --git a/core/consensus/src/state_manifest.rs b/core/consensus/src/state_manifest.rs
index be5a530..cd73bca 100644
--- a/core/consensus/src/state_manifest.rs
+++ b/core/consensus/src/state_manifest.rs
@@ -46,7 +46,7 @@
/// Metadata plane: [`crate::ClientTable::encode`] bytes.
pub const CLIENT_TABLE: u8 = 1;
/// Partition plane: one retained segment's `.log` bytes verbatim
- /// (prepare-stripped `SendMessages2` records); `frontier` = the
+ /// (prepare-stripped `SendMessages` records); `frontier` = the
/// segment's base offset.
pub const SEGMENT_LOG: u8 = 2;
/// Partition plane: the encoded consumer + consumer-group offset table
diff --git a/core/consensus/src/view_change_quorum.rs b/core/consensus/src/view_change_quorum.rs
index 95da481..89d5c26 100644
--- a/core/consensus/src/view_change_quorum.rs
+++ b/core/consensus/src/view_change_quorum.rs
@@ -17,7 +17,7 @@
use crate::REPLICAS_MAX;
use iggy_binary_protocol::{
- CHECKSUM_UNSEALED, Command2, ConsensusHeader, DVC_HEADERS_MAX, Operation, PrepareHeader,
+ CHECKSUM_UNSEALED, Command, ConsensusHeader, DVC_HEADERS_MAX, Operation, PrepareHeader,
};
/// Write prepare headers into a control-message body, high-to-low op.
@@ -50,7 +50,7 @@
#[must_use]
pub fn dvc_blank(op: u64) -> PrepareHeader {
PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
operation: Operation::Reserved,
op,
..Default::default()
diff --git a/core/integration/tests/cluster/client_table_restart.rs b/core/integration/tests/cluster/client_table_restart.rs
index 2d93d10..e2b87a5 100644
--- a/core/integration/tests/cluster/client_table_restart.rs
+++ b/core/integration/tests/cluster/client_table_restart.rs
@@ -75,7 +75,7 @@
use iggy::prelude::*;
use iggy_binary_protocol::codec::{WireDecode, WireEncode};
use iggy_binary_protocol::consensus::{
- Command2, Operation, ReplyHeader, RequestHeader, read_size_field, result_code,
+ Command, Operation, ReplyHeader, RequestHeader, read_size_field, result_code,
result_section_len,
};
use iggy_binary_protocol::requests::streams::CreateStreamRequest;
@@ -272,7 +272,7 @@
body_len: usize,
) -> RequestHeader {
RequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation,
size: u32::try_from(HEADER_SIZE + body_len).unwrap(),
client: CLIENT_ID,
@@ -572,14 +572,14 @@
}
let command_offset = offset_of!(RequestHeader, command);
- if reply_header[command_offset] == Command2::Eviction as u8 {
+ if reply_header[command_offset] == Command::Eviction as u8 {
return Exchange::Eviction {
reason: reply_header[HEADER_SIZE - 1],
};
}
assert_eq!(
reply_header[command_offset],
- Command2::Reply as u8,
+ Command::Reply as u8,
"expected a Reply frame"
);
diff --git a/core/integration/tests/sdk/protocol_version.rs b/core/integration/tests/sdk/protocol_version.rs
index 91bdb3b..3d82a7c 100644
--- a/core/integration/tests/sdk/protocol_version.rs
+++ b/core/integration/tests/sdk/protocol_version.rs
@@ -24,7 +24,7 @@
use iggy::prelude::*;
use iggy_binary_protocol::codec::WireEncode;
-use iggy_binary_protocol::consensus::{Command2, Operation, RequestHeader};
+use iggy_binary_protocol::consensus::{Command, Operation, RequestHeader};
use iggy_binary_protocol::requests::users::LoginRegisterRequest;
use iggy_binary_protocol::{
ClientVersionInfo, HEADER_SIZE, IGGY_PROTOCOL_VERSION, IGGY_PROTOCOL_VERSION_MIN, WireName,
@@ -79,7 +79,7 @@
expected_window: (u32, u32),
) {
let header = RequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::Register,
size: u32::try_from(HEADER_SIZE + body.len()).unwrap(),
client: 0xC0FFEE,
@@ -103,7 +103,7 @@
let command_offset = offset_of!(RequestHeader, command);
assert_eq!(
reply[command_offset],
- Command2::Eviction as u8,
+ Command::Eviction as u8,
"expected an Eviction frame"
);
assert_eq!(
diff --git a/core/integration/tests/server/legacy_login_vsr.rs b/core/integration/tests/server/legacy_login_vsr.rs
index 06ae510..98b685e 100644
--- a/core/integration/tests/server/legacy_login_vsr.rs
+++ b/core/integration/tests/server/legacy_login_vsr.rs
@@ -29,7 +29,7 @@
use iggy_binary_protocol::HEADER_SIZE;
use iggy_binary_protocol::codes::{LOGIN_USER_CODE, LOGIN_WITH_PERSONAL_ACCESS_TOKEN_CODE};
-use iggy_binary_protocol::consensus::{Command2, Operation, RequestHeader};
+use iggy_binary_protocol::consensus::{Command, Operation, RequestHeader};
use integration::harness::TestHarness;
use integration::iggy_harness;
use std::mem::offset_of;
@@ -61,7 +61,7 @@
/// exercises the same path a bound connection would.
async fn assert_legacy_login_code_evicted(harness: &TestHarness, code: u32) {
let mut header = RequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::NonReplicated,
size: u32::try_from(HEADER_SIZE).unwrap(),
// NonReplicated leaves session / request unchecked, but the header
@@ -93,7 +93,7 @@
let command_offset = offset_of!(RequestHeader, command);
assert_eq!(
reply[command_offset],
- Command2::Eviction as u8,
+ Command::Eviction as u8,
"expected an Eviction frame for legacy login code {code}, not a Reply"
);
assert_eq!(
diff --git a/core/integration/tests/server/scenarios/authentication_scenario.rs b/core/integration/tests/server/scenarios/authentication_scenario.rs
index 2736ea5..63a155f 100644
--- a/core/integration/tests/server/scenarios/authentication_scenario.rs
+++ b/core/integration/tests/server/scenarios/authentication_scenario.rs
@@ -144,17 +144,6 @@
) {
continue;
}
- // v2 consumer-offset ops are registered in the dispatch table for the
- // consensus/simulator pathway but are not wired into the legacy binary
- // server's dispatch. They'll move into the server alongside the rest of
- // the v2 surface; re-enable these codes here once that lands.
- if matches!(
- code,
- STORE_CONSUMER_OFFSET_2_CODE | DELETE_CONSUMER_OFFSET_2_CODE
- ) {
- continue;
- }
-
// ================================================================
// REQUIRES AUTH
// ================================================================
diff --git a/core/integration/tests/server/scenarios/purge_delete_scenario.rs b/core/integration/tests/server/scenarios/purge_delete_scenario.rs
index 79f1d29..a217e1f 100644
--- a/core/integration/tests/server/scenarios/purge_delete_scenario.rs
+++ b/core/integration/tests/server/scenarios/purge_delete_scenario.rs
@@ -37,11 +37,11 @@
/// volume carries what a 5 KiB segment used to.
const SEGMENT_SIZE: u64 = 1024 * 1024;
-/// The server persists the actual `SendMessages2` batch framing: a 256-byte
+/// The server persists the actual `SendMessages` batch framing: a 256-byte
/// command header per append (each send below is a single-message batch) plus
/// a 48-byte per-message header, and a 24-byte sparse index entry per flush
/// (one per message with messages_required_to_save = 1). See
-/// `server_common::send_messages2` and `stream_size_validation_scenario`.
+/// `server_common::send_messages` and `stream_size_validation_scenario`.
///
/// Sized so five messages seal a [`SEGMENT_SIZE`] segment and four do not:
/// 4 * 220304 = 881216 < 1 MiB <= 5 * 220304 = 1101520. Must stay a multiple
diff --git a/core/integration/tests/server/scenarios/stream_size_validation_scenario.rs b/core/integration/tests/server/scenarios/stream_size_validation_scenario.rs
index a7b76d4..8d6953a 100644
--- a/core/integration/tests/server/scenarios/stream_size_validation_scenario.rs
+++ b/core/integration/tests/server/scenarios/stream_size_validation_scenario.rs
@@ -38,10 +38,10 @@
const MESSAGE_PAYLOAD_SIZE_BYTES: u64 = 57;
const MSGS_COUNT: u64 = 117; // number of messages in a single topic after one pass of appending
// The server accounts the actual on-disk batch framing: one 256-byte
-// `SendMessages2` command header per append pass plus a 48-byte per-message
-// header (`server_common::send_messages2::{COMMAND_HEADER_SIZE,
-// MESSAGE_HEADER_SIZE}`). Each pass below sends all `MSGS_COUNT` messages in
-// one batch.
+// `SendMessages` command header per append pass plus a 48-byte per-message
+// header (`server_common::send_messages::COMMAND_HEADER_SIZE` and
+// `iggy_binary_protocol::batch::BATCH_MESSAGE_HEADER_SIZE`). Each pass below
+// sends all `MSGS_COUNT` messages in one batch.
const NG_BATCH_HEADER_SIZE: u64 = 256;
const NG_MESSAGE_HEADER_SIZE: u64 = 48;
const MSGS_SIZE: u64 =
diff --git a/core/journal/src/prepare_journal.rs b/core/journal/src/prepare_journal.rs
index f56a746..dc15e02 100644
--- a/core/journal/src/prepare_journal.rs
+++ b/core/journal/src/prepare_journal.rs
@@ -18,7 +18,7 @@
use crate::file_storage::FileStorage;
use crate::{Journal, JournalHandle};
use compio::io::AsyncWriteAtExt;
-use iggy_binary_protocol::consensus::{CHECKSUM_UNSEALED, Command2, PrepareHeader};
+use iggy_binary_protocol::consensus::{CHECKSUM_UNSEALED, Command, PrepareHeader};
use server_common::{MESSAGE_ALIGN, Message, iobuf::Owned};
use std::cell::{Cell, OnceCell, Ref, RefCell};
use std::fmt;
@@ -287,7 +287,7 @@
let last_start = want - HEADER_SIZE;
for offset in 0..=last_start {
- if buf[offset + COMMAND_OFFSET] != Command2::Prepare as u8 {
+ if buf[offset + COMMAND_OFFSET] != Command::Prepare as u8 {
continue;
}
let candidate = &buf[offset..offset + HEADER_SIZE];
@@ -310,7 +310,7 @@
fn valid_entry_header(scratch: &mut Owned<16>, bytes: &[u8]) -> Option<PrepareHeader> {
scratch.as_mut_slice().copy_from_slice(bytes);
let header = *bytemuck::checked::try_from_bytes::<PrepareHeader>(scratch.as_slice()).ok()?;
- if header.command != Command2::Prepare
+ if header.command != Command::Prepare
|| (header.size as usize) < HEADER_SIZE
|| u64::from(header.size) > MAX_ENTRY_SIZE
{
@@ -454,7 +454,7 @@
let header: PrepareHeader = *header_ref;
// Validate: must be a Prepare command with sane size
- if header.command != Command2::Prepare
+ if header.command != Command::Prepare
|| (header.size as usize) < HEADER_SIZE
|| u64::from(header.size) > MAX_ENTRY_SIZE
{
@@ -1206,7 +1206,7 @@
&mut buffer.as_mut_slice()[..HEADER_SIZE],
);
header.size = total_size as u32;
- header.command = Command2::Prepare;
+ header.command = Command::Prepare;
header.op = op;
header.operation = Operation::CreateStream;
header.checksum_body = checksum_body;
@@ -1699,7 +1699,7 @@
#[compio::test]
async fn corrupt_command_byte_truncates_on_reopen() {
- // Bit-flipped `Command2` discriminant: must truncate, not panic.
+ // Bit-flipped `Command` discriminant: must truncate, not panic.
let dir = tempdir().unwrap();
let path = dir.path().join("journal.wal");
@@ -1719,7 +1719,7 @@
use std::io::{Seek, SeekFrom, Write};
let mut file = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
file.seek(SeekFrom::Start(command_byte_offset)).unwrap();
- file.write_all(&[99u8]).unwrap(); // out of range for Command2
+ file.write_all(&[99u8]).unwrap(); // out of range for Command
file.sync_all().unwrap();
}
@@ -1810,7 +1810,7 @@
{
use std::io::Write;
let mut file = std::fs::File::create(&path).unwrap();
- // All-0xFF is not a valid `Command2`/`Operation` bit pattern,
+ // All-0xFF is not a valid `Command`/`Operation` bit pattern,
// so `try_from_bytes` rejects the header.
file.write_all(&[0xFF_u8; HEADER_SIZE]).unwrap();
// Sparse-extend past MAX_ENTRY_SIZE so the corruption at pos 0
@@ -1857,7 +1857,7 @@
use std::io::{Seek, SeekFrom, Write};
let mut file = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
file.seek(SeekFrom::Start(command_byte_offset)).unwrap();
- file.write_all(&[99u8]).unwrap(); // out of range for Command2
+ file.write_all(&[99u8]).unwrap(); // out of range for Command
file.sync_all().unwrap();
}
@@ -1890,7 +1890,7 @@
let header = bytemuck::checked::from_bytes_mut::<PrepareHeader>(
&mut buffer.as_mut_slice()[..HEADER_SIZE],
);
- header.command = Command2::Prepare;
+ header.command = Command::Prepare;
header.op = 1;
header.operation = Operation::CreateStream;
header.size = (HEADER_SIZE + 48) as u32; // 16 bytes of slack
diff --git a/core/message_bus/src/framing.rs b/core/message_bus/src/framing.rs
index 39ce24d..ab626c6 100644
--- a/core/message_bus/src/framing.rs
+++ b/core/message_bus/src/framing.rs
@@ -175,10 +175,10 @@
mod tests {
use super::*;
use compio::net::{TcpListener, TcpStream};
- use iggy_binary_protocol::{Command2, SIZE_FIELD_OFFSET};
+ use iggy_binary_protocol::{Command, SIZE_FIELD_OFFSET};
#[allow(clippy::cast_possible_truncation)]
- fn make_header_only(command: Command2) -> Message<GenericHeader> {
+ fn make_header_only(command: Command) -> Message<GenericHeader> {
Message::<GenericHeader>::new(HEADER_SIZE).transmute_header(|_, h: &mut GenericHeader| {
h.command = command;
h.size = HEADER_SIZE as u32;
@@ -200,10 +200,10 @@
#[allow(clippy::future_not_send)]
async fn write_then_read_header_only() {
let (mut a, mut b) = local_pair().await;
- let msg = make_header_only(Command2::Ping);
+ let msg = make_header_only(Command::Ping);
write_message(&mut a, msg).await.unwrap();
let read = read_message(&mut b, MAX_MESSAGE_SIZE).await.unwrap();
- assert_eq!(read.header().command, Command2::Ping);
+ assert_eq!(read.header().command, Command::Ping);
assert_eq!(read.header().size as usize, HEADER_SIZE);
}
diff --git a/core/message_bus/src/installer/tcp.rs b/core/message_bus/src/installer/tcp.rs
index 3530d14..12cfffb 100644
--- a/core/message_bus/src/installer/tcp.rs
+++ b/core/message_bus/src/installer/tcp.rs
@@ -30,7 +30,7 @@
use async_channel::Receiver;
use compio::net::TcpStream;
use futures::FutureExt;
-use iggy_binary_protocol::{Command2, GenericHeader};
+use iggy_binary_protocol::{Command, GenericHeader};
use server_common::Message;
use std::cell::Cell;
use std::rc::Rc;
@@ -275,7 +275,7 @@
return;
}
let cmd = msg.header().command;
- if cmd != Command2::Request {
+ if cmd != Command::Request {
warn!(
client = client_id,
?cmd,
diff --git a/core/message_bus/src/lib.rs b/core/message_bus/src/lib.rs
index 6445911..414312a 100644
--- a/core/message_bus/src/lib.rs
+++ b/core/message_bus/src/lib.rs
@@ -1418,7 +1418,7 @@
}
/// Reserved client id stamped on server-generated auto-commit
-/// `StoreConsumerOffset2` ops (a poll's `auto_commit` replicated for failover).
+/// `StoreConsumerOffset` ops (a poll's `auto_commit` replicated for failover).
///
/// Never belongs to a live connection: `mint_client_id` produces
/// `(shard << 112) | seq` and no real shard is `u16::MAX`, so `u128::MAX` is
@@ -1463,13 +1463,13 @@
#[cfg(test)]
mod tests {
use super::*;
- use iggy_binary_protocol::{Command2, HEADER_SIZE};
+ use iggy_binary_protocol::{Command, HEADER_SIZE};
use std::cell::RefCell;
#[allow(clippy::cast_possible_truncation)]
fn dummy_message() -> Message<GenericHeader> {
Message::<GenericHeader>::new(HEADER_SIZE).transmute_header(|_, h: &mut GenericHeader| {
- h.command = Command2::Prepare;
+ h.command = Command::Prepare;
h.size = HEADER_SIZE as u32;
})
}
@@ -1513,7 +1513,7 @@
#[allow(clippy::cast_possible_truncation)]
fn reply_message(request: u64) -> Message<ReplyHeader> {
Message::<ReplyHeader>::new(HEADER_SIZE).transmute_header(|_, h: &mut ReplyHeader| {
- h.command = Command2::Reply;
+ h.command = Command::Reply;
h.size = HEADER_SIZE as u32;
h.request = request;
})
diff --git a/core/message_bus/src/lifecycle/connection_registry.rs b/core/message_bus/src/lifecycle/connection_registry.rs
index 8dc22ad..cda45a8 100644
--- a/core/message_bus/src/lifecycle/connection_registry.rs
+++ b/core/message_bus/src/lifecycle/connection_registry.rs
@@ -949,14 +949,14 @@
mod tests {
use super::*;
use crate::lifecycle::Shutdown;
- use iggy_binary_protocol::{Command2, GenericHeader, HEADER_SIZE};
+ use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE};
use server_common::Message;
#[allow(clippy::cast_possible_truncation)]
fn make_bus_msg() -> BusMessage {
Message::<GenericHeader>::new(HEADER_SIZE)
.transmute_header(|_, h: &mut GenericHeader| {
- h.command = Command2::Ping;
+ h.command = Command::Ping;
h.size = HEADER_SIZE as u32;
})
.into_frozen()
diff --git a/core/message_bus/src/replica/auth.rs b/core/message_bus/src/replica/auth.rs
index b455f69..3b46138 100644
--- a/core/message_bus/src/replica/auth.rs
+++ b/core/message_bus/src/replica/auth.rs
@@ -22,7 +22,7 @@
//!
//! The handshake is a 3-message mutual challenge-response riding the
//! already-zeroed `reserved_command` bytes of the 256-byte `GenericHeader`.
-//! Each message has its own `Command2` discriminant (no Ping/Pong reuse):
+//! Each message has its own `Command` discriminant (no Ping/Pong reuse):
//!
//! 1. `ReplicaHello` dialer -> acceptor: `nonce_d` (no MAC; the dialer has
//! no acceptor nonce yet);
diff --git a/core/message_bus/src/replica/handshake.rs b/core/message_bus/src/replica/handshake.rs
index 2267666..13d81ce 100644
--- a/core/message_bus/src/replica/handshake.rs
+++ b/core/message_bus/src/replica/handshake.rs
@@ -65,7 +65,7 @@
use crate::replica::auth::{self, ChannelBinding, HandshakeStatus, ReplicaAuth, Transcript};
use crate::{GenericHeader, Message};
use compio::io::{AsyncRead, AsyncWrite};
-use iggy_binary_protocol::{Command2, HEADER_SIZE};
+use iggy_binary_protocol::{Command, HEADER_SIZE};
use iggy_common::IggyError;
use rustls::pki_types::ServerName;
use std::collections::HashMap;
@@ -154,7 +154,7 @@
// fd without reading, so a reject frame would land in its VSR reader instead.
let nackable = auth.is_some() && has_nonce;
- if header.command != Command2::ReplicaHello {
+ if header.command != Command::ReplicaHello {
return reject(
stream,
our_cluster,
@@ -238,7 +238,7 @@
// reject here is log-only (no frame).
// Check the command before the MAC: the finish frame is identified by its
// own discriminant, not by handshake position.
- if finish.header().command != Command2::ReplicaFinish {
+ if finish.header().command != Command::ReplicaFinish {
return reject(
stream,
our_cluster,
@@ -320,7 +320,7 @@
return Err(());
}
};
- if challenge.header().command != Command2::ReplicaChallenge {
+ if challenge.header().command != Command::ReplicaChallenge {
warn!(
replica = peer_id,
command = ?challenge.header().command,
@@ -415,7 +415,7 @@
#[allow(clippy::cast_possible_truncation)]
Message::<GenericHeader>::new(size_of::<GenericHeader>()).transmute_header(
|_, h: &mut GenericHeader| {
- h.command = Command2::ReplicaChallenge;
+ h.command = Command::ReplicaChallenge;
h.cluster = cluster_id;
h.replica = replica_id;
h.size = HEADER_SIZE as u32;
@@ -441,7 +441,7 @@
#[allow(clippy::cast_possible_truncation)]
Message::<GenericHeader>::new(size_of::<GenericHeader>()).transmute_header(
|_, h: &mut GenericHeader| {
- h.command = Command2::ReplicaHello;
+ h.command = Command::ReplicaHello;
h.cluster = cluster_id;
h.replica = replica_id;
h.size = HEADER_SIZE as u32;
@@ -462,7 +462,7 @@
#[allow(clippy::cast_possible_truncation)]
Message::<GenericHeader>::new(size_of::<GenericHeader>()).transmute_header(
|_, h: &mut GenericHeader| {
- h.command = Command2::ReplicaFinish;
+ h.command = Command::ReplicaFinish;
h.cluster = cluster_id;
h.replica = replica_id;
h.size = HEADER_SIZE as u32;
diff --git a/core/message_bus/src/transports/quic.rs b/core/message_bus/src/transports/quic.rs
index 728c632..26550d5 100644
--- a/core/message_bus/src/transports/quic.rs
+++ b/core/message_bus/src/transports/quic.rs
@@ -90,7 +90,7 @@
crypto::rustls::QuicServerConfig,
};
use futures::FutureExt;
-use iggy_binary_protocol::{Command2, HEADER_SIZE, ReplyHeader, RequestHeader};
+use iggy_binary_protocol::{Command, HEADER_SIZE, ReplyHeader, RequestHeader};
use std::io;
use std::net::SocketAddr;
use std::sync::Arc;
@@ -300,7 +300,7 @@
/// Closing the connection drops the mailbox and any late reply with it.
const REPLY_WAIT_BACKSTOP: Duration = Duration::from_mins(1);
-/// `Command2` byte offset shared by every consensus header (after the
+/// `Command` byte offset shared by every consensus header (after the
/// fixed checksum/cluster/size/view/release prefix).
const COMMAND_OFFSET: usize = 60;
@@ -316,13 +316,13 @@
Unkeyed,
}
-/// Decide how to route an outbound frame. Only `Command2::Reply` carries a
+/// Decide how to route an outbound frame. Only `Command::Reply` carries a
/// `request` id; everything else must pass through unfiltered.
fn reply_route(frame: &BusMessage) -> ReplyRoute {
let bytes = frame.as_slice();
let is_reply = bytes
.get(COMMAND_OFFSET)
- .is_some_and(|&command| command == Command2::Reply as u8);
+ .is_some_and(|&command| command == Command::Reply as u8);
if !is_reply {
return ReplyRoute::Unkeyed;
}
@@ -554,7 +554,7 @@
use async_channel::bounded;
use compio::io::AsyncWrite;
use compio_quic::ClientBuilder;
- use iggy_binary_protocol::{Command2, GenericHeader, HEADER_SIZE, SIZE_FIELD_OFFSET};
+ use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE, SIZE_FIELD_OFFSET};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use server_common::iobuf::Frozen;
use server_common::{MESSAGE_ALIGN, Message};
@@ -566,7 +566,7 @@
let _ = rustls::crypto::ring::default_provider().install_default();
}
- fn header_only(command: Command2) -> Frozen<MESSAGE_ALIGN> {
+ fn header_only(command: Command) -> Frozen<MESSAGE_ALIGN> {
#[allow(clippy::cast_possible_truncation)]
Message::<GenericHeader>::new(HEADER_SIZE)
.transmute_header(|_, h: &mut GenericHeader| {
@@ -654,11 +654,11 @@
// and then awaits exactly one reply on `rx` before
// accepting the next bidi. Feed three replies in step.
let a = in_rx.recv().await.unwrap();
- out_tx.send(header_only(Command2::Reply)).await.unwrap();
+ out_tx.send(header_only(Command::Reply)).await.unwrap();
let b = in_rx.recv().await.unwrap();
- out_tx.send(header_only(Command2::Reply)).await.unwrap();
+ out_tx.send(header_only(Command::Reply)).await.unwrap();
let c = in_rx.recv().await.unwrap();
- out_tx.send(header_only(Command2::Reply)).await.unwrap();
+ out_tx.send(header_only(Command::Reply)).await.unwrap();
shutdown.trigger();
let _ = handle.await;
(a.header().command, b.header().command, c.header().command)
@@ -671,7 +671,7 @@
let connection = connecting.await.expect("client handshake");
// Three sequential bidis, one frame each.
- for command in [Command2::Ping, Command2::Prepare, Command2::Request] {
+ for command in [Command::Ping, Command::Prepare, Command::Request] {
let (mut send, _recv) = connection.open_bi_wait().await.expect("open_bi");
let BufResult(result, _) = send.write_all(header_only(command)).await;
result.expect("write");
@@ -682,9 +682,9 @@
.await
.expect("server task within 5s")
.unwrap();
- assert_eq!(a, Command2::Ping);
- assert_eq!(b, Command2::Prepare);
- assert_eq!(c, Command2::Request);
+ assert_eq!(a, Command::Ping);
+ assert_eq!(b, Command::Prepare);
+ assert_eq!(c, Command::Request);
}
/// An oversize size field in the request header makes
diff --git a/core/message_bus/src/transports/tcp.rs b/core/message_bus/src/transports/tcp.rs
index 340d587..87a3103 100644
--- a/core/message_bus/src/transports/tcp.rs
+++ b/core/message_bus/src/transports/tcp.rs
@@ -294,14 +294,14 @@
use super::*;
use crate::lifecycle::Shutdown;
use async_channel::bounded;
- use iggy_binary_protocol::{Command2, GenericHeader, HEADER_SIZE, SIZE_FIELD_OFFSET};
+ use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE, SIZE_FIELD_OFFSET};
use server_common::MESSAGE_ALIGN;
use server_common::Message;
use server_common::iobuf::Frozen;
use std::time::Duration;
#[allow(clippy::cast_possible_truncation)]
- fn header_only(command: Command2) -> Frozen<MESSAGE_ALIGN> {
+ fn header_only(command: Command) -> Frozen<MESSAGE_ALIGN> {
Message::<GenericHeader>::new(HEADER_SIZE)
.transmute_header(|_, h: &mut GenericHeader| {
h.command = command;
@@ -369,7 +369,7 @@
let (_server_out, server_in, server_shutdown, server_handle) =
drive(TcpTransportConn::new(server));
- for cmd in [Command2::Ping, Command2::Prepare, Command2::Request] {
+ for cmd in [Command::Ping, Command::Prepare, Command::Request] {
client_out.send(header_only(cmd)).await.unwrap();
}
@@ -385,9 +385,9 @@
let a = recv_with_timeout(&server_in).await;
let b = recv_with_timeout(&server_in).await;
let c = recv_with_timeout(&server_in).await;
- assert_eq!(a.header().command, Command2::Ping);
- assert_eq!(b.header().command, Command2::Prepare);
- assert_eq!(c.header().command, Command2::Request);
+ assert_eq!(a.header().command, Command::Ping);
+ assert_eq!(b.header().command, Command::Prepare);
+ assert_eq!(c.header().command, Command::Request);
client_shutdown.trigger();
server_shutdown.trigger();
diff --git a/core/message_bus/src/transports/tcp_tls.rs b/core/message_bus/src/transports/tcp_tls.rs
index 7c699d9..a248fb8 100644
--- a/core/message_bus/src/transports/tcp_tls.rs
+++ b/core/message_bus/src/transports/tcp_tls.rs
@@ -657,7 +657,7 @@
use crate::transports::tls::{install_default_crypto_provider, self_signed_for_loopback};
use async_channel::{Receiver, Sender, bounded};
use compio::net::TcpListener;
- use iggy_binary_protocol::{Command2, GenericHeader, HEADER_SIZE};
+ use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE};
use rustls::RootCertStore;
use server_common::{MESSAGE_ALIGN, Message, iobuf::Frozen};
use std::net::SocketAddr;
@@ -719,7 +719,7 @@
}
#[allow(clippy::cast_possible_truncation)]
- fn header_only(command: Command2) -> Frozen<MESSAGE_ALIGN> {
+ fn header_only(command: Command) -> Frozen<MESSAGE_ALIGN> {
Message::<GenericHeader>::new(HEADER_SIZE)
.transmute_header(|_, h: &mut GenericHeader| {
h.command = command;
@@ -729,7 +729,7 @@
}
#[allow(clippy::cast_possible_truncation)]
- fn padded(command: Command2, total_size: usize) -> Frozen<MESSAGE_ALIGN> {
+ fn padded(command: Command, total_size: usize) -> Frozen<MESSAGE_ALIGN> {
Message::<GenericHeader>::new(total_size)
.transmute_header(|_, h: &mut GenericHeader| {
h.command = command;
@@ -780,24 +780,24 @@
let (client_out, client_in, client_shutdown, client_handle) = drive(client_conn);
client_out
- .send(header_only(Command2::Request))
+ .send(header_only(Command::Request))
.await
.expect("client send");
let received = compio::time::timeout(Duration::from_secs(5), server_in.recv())
.await
.expect("server recv within 5 s")
.expect("server frame");
- assert_eq!(received.header().command, Command2::Request);
+ assert_eq!(received.header().command, Command::Request);
server_out
- .send(header_only(Command2::Reply))
+ .send(header_only(Command::Reply))
.await
.expect("server send");
let reply = compio::time::timeout(Duration::from_secs(5), client_in.recv())
.await
.expect("client recv within 5 s")
.expect("client frame");
- assert_eq!(reply.header().command, Command2::Reply);
+ assert_eq!(reply.header().command, Command::Reply);
server_shutdown.trigger();
client_shutdown.trigger();
@@ -854,14 +854,14 @@
let (client_out, _client_in, client_shutdown, client_handle) = drive(client_conn);
client_out
- .send(padded(Command2::Request, total))
+ .send(padded(Command::Request, total))
.await
.expect("client send 1 MiB");
let received = compio::time::timeout(Duration::from_secs(10), server_in.recv())
.await
.expect("server recv within 10 s")
.expect("server frame");
- assert_eq!(received.header().command, Command2::Request);
+ assert_eq!(received.header().command, Command::Request);
assert_eq!(received.header().size as usize, total);
server_shutdown.trigger();
@@ -924,14 +924,14 @@
let (client_out, _client_in, client_shutdown, client_handle) = drive(client_conn);
client_out
- .send(header_only(Command2::Request))
+ .send(header_only(Command::Request))
.await
.expect("client send");
let received = compio::time::timeout(Duration::from_secs(5), server_in.recv())
.await
.expect("server recv within 5 s")
.expect("server frame");
- assert_eq!(received.header().command, Command2::Request);
+ assert_eq!(received.header().command, Command::Request);
server_shutdown.trigger();
client_shutdown.trigger();
diff --git a/core/message_bus/src/transports/ws.rs b/core/message_bus/src/transports/ws.rs
index e7b6253..f985191 100644
--- a/core/message_bus/src/transports/ws.rs
+++ b/core/message_bus/src/transports/ws.rs
@@ -316,13 +316,13 @@
use crate::lifecycle::Shutdown;
use async_channel::{Receiver, Sender, bounded};
use compio::net::TcpListener;
- use iggy_binary_protocol::{Command2, GenericHeader, HEADER_SIZE};
+ use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE};
use server_common::Message;
use server_common::iobuf::Frozen;
use std::time::Duration;
#[allow(clippy::cast_possible_truncation)]
- fn header_only(command: Command2) -> Frozen<MESSAGE_ALIGN> {
+ fn header_only(command: Command) -> Frozen<MESSAGE_ALIGN> {
Message::<GenericHeader>::new(HEADER_SIZE)
.transmute_header(|_, h: &mut GenericHeader| {
h.command = command;
@@ -332,7 +332,7 @@
}
#[allow(clippy::cast_possible_truncation)]
- fn padded(command: Command2, total_size: usize) -> Frozen<MESSAGE_ALIGN> {
+ fn padded(command: Command, total_size: usize) -> Frozen<MESSAGE_ALIGN> {
Message::<GenericHeader>::new(total_size)
.transmute_header(|_, h: &mut GenericHeader| {
h.command = command;
@@ -432,23 +432,23 @@
let (server_out, server_in, server_shutdown, server_handle) = drive(server_conn);
// Client raw-sends a Request; the server pump reads it.
- raw_send(&mut client_ws, header_only(Command2::Request)).await;
+ raw_send(&mut client_ws, header_only(Command::Request)).await;
let received = compio::time::timeout(Duration::from_secs(5), server_in.recv())
.await
.expect("server recv within 5 s")
.expect("server frame");
- assert_eq!(received.header().command, Command2::Request);
+ assert_eq!(received.header().command, Command::Request);
// Server replies via its outbound mailbox; the serial pump writes
// the reply on the same bidi the request arrived on, client reads.
server_out
- .send(header_only(Command2::Reply))
+ .send(header_only(Command::Reply))
.await
.expect("server send");
let reply = compio::time::timeout(Duration::from_secs(5), raw_recv(&mut client_ws))
.await
.expect("client recv within 5 s");
- assert_eq!(reply.header().command, Command2::Reply);
+ assert_eq!(reply.header().command, Command::Reply);
server_shutdown.trigger();
let _ = compio::time::timeout(Duration::from_secs(5), server_handle).await;
@@ -464,12 +464,12 @@
let server_conn = WsTransportConn::new_server(server_ws);
let (_server_out, server_in, server_shutdown, server_handle) = drive(server_conn);
- raw_send(&mut client_ws, padded(Command2::Request, total)).await;
+ raw_send(&mut client_ws, padded(Command::Request, total)).await;
let received = compio::time::timeout(Duration::from_secs(15), server_in.recv())
.await
.expect("server recv within 15 s")
.expect("server frame");
- assert_eq!(received.header().command, Command2::Request);
+ assert_eq!(received.header().command, Command::Request);
assert_eq!(received.header().size as usize, total);
server_shutdown.trigger();
@@ -487,7 +487,7 @@
let (_server_out, server_in, _server_shutdown, server_handle) =
drive_with_cap(server_conn, CUSTOM_CAP);
- raw_send(&mut client_ws, padded(Command2::Request, OVER_CAP)).await;
+ raw_send(&mut client_ws, padded(Command::Request, OVER_CAP)).await;
// Decode rejection tears the server pump down; join must complete
// within the grace window and no frame must surface to in_rx.
diff --git a/core/message_bus/src/transports/wss.rs b/core/message_bus/src/transports/wss.rs
index a83c67d..e30b6a2 100644
--- a/core/message_bus/src/transports/wss.rs
+++ b/core/message_bus/src/transports/wss.rs
@@ -476,7 +476,7 @@
use crate::transports::tls::{install_default_crypto_provider, self_signed_for_loopback};
use async_channel::{Receiver, Sender, bounded};
use compio::net::TcpListener;
- use iggy_binary_protocol::{Command2, GenericHeader, HEADER_SIZE};
+ use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE};
use rustls::RootCertStore;
use server_common::MESSAGE_ALIGN;
use server_common::Message;
@@ -531,7 +531,7 @@
}
#[allow(clippy::cast_possible_truncation)]
- fn header_only(command: Command2) -> Frozen<MESSAGE_ALIGN> {
+ fn header_only(command: Command) -> Frozen<MESSAGE_ALIGN> {
Message::<GenericHeader>::new(HEADER_SIZE)
.transmute_header(|_, h: &mut GenericHeader| {
h.command = command;
@@ -541,7 +541,7 @@
}
#[allow(clippy::cast_possible_truncation)]
- fn padded(command: Command2, total_size: usize) -> Frozen<MESSAGE_ALIGN> {
+ fn padded(command: Command, total_size: usize) -> Frozen<MESSAGE_ALIGN> {
Message::<GenericHeader>::new(total_size)
.transmute_header(|_, h: &mut GenericHeader| {
h.command = command;
@@ -604,24 +604,24 @@
let (client_out, client_in, client_shutdown, client_handle) = drive(client_conn);
client_out
- .send(header_only(Command2::Request))
+ .send(header_only(Command::Request))
.await
.expect("client send");
let received = compio::time::timeout(Duration::from_secs(5), server_in.recv())
.await
.expect("server recv within 5 s")
.expect("server frame");
- assert_eq!(received.header().command, Command2::Request);
+ assert_eq!(received.header().command, Command::Request);
server_out
- .send(header_only(Command2::Reply))
+ .send(header_only(Command::Reply))
.await
.expect("server send");
let reply = compio::time::timeout(Duration::from_secs(5), client_in.recv())
.await
.expect("client recv within 5 s")
.expect("client frame");
- assert_eq!(reply.header().command, Command2::Reply);
+ assert_eq!(reply.header().command, Command::Reply);
server_shutdown.trigger();
client_shutdown.trigger();
@@ -647,14 +647,14 @@
let (client_out, _client_in, client_shutdown, client_handle) = drive(client_conn);
client_out
- .send(padded(Command2::Request, total))
+ .send(padded(Command::Request, total))
.await
.expect("client send 1 MiB");
let received = compio::time::timeout(Duration::from_secs(15), server_in.recv())
.await
.expect("server recv within 15 s")
.expect("server frame");
- assert_eq!(received.header().command, Command2::Request);
+ assert_eq!(received.header().command, Command::Request);
assert_eq!(received.header().size as usize, total);
server_shutdown.trigger();
@@ -683,7 +683,7 @@
drive_with_cap(client_conn, framing::MAX_MESSAGE_SIZE);
client_out
- .send(padded(Command2::Request, OVER_CAP))
+ .send(padded(Command::Request, OVER_CAP))
.await
.expect("client send oversize");
diff --git a/core/message_bus/tests/backpressure.rs b/core/message_bus/tests/backpressure.rs
index ef46d92..a329d50 100644
--- a/core/message_bus/tests/backpressure.rs
+++ b/core/message_bus/tests/backpressure.rs
@@ -28,7 +28,7 @@
header_only, install_dialed_replicas_locally, install_replicas_locally, loopback,
set_replica_ctx,
};
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
use message_bus::connector::{DEFAULT_RECONNECT_PERIOD, start as start_connector};
use message_bus::replica::listener::{MessageHandler, bind, run};
use message_bus::{IggyMessageBus, MessageBus, SendError};
@@ -82,7 +82,7 @@
// the exact threshold, only that it is finite and reachable.
let mut hit_backpressure = false;
for _ in 0..100_000 {
- let msg = header_only(Command2::Prepare, CLUSTER, 0);
+ let msg = header_only(Command::Prepare, CLUSTER, 0);
match bus0.send_to_replica(1, msg.into_frozen()).await {
Ok(()) => {}
Err(SendError::Backpressure) => {
diff --git a/core/message_bus/tests/common/mod.rs b/core/message_bus/tests/common/mod.rs
index b90a1c6..b5630b7 100644
--- a/core/message_bus/tests/common/mod.rs
+++ b/core/message_bus/tests/common/mod.rs
@@ -26,7 +26,7 @@
#![allow(dead_code)] // each test binary uses a subset
-use iggy_binary_protocol::{Command2, GenericHeader, HEADER_SIZE};
+use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE};
use message_bus::ConnectionInstaller;
use message_bus::client_listener::RequestHandler;
use message_bus::replica::auth::ReplicaAuth;
@@ -67,7 +67,7 @@
/// Used by tests to fabricate `Request`, `Reply`, `Ping`, etc. directly.
#[must_use]
#[allow(clippy::cast_possible_truncation)]
-pub fn header_only(command: Command2, cluster: u128, replica: u8) -> Message<GenericHeader> {
+pub fn header_only(command: Command, cluster: u128, replica: u8) -> Message<GenericHeader> {
Message::<GenericHeader>::new(HEADER_SIZE).transmute_header(|_, h: &mut GenericHeader| {
h.command = command;
h.cluster = cluster;
diff --git a/core/message_bus/tests/duplicate_client_id.rs b/core/message_bus/tests/duplicate_client_id.rs
index 8721b70..0f311d3 100644
--- a/core/message_bus/tests/duplicate_client_id.rs
+++ b/core/message_bus/tests/duplicate_client_id.rs
@@ -25,7 +25,7 @@
use common::{header_only, loopback, test_client_meta};
use compio::net::{TcpListener, TcpStream};
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
use message_bus::client_listener::RequestHandler;
use message_bus::framing::write_message;
use message_bus::installer::install_client_tcp;
@@ -145,7 +145,7 @@
// Push a valid Request frame down the orphan reader's wire. Before
// the fix, the orphan would call `on_request(client_id, msg)` and
// bump the counter; with the fix the `aborted` guard skips it.
- let msg = header_only(Command2::Request, 0, 0);
+ let msg = header_only(Command::Request, 0, 0);
write_message(&mut second_peer, msg)
.await
.expect("write Request");
diff --git a/core/message_bus/tests/graceful_shutdown.rs b/core/message_bus/tests/graceful_shutdown.rs
index 1803803..b37ac40 100644
--- a/core/message_bus/tests/graceful_shutdown.rs
+++ b/core/message_bus/tests/graceful_shutdown.rs
@@ -71,7 +71,7 @@
assert!(bus.is_shutting_down());
// Sends after shutdown must fail with the right variant.
- let dummy = common::header_only(iggy_binary_protocol::Command2::Reply, 0, 0);
+ let dummy = common::header_only(iggy_binary_protocol::Command::Reply, 0, 0);
let err = bus
.send_to_client(1u128 << 112, dummy.into_frozen())
.await
diff --git a/core/message_bus/tests/head_of_line.rs b/core/message_bus/tests/head_of_line.rs
index e367364..d632f6b 100644
--- a/core/message_bus/tests/head_of_line.rs
+++ b/core/message_bus/tests/head_of_line.rs
@@ -30,7 +30,7 @@
set_replica_ctx,
};
use compio::net::TcpListener;
-use iggy_binary_protocol::{Command2, HEADER_SIZE};
+use iggy_binary_protocol::{Command, HEADER_SIZE};
use message_bus::connector::{DEFAULT_RECONNECT_PERIOD, start as start_connector};
use message_bus::replica::listener::{MessageHandler, bind, run};
use message_bus::{IggyMessageBus, MessageBus, SendError};
@@ -79,7 +79,7 @@
.await
.is_ok()
{
- let ack = header_only(Command2::Ping, CLUSTER, 2);
+ let ack = header_only(Command::Ping, CLUSTER, 2);
let _ = message_bus::framing::write_message(&mut stream, ack).await;
}
held_streams_clone.borrow_mut().push(stream);
@@ -108,7 +108,7 @@
// Baseline: a single send to A before any saturation. Both peers are
// healthy, both sends take try_send fast-path time.
- bus0.send_to_replica(1, header_only(Command2::Prepare, 0, 0).into_frozen())
+ bus0.send_to_replica(1, header_only(Command::Prepare, 0, 0).into_frozen())
.await
.expect("send to A (baseline)");
@@ -119,7 +119,7 @@
let mut b_saturated = false;
for _ in 0..100_000 {
match bus0
- .send_to_replica(2, header_only(Command2::Prepare, 0, 0).into_frozen())
+ .send_to_replica(2, header_only(Command::Prepare, 0, 0).into_frozen())
.await
{
Ok(()) => {}
@@ -140,14 +140,14 @@
// against a freshly-timed B send that we expect to return Backpressure
// instantly. HOL would manifest as A being as slow as a blocked writev.
let send_a_start = Instant::now();
- bus0.send_to_replica(1, header_only(Command2::Prepare, 0, 0).into_frozen())
+ bus0.send_to_replica(1, header_only(Command::Prepare, 0, 0).into_frozen())
.await
.expect("send to A while B is saturated");
let send_a_elapsed = send_a_start.elapsed();
let send_b_start = Instant::now();
let send_b_result = bus0
- .send_to_replica(2, header_only(Command2::Prepare, 0, 0).into_frozen())
+ .send_to_replica(2, header_only(Command::Prepare, 0, 0).into_frozen())
.await;
let send_b_elapsed = send_b_start.elapsed();
let b_backpressured = matches!(send_b_result, Err(SendError::Backpressure));
diff --git a/core/message_bus/tests/quic_client_roundtrip.rs b/core/message_bus/tests/quic_client_roundtrip.rs
index 679bf66..23ec2b9 100644
--- a/core/message_bus/tests/quic_client_roundtrip.rs
+++ b/core/message_bus/tests/quic_client_roundtrip.rs
@@ -27,7 +27,7 @@
use compio::BufResult;
use compio::io::AsyncWriteExt;
use compio_quic::{ClientBuilder, Endpoint};
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
use message_bus::QuicTuning;
use message_bus::client_listener::RequestHandler;
use message_bus::client_listener::quic::{bind, run};
@@ -68,10 +68,10 @@
let bus_for_handler = bus.clone();
let on_request: RequestHandler = Rc::new(move |client_id, msg| {
- assert_eq!(msg.header().command, Command2::Request);
+ assert_eq!(msg.header().command, Command::Request);
let bus = bus_for_handler.clone();
compio::runtime::spawn(async move {
- let reply = header_only(Command2::Reply, 42, 0);
+ let reply = header_only(Command::Reply, 42, 0);
bus.send_to_client(client_id, reply.into_frozen())
.await
.expect("send_to_client should succeed");
@@ -103,7 +103,7 @@
// pattern; the server's accept_bi loop accepts the bidi, dispatches
// the request, writes the Reply, `finish()`-es the send half.
let (mut send, mut recv) = connection.open_bi_wait().await.expect("open_bi");
- let request = header_only(Command2::Request, 42, 0).into_frozen();
+ let request = header_only(Command::Request, 42, 0).into_frozen();
let BufResult(result, _) = send.write_all(request).await;
result.expect("client write request");
send.finish().expect("client finish");
@@ -115,7 +115,7 @@
.await
.expect("client must receive reply within 5 s")
.expect("reply frame");
- assert_eq!(reply.header().command, Command2::Reply);
+ assert_eq!(reply.header().command, Command::Reply);
assert_eq!(reply.header().cluster, 42);
let outcome = bus.shutdown(Duration::from_secs(2)).await;
@@ -143,7 +143,7 @@
let (request_tx, request_rx) = bounded::<()>(8);
let request_tx = Rc::new(request_tx);
let on_request: RequestHandler = Rc::new(move |_client_id, msg| {
- assert_eq!(msg.header().command, Command2::Request);
+ assert_eq!(msg.header().command, Command::Request);
let _ = request_tx.try_send(());
});
@@ -189,7 +189,7 @@
.expect("fast client open_bi within 2 s")
.expect("open_bi");
- let request = header_only(Command2::Request, 1, 0).into_frozen();
+ let request = header_only(Command::Request, 1, 0).into_frozen();
let BufResult(result, _) = send.write_all(request).await;
result.expect("fast client write request");
send.finish().expect("fast client finish");
diff --git a/core/message_bus/tests/replica_auth_handshake.rs b/core/message_bus/tests/replica_auth_handshake.rs
index 44e4893..d20787b 100644
--- a/core/message_bus/tests/replica_auth_handshake.rs
+++ b/core/message_bus/tests/replica_auth_handshake.rs
@@ -31,7 +31,7 @@
install_dialed_replicas_locally, install_replicas_locally, loopback, set_replica_ctx,
};
use compio::net::TcpStream;
-use iggy_binary_protocol::{Command2, GenericHeader, HEADER_SIZE};
+use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE};
use iggy_common::IggyError;
use message_bus::connector::start as start_connector;
use message_bus::framing::{self, MAX_MESSAGE_SIZE};
@@ -220,7 +220,7 @@
let resp = framing::read_message(&mut stream, MAX_MESSAGE_SIZE)
.await
.expect("read reject");
- assert_eq!(resp.header().command, Command2::ReplicaChallenge);
+ assert_eq!(resp.header().command, Command::ReplicaChallenge);
assert_eq!(
auth::read_status(&resp.header().reserved_command),
HandshakeStatus::ClusterMismatch,
@@ -269,14 +269,14 @@
let challenge = framing::read_message(&mut stream, MAX_MESSAGE_SIZE)
.await
.expect("read challenge");
- assert_eq!(challenge.header().command, Command2::ReplicaChallenge);
+ assert_eq!(challenge.header().command, Command::ReplicaChallenge);
assert_eq!(
auth::read_status(&challenge.header().reserved_command),
HandshakeStatus::Ok,
);
// Wrong command in the finish slot (Prepare instead of ReplicaFinish).
- framing::write_message(&mut stream, build_raw(CLUSTER, 0, Command2::Prepare))
+ framing::write_message(&mut stream, build_raw(CLUSTER, 0, Command::Prepare))
.await
.expect("write wrong finish");
@@ -303,7 +303,7 @@
nonce: Option<&[u8; auth::NONCE_LEN]>,
) -> Message<GenericHeader> {
Message::<GenericHeader>::new(HEADER_SIZE).transmute_header(|_, h: &mut GenericHeader| {
- h.command = Command2::ReplicaHello;
+ h.command = Command::ReplicaHello;
h.cluster = cluster_id;
h.replica = replica_id;
h.size = HEADER_SIZE as u32;
@@ -316,7 +316,7 @@
/// Build a raw frame with an arbitrary command for the wire-level tests (used to
/// send a wrong-command third frame in place of a `ReplicaFinish`).
#[allow(clippy::cast_possible_truncation)]
-fn build_raw(cluster_id: u128, replica_id: u8, command: Command2) -> Message<GenericHeader> {
+fn build_raw(cluster_id: u128, replica_id: u8, command: Command) -> Message<GenericHeader> {
Message::<GenericHeader>::new(HEADER_SIZE).transmute_header(|_, h: &mut GenericHeader| {
h.command = command;
h.cluster = cluster_id;
diff --git a/core/message_bus/tests/replica_roundtrip.rs b/core/message_bus/tests/replica_roundtrip.rs
index e16fbf3..0b2ed65 100644
--- a/core/message_bus/tests/replica_roundtrip.rs
+++ b/core/message_bus/tests/replica_roundtrip.rs
@@ -27,7 +27,7 @@
header_only, install_dialed_replicas_locally, install_replicas_locally, loopback,
set_replica_ctx,
};
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
use message_bus::connector::{DEFAULT_RECONNECT_PERIOD, start as start_connector};
use message_bus::replica::listener::{MessageHandler, bind, run};
use message_bus::{IggyMessageBus, MessageBus};
@@ -78,24 +78,24 @@
wait_until(|| bus0.replicas().contains(1), Duration::from_secs(2)).await;
// Replica 0 sends a Prepare to replica 1.
- let prepare = header_only(Command2::Prepare, CLUSTER, 0);
+ let prepare = header_only(Command::Prepare, CLUSTER, 0);
bus0.send_to_replica(1, prepare.into_frozen())
.await
.expect("send prepare");
// Replica 1 should observe the Prepare via its on_message callback.
let cmd = expect_recv(&rx1, Duration::from_secs(2)).await;
- assert_eq!(cmd, Command2::Prepare as u8);
+ assert_eq!(cmd, Command::Prepare as u8);
// Replica 1 responds with PrepareOk.
- let ack = header_only(Command2::PrepareOk, CLUSTER, 1);
+ let ack = header_only(Command::PrepareOk, CLUSTER, 1);
bus1.send_to_replica(0, ack.into_frozen())
.await
.expect("send ack");
// Replica 0 should observe the PrepareOk on its outbound read loop.
let cmd = expect_recv(&rx0, Duration::from_secs(2)).await;
- assert_eq!(cmd, Command2::PrepareOk as u8);
+ assert_eq!(cmd, Command::PrepareOk as u8);
bus0.shutdown(Duration::from_secs(2)).await;
bus1.shutdown(Duration::from_secs(2)).await;
diff --git a/core/message_bus/tests/replica_tls_bench.rs b/core/message_bus/tests/replica_tls_bench.rs
index 059699f..f3f9f30 100644
--- a/core/message_bus/tests/replica_tls_bench.rs
+++ b/core/message_bus/tests/replica_tls_bench.rs
@@ -37,7 +37,7 @@
install_dialed_replicas_locally, install_replicas_locally, loopback,
self_signed_replica_tls_ctx, set_replica_ctx, set_replica_ctx_with_tls,
};
-use iggy_binary_protocol::{Command2, GenericHeader};
+use iggy_binary_protocol::{Command, GenericHeader};
use message_bus::connector::{DEFAULT_RECONNECT_PERIOD, start as start_connector};
use message_bus::replica::listener::{MessageHandler, bind, run};
use message_bus::{IggyMessageBus, MessageBus, SendError};
@@ -121,7 +121,7 @@
loop {
let frame = Message::<GenericHeader>::new(FRAME_SIZE)
.transmute_header(|_, h: &mut GenericHeader| {
- h.command = Command2::Prepare;
+ h.command = Command::Prepare;
h.cluster = CLUSTER;
h.size = FRAME_SIZE as u32;
})
diff --git a/core/message_bus/tests/replica_tls_roundtrip.rs b/core/message_bus/tests/replica_tls_roundtrip.rs
index e3f1bee..8cf1c0b 100644
--- a/core/message_bus/tests/replica_tls_roundtrip.rs
+++ b/core/message_bus/tests/replica_tls_roundtrip.rs
@@ -28,7 +28,7 @@
header_only, install_dialed_replicas_locally, install_replicas_locally, loopback,
self_signed_replica_tls_ctx, set_replica_ctx, set_replica_ctx_with_tls,
};
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
use message_bus::connector::{DEFAULT_RECONNECT_PERIOD, start as start_connector};
use message_bus::replica::auth::ReplicaAuth;
use message_bus::replica::listener::{MessageHandler, bind, run};
@@ -95,21 +95,21 @@
wait_until(|| bus0.replicas().contains(1), Duration::from_secs(2)).await;
- let prepare = header_only(Command2::Prepare, CLUSTER, 0);
+ let prepare = header_only(Command::Prepare, CLUSTER, 0);
bus0.send_to_replica(1, prepare.into_frozen())
.await
.expect("send prepare");
let cmd = expect_recv(&rx1, Duration::from_secs(2)).await;
- assert_eq!(cmd, Command2::Prepare as u8);
+ assert_eq!(cmd, Command::Prepare as u8);
- let ack = header_only(Command2::PrepareOk, CLUSTER, 1);
+ let ack = header_only(Command::PrepareOk, CLUSTER, 1);
bus1.send_to_replica(0, ack.into_frozen())
.await
.expect("send ack");
let cmd = expect_recv(&rx0, Duration::from_secs(2)).await;
- assert_eq!(cmd, Command2::PrepareOk as u8);
+ assert_eq!(cmd, Command::PrepareOk as u8);
bus0.shutdown(Duration::from_secs(2)).await;
bus1.shutdown(Duration::from_secs(2)).await;
diff --git a/core/message_bus/tests/tcp_client_roundtrip.rs b/core/message_bus/tests/tcp_client_roundtrip.rs
index 6c8cd59..9925026 100644
--- a/core/message_bus/tests/tcp_client_roundtrip.rs
+++ b/core/message_bus/tests/tcp_client_roundtrip.rs
@@ -23,7 +23,7 @@
use common::{header_only, install_clients_locally, loopback};
use compio::net::TcpStream;
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
use message_bus::client_listener::RequestHandler;
use message_bus::client_listener::tcp::{bind, run};
use message_bus::framing;
@@ -38,10 +38,10 @@
// Handler echoes a Reply back via send_to_client.
let bus_for_handler = bus.clone();
let on_request: RequestHandler = Rc::new(move |client_id, msg| {
- assert_eq!(msg.header().command, Command2::Request);
+ assert_eq!(msg.header().command, Command::Request);
let bus = bus_for_handler.clone();
compio::runtime::spawn(async move {
- let reply = header_only(Command2::Reply, 42, 0);
+ let reply = header_only(Command::Reply, 42, 0);
bus.send_to_client(client_id, reply.into_frozen())
.await
.expect("send_to_client should succeed");
@@ -60,7 +60,7 @@
// Dial as a raw TCP client.
let mut client = TcpStream::connect(addr).await.expect("connect");
- let request = header_only(Command2::Request, 42, 0);
+ let request = header_only(Command::Request, 42, 0);
framing::write_message(&mut client, request)
.await
.expect("client write");
@@ -68,7 +68,7 @@
let reply = framing::read_message(&mut client, framing::MAX_MESSAGE_SIZE)
.await
.expect("client read");
- assert_eq!(reply.header().command, Command2::Reply);
+ assert_eq!(reply.header().command, Command::Reply);
assert_eq!(reply.header().cluster, 42);
let outcome = bus.shutdown(Duration::from_secs(2)).await;
@@ -97,7 +97,7 @@
bus.track_background(accept_handle);
let mut client = TcpStream::connect(addr).await.unwrap();
- let bogus = header_only(Command2::Ping, 0, 0);
+ let bogus = header_only(Command::Ping, 0, 0);
framing::write_message(&mut client, bogus).await.unwrap();
// Give the read loop a chance to observe + ignore it.
diff --git a/core/message_bus/tests/tcp_tls_client_listener.rs b/core/message_bus/tests/tcp_tls_client_listener.rs
index a3ba661..0ce3ed0 100644
--- a/core/message_bus/tests/tcp_tls_client_listener.rs
+++ b/core/message_bus/tests/tcp_tls_client_listener.rs
@@ -20,7 +20,7 @@
use async_channel::bounded;
use common::{header_only, install_tls_clients_locally, loopback};
use compio::net::TcpStream;
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
use iggy_binary_protocol::GenericHeader;
use message_bus::client_listener::RequestHandler;
use message_bus::client_listener::tcp_tls::{bind, run};
@@ -46,10 +46,10 @@
// take. Spawned because the handler signature is synchronous.
let bus_for_handler = Rc::clone(&bus);
let on_request: RequestHandler = Rc::new(move |client_id, msg| {
- assert_eq!(msg.header().command, Command2::Request);
+ assert_eq!(msg.header().command, Command::Request);
let bus = Rc::clone(&bus_for_handler);
compio::runtime::spawn(async move {
- let reply = header_only(Command2::Reply, 42, 0).into_frozen();
+ let reply = header_only(Command::Reply, 42, 0).into_frozen();
bus.send_to_client(client_id, reply)
.await
.expect("server send_to_client");
@@ -98,14 +98,14 @@
};
let client_handle = compio::runtime::spawn(async move { conn.run(ctx).await });
- let request = header_only(Command2::Request, 42, 0).into_frozen();
+ let request = header_only(Command::Request, 42, 0).into_frozen();
out_tx.send(request).await.expect("client send");
let reply = compio::time::timeout(Duration::from_secs(5), in_rx.recv())
.await
.expect("client must receive reply within 5 s")
.expect("reply frame");
- assert_eq!(reply.header().command, Command2::Reply);
+ assert_eq!(reply.header().command, Command::Reply);
assert_eq!(reply.header().cluster, 42);
client_shutdown.trigger();
diff --git a/core/message_bus/tests/tcp_tls_client_roundtrip.rs b/core/message_bus/tests/tcp_tls_client_roundtrip.rs
index 2ecb840..f4d55f5 100644
--- a/core/message_bus/tests/tcp_tls_client_roundtrip.rs
+++ b/core/message_bus/tests/tcp_tls_client_roundtrip.rs
@@ -23,7 +23,7 @@
install_replicas_locally, install_tls_clients_locally, loopback, set_replica_ctx,
};
use compio::net::TcpStream;
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
use iggy_binary_protocol::GenericHeader;
use message_bus::client_listener::RequestHandler;
use message_bus::connector::DEFAULT_RECONNECT_PERIOD;
@@ -51,10 +51,10 @@
let on_message: MessageHandler = Rc::new(|_, _| {});
let bus_for_handler = Rc::clone(&bus);
let on_request: RequestHandler = Rc::new(move |client_id, msg| {
- assert_eq!(msg.header().command, Command2::Request);
+ assert_eq!(msg.header().command, Command::Request);
let bus = Rc::clone(&bus_for_handler);
compio::runtime::spawn(async move {
- let reply = header_only(Command2::Reply, CLUSTER, 0).into_frozen();
+ let reply = header_only(Command::Reply, CLUSTER, 0).into_frozen();
bus.send_to_client(client_id, reply)
.await
.expect("server send_to_client");
@@ -131,14 +131,14 @@
};
let client_handle = compio::runtime::spawn(async move { conn.run(ctx).await });
- let request = header_only(Command2::Request, CLUSTER, 0).into_frozen();
+ let request = header_only(Command::Request, CLUSTER, 0).into_frozen();
out_tx.send(request).await.expect("client send");
let reply = compio::time::timeout(Duration::from_secs(5), in_rx.recv())
.await
.expect("client must receive reply within 5 s")
.expect("reply frame");
- assert_eq!(reply.header().command, Command2::Reply);
+ assert_eq!(reply.header().command, Command::Reply);
assert_eq!(reply.header().cluster, CLUSTER);
client_shutdown.trigger();
diff --git a/core/message_bus/tests/vectored_batch.rs b/core/message_bus/tests/vectored_batch.rs
index 6f6adb6..fc692c3 100644
--- a/core/message_bus/tests/vectored_batch.rs
+++ b/core/message_bus/tests/vectored_batch.rs
@@ -24,7 +24,7 @@
header_only, install_dialed_replicas_locally, install_replicas_locally, loopback,
set_replica_ctx,
};
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
use message_bus::connector::{DEFAULT_RECONNECT_PERIOD, start as start_connector};
use message_bus::replica::listener::{MessageHandler, bind, run};
use message_bus::{IggyMessageBus, MessageBus};
@@ -73,7 +73,7 @@
// Pipeline N sends back-to-back without yielding. The writer task
// should drain many of them in single writev calls.
for i in 0..N {
- let msg = header_only(Command2::Prepare, i as u128, 0);
+ let msg = header_only(Command::Prepare, i as u128, 0);
bus0.send_to_replica(1, msg.into_frozen())
.await
.expect("send should succeed - queue capacity is 256");
diff --git a/core/message_bus/tests/ws_client_roundtrip.rs b/core/message_bus/tests/ws_client_roundtrip.rs
index a66f5a6..3f10ff4 100644
--- a/core/message_bus/tests/ws_client_roundtrip.rs
+++ b/core/message_bus/tests/ws_client_roundtrip.rs
@@ -35,7 +35,7 @@
use compio::net::TcpStream;
use compio_ws::WebSocketStream;
use compio_ws::tungstenite::Message as WsMessage;
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
use iggy_binary_protocol::GenericHeader;
use message_bus::client_listener::RequestHandler;
use message_bus::client_listener::ws::{bind, run};
@@ -77,10 +77,10 @@
// completes within the same runtime tick.
let bus_for_handler = Rc::clone(&bus);
let on_request: RequestHandler = Rc::new(move |client_id, msg| {
- assert_eq!(msg.header().command, Command2::Request);
+ assert_eq!(msg.header().command, Command::Request);
let bus = Rc::clone(&bus_for_handler);
compio::runtime::spawn(async move {
- let reply = header_only(Command2::Reply, 42, 0).into_frozen();
+ let reply = header_only(Command::Reply, 42, 0).into_frozen();
bus.send_to_client(client_id, reply)
.await
.expect("server send_to_client");
@@ -105,7 +105,7 @@
.await
.expect("ws handshake");
- let request = header_only(Command2::Request, 42, 0).into_frozen();
+ let request = header_only(Command::Request, 42, 0).into_frozen();
ws_client
.send(WsMessage::Binary(Bytes::from_owner(request)))
.await
@@ -114,7 +114,7 @@
let reply = compio::time::timeout(Duration::from_secs(2), raw_recv(&mut ws_client))
.await
.expect("client must receive reply within 2 s");
- assert_eq!(reply.header().command, Command2::Reply);
+ assert_eq!(reply.header().command, Command::Reply);
bus.shutdown(Duration::from_secs(2)).await;
}
diff --git a/core/message_bus/tests/wss_client_listener.rs b/core/message_bus/tests/wss_client_listener.rs
index 4136379..56566a6 100644
--- a/core/message_bus/tests/wss_client_listener.rs
+++ b/core/message_bus/tests/wss_client_listener.rs
@@ -20,7 +20,7 @@
use async_channel::bounded;
use common::{header_only, install_wss_clients_locally, loopback};
use compio::net::TcpStream;
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
use iggy_binary_protocol::GenericHeader;
use message_bus::client_listener::RequestHandler;
use message_bus::client_listener::wss::{bind, run};
@@ -43,10 +43,10 @@
let bus_for_handler = Rc::clone(&bus);
let on_request: RequestHandler = Rc::new(move |client_id, msg| {
- assert_eq!(msg.header().command, Command2::Request);
+ assert_eq!(msg.header().command, Command::Request);
let bus = Rc::clone(&bus_for_handler);
compio::runtime::spawn(async move {
- let reply = header_only(Command2::Reply, 7, 0).into_frozen();
+ let reply = header_only(Command::Reply, 7, 0).into_frozen();
bus.send_to_client(client_id, reply)
.await
.expect("server send_to_client");
@@ -93,14 +93,14 @@
};
let client_handle = compio::runtime::spawn(async move { conn.run(ctx).await });
- let request = header_only(Command2::Request, 7, 0).into_frozen();
+ let request = header_only(Command::Request, 7, 0).into_frozen();
out_tx.send(request).await.expect("client send");
let reply = compio::time::timeout(Duration::from_secs(5), in_rx.recv())
.await
.expect("client must receive reply within 5 s")
.expect("reply frame");
- assert_eq!(reply.header().command, Command2::Reply);
+ assert_eq!(reply.header().command, Command::Reply);
assert_eq!(reply.header().cluster, 7);
client_shutdown.trigger();
diff --git a/core/message_bus/tests/wss_client_roundtrip.rs b/core/message_bus/tests/wss_client_roundtrip.rs
index 971df4c..24cb272 100644
--- a/core/message_bus/tests/wss_client_roundtrip.rs
+++ b/core/message_bus/tests/wss_client_roundtrip.rs
@@ -23,7 +23,7 @@
install_replicas_locally, install_wss_clients_locally, loopback, set_replica_ctx,
};
use compio::net::TcpStream;
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
use iggy_binary_protocol::GenericHeader;
use message_bus::client_listener::RequestHandler;
use message_bus::connector::DEFAULT_RECONNECT_PERIOD;
@@ -51,10 +51,10 @@
let on_message: MessageHandler = Rc::new(|_, _| {});
let bus_for_handler = Rc::clone(&bus);
let on_request: RequestHandler = Rc::new(move |client_id, msg| {
- assert_eq!(msg.header().command, Command2::Request);
+ assert_eq!(msg.header().command, Command::Request);
let bus = Rc::clone(&bus_for_handler);
compio::runtime::spawn(async move {
- let reply = header_only(Command2::Reply, CLUSTER, 0).into_frozen();
+ let reply = header_only(Command::Reply, CLUSTER, 0).into_frozen();
bus.send_to_client(client_id, reply)
.await
.expect("server send_to_client");
@@ -132,14 +132,14 @@
};
let client_handle = compio::runtime::spawn(async move { conn.run(ctx).await });
- let request = header_only(Command2::Request, CLUSTER, 0).into_frozen();
+ let request = header_only(Command::Request, CLUSTER, 0).into_frozen();
out_tx.send(request).await.expect("client send");
let reply = compio::time::timeout(Duration::from_secs(5), in_rx.recv())
.await
.expect("client must receive reply within 5 s")
.expect("reply frame");
- assert_eq!(reply.header().command, Command2::Reply);
+ assert_eq!(reply.header().command, Command::Reply);
assert_eq!(reply.header().cluster, CLUSTER);
client_shutdown.trigger();
diff --git a/core/metadata/src/impls/metadata.rs b/core/metadata/src/impls/metadata.rs
index 2bb877f..acd3574 100644
--- a/core/metadata/src/impls/metadata.rs
+++ b/core/metadata/src/impls/metadata.rs
@@ -43,7 +43,7 @@
use iggy_binary_protocol::requests::topics::CreateTopicRequest as WireCreateTopicRequest;
use iggy_binary_protocol::requests::topics::CreateTopicWithAssignmentsRequest as PersistedCreateTopicRequest;
use iggy_binary_protocol::{
- Command2, ConsensusHeader, EvictionReason, GenericHeader, Operation, PrepareHeader,
+ Command, ConsensusHeader, EvictionReason, GenericHeader, Operation, PrepareHeader,
PrepareOkHeader, ProtocolVersion, ReplyHeader, RoutedRequestHeader, WireDecode, WireEncode,
WireName,
};
@@ -1326,7 +1326,7 @@
{
assert!(matches!(
message.header().command(),
- Command2::Request | Command2::Prepare | Command2::PrepareOk
+ Command::Request | Command::Prepare | Command::PrepareOk
));
message.header().operation().is_metadata_plane()
}
@@ -2508,7 +2508,7 @@
consensus.drain_loopback_into(&mut loopback);
for message in loopback {
match message.header().command {
- Command2::PrepareOk => match message.try_into_typed::<PrepareOkHeader>() {
+ Command::PrepareOk => match message.try_into_typed::<PrepareOkHeader>() {
Ok(prepare_ok) => self.on_ack(prepare_ok).await,
Err(error) => warn!(
error = %error,
@@ -2614,7 +2614,7 @@
}
for message in loopback.drain(..) {
match message.header().command {
- Command2::PrepareOk => match message.try_into_typed::<PrepareOkHeader>() {
+ Command::PrepareOk => match message.try_into_typed::<PrepareOkHeader>() {
Ok(prepare_ok) => self.on_ack(prepare_ok).await,
Err(error) => warn!(
error = %error,
@@ -3424,7 +3424,7 @@
// TODO: calculate the index;
#[allow(clippy::cast_possible_truncation)]
let idx = header.op as usize;
- assert_eq!(header.command, Command2::Prepare);
+ assert_eq!(header.command, Command::Prepare);
assert!(
journal.handle().header(idx).is_some(),
"replicate: prepare must be durable in local journal before chain-forward"
@@ -3579,7 +3579,7 @@
)
.expect("zeroed bytes are a valid RoutedRequestHeader");
*header = RoutedRequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::Register,
size: u32::try_from(header_size).expect("RoutedRequestHeader size fits u32"),
cluster: consensus.cluster(),
@@ -3617,7 +3617,7 @@
)
.expect("zeroed bytes are a valid RoutedRequestHeader");
*header = RoutedRequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::Logout,
size: u32::try_from(header_size).expect("RoutedRequestHeader size fits u32"),
cluster: consensus.cluster(),
@@ -3653,7 +3653,7 @@
bytemuck::checked::try_from_bytes_mut::<RoutedRequestHeader>(&mut slice[..header_size])
.expect("zeroed bytes are a valid RoutedRequestHeader");
*header = RoutedRequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::CompleteConsumerGroupRevocation,
size: u32::try_from(total).expect("request size fits u32"),
cluster: consensus.cluster(),
@@ -3744,7 +3744,7 @@
bytemuck::checked::try_from_bytes_mut::<RoutedRequestHeader>(&mut slice[..header_size])
.expect("zeroed bytes are a valid RoutedRequestHeader");
*header = RoutedRequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::TruncatePartition,
size: u32::try_from(total).expect("request size fits u32"),
cluster: template.cluster,
@@ -3797,7 +3797,7 @@
size: u32::try_from(size).expect("prepare message size exceeds u32"),
view: consensus.view(),
release: request.release,
- command: Command2::Prepare,
+ command: Command::Prepare,
replica: consensus.replica(),
client: request.client,
parent: consensus.last_prepare_checksum(),
@@ -4165,7 +4165,7 @@
client,
request: 0,
commit: session,
- command: Command2::Reply,
+ command: Command::Reply,
operation: Operation::Register,
..Default::default()
};
@@ -4344,7 +4344,7 @@
let header =
bytemuck::checked::from_bytes_mut::<RoutedRequestHeader>(&mut slice[..header_size]);
*header = RoutedRequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::CreateTopic,
size: u32::try_from(total).unwrap(),
client,
@@ -4578,7 +4578,7 @@
request,
commit: request,
size: u32::try_from(total).unwrap(),
- command: Command2::Reply,
+ command: Command::Reply,
operation,
..Default::default()
};
@@ -4593,7 +4593,7 @@
&mut message.as_mut_slice()[..header_size],
);
*header = RoutedRequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::CreatePersonalAccessToken,
size: u32::try_from(header_size).unwrap(),
client,
@@ -4624,7 +4624,7 @@
let header =
bytemuck::checked::from_bytes_mut::<RoutedRequestHeader>(&mut slice[..header_size]);
*header = RoutedRequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::CreateStream,
size: u32::try_from(total).unwrap(),
client,
diff --git a/core/metadata/src/impls/recovery.rs b/core/metadata/src/impls/recovery.rs
index 624f933..f192d38 100644
--- a/core/metadata/src/impls/recovery.rs
+++ b/core/metadata/src/impls/recovery.rs
@@ -796,7 +796,7 @@
use crate::impls::metadata::checkpoint_checksum;
use crate::stm::snapshot::SNAPSHOT_FORMAT_VERSION;
use consensus::CLIENTS_TABLE_MAX;
- use iggy_binary_protocol::consensus::{Command2, Operation};
+ use iggy_binary_protocol::consensus::{Command, Operation};
use journal::Journal;
use server_common::iobuf::Owned;
use tempfile::tempdir;
@@ -910,7 +910,7 @@
&mut buffer.as_mut_slice()[..HEADER_SIZE],
);
header.size = total_size as u32;
- header.command = Command2::Prepare;
+ header.command = Command::Prepare;
header.op = op;
header.commit = commit;
header.operation = Operation::CreateStream;
@@ -936,7 +936,7 @@
&mut buffer.as_mut_slice()[..HEADER_SIZE],
);
header.size = total_size as u32;
- header.command = Command2::Prepare;
+ header.command = Command::Prepare;
header.op = op;
header.commit = op.saturating_sub(1);
header.operation = operation;
diff --git a/core/metadata/src/stm/authz.rs b/core/metadata/src/stm/authz.rs
index 4f235ff..de33414 100644
--- a/core/metadata/src/stm/authz.rs
+++ b/core/metadata/src/stm/authz.rs
@@ -341,9 +341,7 @@
| Operation::DeletePersonalAccessToken
| Operation::SendMessages
| Operation::StoreConsumerOffset
- | Operation::DeleteConsumerOffset
- | Operation::StoreConsumerOffset2
- | Operation::DeleteConsumerOffset2 => None,
+ | Operation::DeleteConsumerOffset => None,
}
}
@@ -409,7 +407,7 @@
use iggy_binary_protocol::requests::streams::CreateStreamRequest;
use iggy_binary_protocol::requests::topics::CreateTopicRequest;
use iggy_binary_protocol::requests::users::CreateUserRequest;
- use iggy_binary_protocol::{Command2, WireEncode, WireName, WireOptions};
+ use iggy_binary_protocol::{Command, WireEncode, WireName, WireOptions};
use iggy_common::UserStatus;
use server_common::iobuf::Owned;
@@ -438,7 +436,7 @@
let header = bytemuck::checked::from_bytes_mut::<PrepareHeader>(
&mut buffer.as_mut_slice()[..HEADER_SIZE],
);
- header.command = Command2::Prepare;
+ header.command = Command::Prepare;
header.operation = operation;
header.op = op;
header.user_id = user_id;
diff --git a/core/metadata/src/stm/stream.rs b/core/metadata/src/stm/stream.rs
index f7ba28f..eefe9e4 100644
--- a/core/metadata/src/stm/stream.rs
+++ b/core/metadata/src/stm/stream.rs
@@ -3270,7 +3270,7 @@
name: &str,
op: u64,
) -> server_common::Message<iggy_binary_protocol::PrepareHeader> {
- use iggy_binary_protocol::{Command2, Operation, PrepareHeader};
+ use iggy_binary_protocol::{Command, Operation, PrepareHeader};
use server_common::Message;
use server_common::iobuf::Owned;
use std::mem::size_of;
@@ -3287,7 +3287,7 @@
let header = bytemuck::checked::from_bytes_mut::<PrepareHeader>(
&mut buffer.as_mut_slice()[..header_size],
);
- header.command = Command2::Prepare;
+ header.command = Command::Prepare;
header.operation = Operation::CreateStream;
header.op = op;
header.size = u32::try_from(total).unwrap();
diff --git a/core/partitions/src/iggy_partition.rs b/core/partitions/src/iggy_partition.rs
index 2aec169..76267af 100644
--- a/core/partitions/src/iggy_partition.rs
+++ b/core/partitions/src/iggy_partition.rs
@@ -45,8 +45,7 @@
send_prepare_ok as send_prepare_ok_common, verify_prepare_integrity,
};
use iggy_binary_protocol::requests::consumer_offsets::{
- DeleteConsumerOffset2Request, DeleteConsumerOffsetRequest, StoreConsumerOffset2Request,
- StoreConsumerOffsetRequest,
+ DeleteConsumerOffsetRequest, StoreConsumerOffsetRequest,
};
use iggy_binary_protocol::responses::messages::{
SendMessagesConfirmationResponse, SendMessagesResponse,
@@ -70,8 +69,8 @@
use server_common::{
MESSAGE_ALIGN, Message, SegmentStorage,
iobuf::{Frozen, Owned},
- send_messages2::{
- ChecksumMode, SendMessages2Header, convert_request_message, decode_prepare_slice,
+ send_messages::{
+ BatchHeader, ChecksumMode, convert_request_message, decode_prepare_slice,
decode_prepare_slice_trusted, stamp_prepare_for_persistence,
},
sharding::IggyNamespace,
@@ -1268,7 +1267,7 @@
// primary is real divergence (log corruption / out-of-order apply)
// and must surface rather than silently mask a split state. A
// FOLLOWER may legitimately miss the offset: `AckLevel::NoAck`
- // (v2) stores apply on the primary only and are never replicated,
+ // stores apply on the primary only and are never replicated,
// so a later quorum delete finds nothing on the backups -- erroring
// there would fail the committed apply, panic the replica as
// divergent, and crash-loop on every journal replay. The
@@ -1894,9 +1893,9 @@
// Skip the batch-checksum pass: on the partition ingest path
// nothing reads it before `stamp_prepare_for_persistence`
// recomputes it over the stamped header. An already-canonical
- // batch (native v2, or the plane's pre-encrypt convert output)
- // returns early above, so Skip only affects the legacy
- // transcode, whose output goes straight to project/stamp.
+ // batch (the plane's pre-encrypt convert output) returns early
+ // inside the convert, so Skip only affects the wire-form
+ // admission, whose output goes straight to project/stamp.
match convert_request_message(namespace, message, ChecksumMode::Skip) {
Ok(message) => message,
Err(error) => {
@@ -1918,10 +1917,7 @@
// Parse once for both the delete-existence check and AckLevel dispatch.
let consumer_offset = match message.header().operation {
- Operation::StoreConsumerOffset
- | Operation::StoreConsumerOffset2
- | Operation::DeleteConsumerOffset
- | Operation::DeleteConsumerOffset2 => {
+ Operation::StoreConsumerOffset | Operation::DeleteConsumerOffset => {
match Self::parse_consumer_offset_request(message.header().operation, &message)
{
Ok(parsed) => Some(parsed),
@@ -1945,10 +1941,8 @@
_ => None,
};
- if matches!(
- message.header().operation,
- Operation::DeleteConsumerOffset | Operation::DeleteConsumerOffset2
- ) && let Some((kind, consumer_id, _, _)) = consumer_offset
+ if matches!(message.header().operation, Operation::DeleteConsumerOffset)
+ && let Some((kind, consumer_id, _, _)) = consumer_offset
&& let Err(error) = self.ensure_consumer_offset_exists(kind, consumer_id)
{
emit_partition_diag(
@@ -1984,10 +1978,8 @@
// `InvalidOffset` rides `ReplyHeader.status` (op=0, empty body): the
// status-only `classify_partition_reply` would misread a result-body
// code on this committed-shaped frame (op=commit_max) as success.
- if matches!(
- message.header().operation,
- Operation::StoreConsumerOffset | Operation::StoreConsumerOffset2
- ) && let Some((_, _, Some(requested_offset), _)) = consumer_offset
+ if matches!(message.header().operation, Operation::StoreConsumerOffset)
+ && let Some((_, _, Some(requested_offset), _)) = consumer_offset
{
let current_offset = self.stats.current_offset();
let partition_empty =
@@ -2039,11 +2031,11 @@
return;
}
- // NoAck v2 -> fast path. Quorum + v1 -> VSR pipeline.
+ // NoAck -> fast path. Quorum -> VSR pipeline.
if let Some((kind, consumer_id, offset, AckLevel::NoAck)) = consumer_offset
&& matches!(
message.header().operation,
- Operation::StoreConsumerOffset2 | Operation::DeleteConsumerOffset2,
+ Operation::StoreConsumerOffset | Operation::DeleteConsumerOffset,
)
{
Disposition::NoAck {
@@ -2546,10 +2538,7 @@
);
Ok(frozen)
}
- Operation::StoreConsumerOffset
- | Operation::DeleteConsumerOffset
- | Operation::StoreConsumerOffset2
- | Operation::DeleteConsumerOffset2 => {
+ Operation::StoreConsumerOffset | Operation::DeleteConsumerOffset => {
// Replicated path is Quorum-only by construction; ack ignored.
let (kind, consumer_id, offset, _ack) =
Self::parse_staged_consumer_offset_commit(header.operation, &message)?;
@@ -2574,7 +2563,7 @@
.map_err(|_| IggyError::CannotAppendMessage)?;
match header.operation {
- Operation::StoreConsumerOffset | Operation::StoreConsumerOffset2 => {
+ Operation::StoreConsumerOffset => {
self.stage_consumer_offset_upsert(
header.op,
kind,
@@ -2583,7 +2572,7 @@
is_auto_commit_client(header.client),
);
}
- Operation::DeleteConsumerOffset | Operation::DeleteConsumerOffset2 => {
+ Operation::DeleteConsumerOffset => {
self.stage_consumer_offset_delete(header.op, kind, consumer_id);
}
_ => unreachable!(),
@@ -2663,7 +2652,7 @@
async fn append_stamped_messages(
&mut self,
message: Message<PrepareHeader>,
- batch: SendMessages2Header,
+ batch: BatchHeader,
) -> Result<JournaledMessages, IggyError> {
let batch_messages_count = batch.message_count;
if batch_messages_count == 0 {
@@ -3367,10 +3356,7 @@
}
!*failed_commit
}
- Operation::StoreConsumerOffset
- | Operation::DeleteConsumerOffset
- | Operation::StoreConsumerOffset2
- | Operation::DeleteConsumerOffset2 => {
+ Operation::StoreConsumerOffset | Operation::DeleteConsumerOffset => {
self.commit_consumer_offset_entry(prepare_header, failed_commit)
.await
}
@@ -3499,7 +3485,7 @@
let (kind, consumer_id, offset, _ack) =
Self::parse_staged_consumer_offset_commit(header.operation, &message)?;
match header.operation {
- Operation::StoreConsumerOffset | Operation::StoreConsumerOffset2 => {
+ Operation::StoreConsumerOffset => {
let offset = offset.ok_or(IggyError::InvalidCommand)?;
Ok(if is_auto_commit_client(header.client) {
PendingConsumerOffsetCommit::upsert_auto_commit(kind, consumer_id, offset)
@@ -3507,7 +3493,7 @@
PendingConsumerOffsetCommit::upsert(kind, consumer_id, offset)
})
}
- Operation::DeleteConsumerOffset | Operation::DeleteConsumerOffset2 => {
+ Operation::DeleteConsumerOffset => {
Ok(PendingConsumerOffsetCommit::delete(kind, consumer_id))
}
_ => Err(IggyError::InvalidCommand),
@@ -3540,21 +3526,11 @@
Operation::StoreConsumerOffset => {
let request = StoreConsumerOffsetRequest::decode_from(body)
.map_err(|_| IggyError::InvalidCommand)?;
- (request.consumer, Some(request.offset), AckLevel::Quorum)
- }
- Operation::StoreConsumerOffset2 => {
- let request = StoreConsumerOffset2Request::decode_from(body)
- .map_err(|_| IggyError::InvalidCommand)?;
(request.consumer, Some(request.offset), request.ack)
}
Operation::DeleteConsumerOffset => {
let request = DeleteConsumerOffsetRequest::decode_from(body)
.map_err(|_| IggyError::InvalidCommand)?;
- (request.consumer, None, AckLevel::Quorum)
- }
- Operation::DeleteConsumerOffset2 => {
- let request = DeleteConsumerOffset2Request::decode_from(body)
- .map_err(|_| IggyError::InvalidCommand)?;
(request.consumer, None, request.ack)
}
_ => return Err(IggyError::InvalidCommand),
@@ -3663,7 +3639,6 @@
let segment_index = self.log.segments().len() - 1;
let segment = &mut self.log.segments_mut()[segment_index];
segment.size = IggyByteSize::from(segment.size.as_bytes_u64() + saved_bytes as u64);
- self.log.clear_in_flight();
return Ok(());
}
@@ -3715,7 +3690,6 @@
let segment = &mut self.log.segments_mut()[segment_index];
segment.size = IggyByteSize::from(segment.size.as_bytes_u64() + saved.as_bytes_u64());
- self.log.clear_in_flight();
Ok(())
}
@@ -3759,17 +3733,9 @@
},
);
- let storage = SegmentStorage::new(
- &messages_path,
- &index_path,
- 0,
- 0,
- enforce_fsync,
- enforce_fsync,
- false,
- )
- .await
- .map_err(|_| IggyError::CannotCreateSegmentLogFile(messages_path.clone()))?;
+ let storage = SegmentStorage::new(&messages_path, &index_path, 0, 0, false)
+ .await
+ .map_err(|_| IggyError::CannotCreateSegmentLogFile(messages_path.clone()))?;
let messages_size_bytes = storage
.messages_writer
.as_ref()
@@ -4035,17 +4001,9 @@
let enforce_fsync = self.effective_enforce_fsync(config);
let preallocate_segments = self.effective_preallocate_segments(config);
let segment = Segment::new(start_offset, segment_size);
- let storage = SegmentStorage::new(
- &messages_path,
- &index_path,
- 0,
- 0,
- enforce_fsync,
- enforce_fsync,
- false,
- )
- .await
- .map_err(|_| IggyError::CannotCreateSegmentLogFile(messages_path.clone()))?;
+ let storage = SegmentStorage::new(&messages_path, &index_path, 0, 0, false)
+ .await
+ .map_err(|_| IggyError::CannotCreateSegmentLogFile(messages_path.clone()))?;
let messages_size_bytes = storage
.messages_writer
.as_ref()
@@ -5042,11 +5000,11 @@
use bytes::Bytes;
use compio::io::AsyncWriteAtExt;
use consensus::LocalPipeline;
- use iggy_binary_protocol::{Command2, ReplyHeader, WireConsumer, WireEncode};
+ use iggy_binary_protocol::{Command, ReplyHeader, WireConsumer, WireEncode};
use message_bus::SendError;
use server_common::MESSAGE_ALIGN;
- use server_common::send_messages2::{
- COMMAND_HEADER_SIZE, IggyMessage2, IggyMessage2Header, IggyMessages2, SendMessages2Owned,
+ use server_common::send_messages::{
+ COMMAND_HEADER_SIZE, IggyMessage, IggyMessageHeader, IggyMessages, SendMessagesOwned,
};
use std::cell::RefCell;
use std::rc::Rc;
@@ -5354,7 +5312,7 @@
let size = std::mem::size_of::<PrepareHeader>();
let prepare = Message::<PrepareHeader>::new(size).transmute_header(
|_, header: &mut PrepareHeader| {
- header.command = Command2::Prepare;
+ header.command = Command::Prepare;
header.op = 1;
// Current view: an older-view prepare is fenced as deposed-primary
// traffic and would never reach the ack send under test.
@@ -5479,7 +5437,7 @@
request_id: u64,
consumer_id: u32,
) -> Message<RoutedRequestHeader> {
- let body = DeleteConsumerOffset2Request {
+ let body = DeleteConsumerOffsetRequest {
consumer: WireConsumer::consumer(WireIdentifier::Numeric(consumer_id)),
stream_id: WireIdentifier::Numeric(1),
topic_id: WireIdentifier::Numeric(1),
@@ -5492,8 +5450,8 @@
let mut message = Message::<RoutedRequestHeader>::new(total);
message.as_mut_slice()[header_size..].copy_from_slice(&body);
message.transmute_header(|_, header: &mut RoutedRequestHeader| {
- header.command = Command2::Request;
- header.operation = Operation::DeleteConsumerOffset2;
+ header.command = Command::Request;
+ header.operation = Operation::DeleteConsumerOffset;
header.client = client_id;
header.session = 1;
header.request = request_id;
@@ -5527,7 +5485,7 @@
&frame.as_slice()[..std::mem::size_of::<ReplyHeader>()],
)
.expect("deny frame starts with a valid reply header");
- assert_eq!(header.command, Command2::Reply);
+ assert_eq!(header.command, Command::Reply);
assert_eq!(
header.status,
IggyError::ConsumerOffsetNotFound(0).as_code()
@@ -5784,17 +5742,17 @@
/// stamped at `base_offset`, with a valid batch checksum so it decodes
/// through `decode_batch_slice` and matches an `Offset` poll.
pub(super) fn build_segment_record(namespace: IggyNamespace, base_offset: u64) -> Vec<u8> {
- let mut batch = IggyMessages2::with_capacity(1);
- batch.push(IggyMessage2 {
- header: IggyMessage2Header {
+ let mut batch = IggyMessages::with_capacity(1);
+ batch.push(IggyMessage {
+ header: IggyMessageHeader {
payload_length: 8,
..Default::default()
},
payload: Bytes::from_static(b"abcdefgh"),
user_headers: None,
});
- let mut owned = SendMessages2Owned::from_messages(namespace, &batch)
- .expect("build send_messages batch");
+ let mut owned =
+ SendMessagesOwned::from_messages(namespace, &batch).expect("build send_messages batch");
owned.header.base_offset = base_offset;
owned.header.batch_checksum = owned.header.checksum_for_blob(&owned.blob);
@@ -6471,10 +6429,9 @@
let log_path = format!("{partition_dir}/{:0>20}.log", 0u64);
let index_path = format!("{partition_dir}/{:0>20}.index", 0u64);
partition.log.segments_mut()[0].sealed = true;
- partition.log.storages_mut()[0] =
- SegmentStorage::new(&log_path, &index_path, 0, 0, false, false, false)
- .await
- .expect("create segment storage");
+ partition.log.storages_mut()[0] = SegmentStorage::new(&log_path, &index_path, 0, 0, false)
+ .await
+ .expect("create segment storage");
let record = build_segment_record(namespace, 0);
let record_len = record.len() as u64;
@@ -6594,7 +6551,7 @@
let size = std::mem::size_of::<PrepareHeader>();
let prepare = Message::<PrepareHeader>::new(size).transmute_header(
|_, header: &mut PrepareHeader| {
- header.command = Command2::Prepare;
+ header.command = Command::Prepare;
header.op = op;
header.operation = operation;
header.size = u32::try_from(size).expect("prepare header size fits in u32");
@@ -7043,7 +7000,7 @@
#[test]
fn given_result_framed_operation_when_committed_should_reply_empty_result_section() {
assert_eq!(
- &committed_reply_body(Operation::StoreConsumerOffset2)[..],
+ &committed_reply_body(Operation::StoreConsumerOffset)[..],
&[0, 0, 0, 0]
);
}
@@ -7341,7 +7298,7 @@
mod purge_floor_tests {
use super::tests::{build_segment_record, repair_config, test_partition};
use super::*;
- use iggy_binary_protocol::{Command2, WireConsumer, WireEncode};
+ use iggy_binary_protocol::{Command, WireConsumer, WireEncode};
/// Fresh temp dir wired as the partition dir, so `purge()` can recreate
/// real segment files and write `purge.gen`.
@@ -7371,7 +7328,7 @@
let mut message = Message::<PrepareHeader>::new(total);
message.as_mut_slice()[header_size..].copy_from_slice(&record);
let message = message.transmute_header(|_, header: &mut PrepareHeader| {
- header.command = Command2::Prepare;
+ header.command = Command::Prepare;
header.operation = Operation::SendMessages;
header.op = op;
header.timestamp = op;
@@ -7385,7 +7342,7 @@
partition.consensus().sequencer().set_sequence(op);
}
- /// A `StoreConsumerOffset2` prepare for `op`, journaled and staged through
+ /// A `StoreConsumerOffset` prepare for `op`, journaled and staged through
/// the replicated-apply path.
async fn journal_store_offset(
partition: &mut IggyPartition<IggyMessageBus>,
@@ -7393,7 +7350,7 @@
consumer_id: u32,
offset: u64,
) {
- let body = StoreConsumerOffset2Request {
+ let body = StoreConsumerOffsetRequest {
consumer: WireConsumer::consumer(WireIdentifier::Numeric(consumer_id)),
stream_id: WireIdentifier::Numeric(1),
topic_id: WireIdentifier::Numeric(1),
@@ -7407,8 +7364,8 @@
let mut message = Message::<PrepareHeader>::new(total);
message.as_mut_slice()[header_size..].copy_from_slice(&body);
let message = message.transmute_header(|_, header: &mut PrepareHeader| {
- header.command = Command2::Prepare;
- header.operation = Operation::StoreConsumerOffset2;
+ header.command = Command::Prepare;
+ header.operation = Operation::StoreConsumerOffset;
header.op = op;
header.group = IggyNamespace::new(1, 1, 0).inner();
header.size = u32::try_from(total).expect("prepare size fits u32");
@@ -7737,7 +7694,7 @@
let mut message = Message::<PrepareHeader>::new(total);
message.as_mut_slice()[header_size..].copy_from_slice(&record);
let message = message.transmute_header(|_, header: &mut PrepareHeader| {
- header.command = Command2::Prepare;
+ header.command = Command::Prepare;
header.operation = Operation::SendMessages;
header.op = 1;
header.group = namespace.inner();
diff --git a/core/partitions/src/iggy_partitions.rs b/core/partitions/src/iggy_partitions.rs
index 45c07ad..c0bcead 100644
--- a/core/partitions/src/iggy_partitions.rs
+++ b/core/partitions/src/iggy_partitions.rs
@@ -23,11 +23,11 @@
use ahash::AHashSet;
use consensus::{Consensus, Plane, PlaneIdentity, VsrConsensus};
use iggy_binary_protocol::{
- Command2, ConsensusHeader, Operation, PrepareHeader, PrepareOkHeader, RoutedRequestHeader,
+ Command, ConsensusHeader, Operation, PrepareHeader, PrepareOkHeader, RoutedRequestHeader,
};
use journal::superblock::{PingPongSuperblock, SuperblockStore};
use message_bus::MessageBus;
-use server_common::send_messages2::{ChecksumMode, convert_request_message, encrypt_batch_request};
+use server_common::send_messages::{ChecksumMode, convert_request_message, encrypt_batch_request};
use server_common::sharding::{IggyNamespace, LocalIdx, ShardId};
#[cfg(debug_assertions)]
use std::cell::Cell;
@@ -643,7 +643,7 @@
{
assert!(matches!(
message.header().command(),
- Command2::Request | Command2::Prepare | Command2::PrepareOk
+ Command::Request | Command::Prepare | Command::PrepareOk
));
message.header().operation().is_partition()
}
@@ -659,8 +659,8 @@
use iggy_common::{IggyByteSize, PartitionStats};
use journal::Journal as _;
use message_bus::IggyMessageBus;
- use server_common::send_messages2::{
- IggyMessage2, IggyMessage2Header, IggyMessages2, PREPARE_SPLIT_POINT, SendMessages2Owned,
+ use server_common::send_messages::{
+ IggyMessage, IggyMessageHeader, IggyMessages, PREPARE_SPLIT_POINT, SendMessagesOwned,
stamp_prepare_for_persistence,
};
use server_common::{Message, iobuf::Frozen};
@@ -718,22 +718,22 @@
op: u64,
base_offset: u64,
) -> Frozen<4096> {
- let mut batch = IggyMessages2::with_capacity(1);
- batch.push(IggyMessage2 {
- header: IggyMessage2Header {
+ let mut batch = IggyMessages::with_capacity(1);
+ batch.push(IggyMessage {
+ header: IggyMessageHeader {
payload_length: 8,
..Default::default()
},
payload: Bytes::from_static(b"abcdefgh"),
user_headers: None,
});
- let owned = SendMessages2Owned::from_messages(namespace, &batch)
- .expect("build send_messages batch");
+ let owned =
+ SendMessagesOwned::from_messages(namespace, &batch).expect("build send_messages batch");
let total_size = PREPARE_SPLIT_POINT + owned.blob.len();
let prepare = Message::<PrepareHeader>::new(total_size).transmute_header(
|_, header: &mut PrepareHeader| {
- header.command = Command2::Prepare;
+ header.command = Command::Prepare;
header.operation = Operation::SendMessages;
header.op = op;
header.size = u32::try_from(total_size).expect("size fits u32");
diff --git a/core/partitions/src/journal.rs b/core/partitions/src/journal.rs
index 689e2b7..905ae61 100644
--- a/core/partitions/src/journal.rs
+++ b/core/partitions/src/journal.rs
@@ -19,7 +19,7 @@
use journal::{Journal, Storage};
use server_common::{
iobuf::{Frozen, Owned},
- send_messages2::{COMMAND_HEADER_SIZE, SendMessages2Ref, decode_prepare_slice_trusted},
+ send_messages::{self, BatchRef, COMMAND_HEADER_SIZE, decode_prepare_slice_trusted},
};
use std::io;
use std::{
@@ -1023,7 +1023,7 @@
}
pub fn select_batch_slice(
- batch: &SendMessages2Ref<'_>,
+ batch: &BatchRef<'_>,
query: MessageLookup,
already_matched: u32,
) -> Option<SelectedBatchSlice> {
@@ -1087,7 +1087,7 @@
/// Push the fragments for one selected batch, shared by the resident-journal
/// walk and the disk-chunk walk. `source` holds a stamped
-/// `[256B SendMessages2Header][blob]` batch starting at byte `batch_base`
+/// `[256B BatchHeader][blob]` batch starting at byte `batch_base`
/// (the disk walk passes the chunk cursor; the resident walk passes
/// `size_of::<PrepareHeader>()`, the batch's offset past the prepare header).
/// A full-body selection forwards the original batch bytes by reference; a
@@ -1099,7 +1099,7 @@
matched_messages: &mut u32,
source: &Frozen<4096>,
batch_base: usize,
- batch: &SendMessages2Ref<'_>,
+ batch: &BatchRef<'_>,
selection: SelectedBatchSlice,
) {
let full_body_selected = selection.start == 0 && selection.end == batch.blob().len();
@@ -1122,7 +1122,9 @@
.get(selection.start..selection.end)
.expect("selected batch slice must stay within blob bounds"),
);
- fragments.push(Fragment::whole(rewritten.into_frozen()));
+ fragments.push(Fragment::whole(send_messages::frozen_batch_header(
+ &rewritten,
+ )));
fragments.push(Fragment::slice(
source.clone(),
batch_base + COMMAND_HEADER_SIZE + selection.start,
@@ -1228,17 +1230,17 @@
mod tests {
use super::*;
use bytes::Bytes;
- use iggy_binary_protocol::{Command2, HEADER_SIZE};
+ use iggy_binary_protocol::{Command, HEADER_SIZE};
use journal::Journal;
use server_common::Message;
- use server_common::send_messages2::{
- IggyMessage2, IggyMessage2Header, IggyMessages2, SendMessages2Owned, decode_batch_slice,
+ use server_common::send_messages::{
+ IggyMessage, IggyMessageHeader, IggyMessages, SendMessagesOwned, decode_batch_slice,
};
use server_common::sharding::IggyNamespace;
fn build_prepare(op: u64, size: usize) -> Message<PrepareHeader> {
Message::<PrepareHeader>::new(size).transmute_header(|_, h: &mut PrepareHeader| {
- h.command = Command2::Prepare;
+ h.command = Command::Prepare;
h.op = op;
h.size = u32::try_from(size).expect("size fits in u32");
})
@@ -1468,10 +1470,10 @@
/// producer). Timestamp polls filter on the broker time because that is
/// the timestamp replies surface per message.
fn build_timestamped_batch(base_timestamp: u64, origin_timestamp: u64) -> Vec<u8> {
- let mut messages = IggyMessages2::with_capacity(3);
+ let mut messages = IggyMessages::with_capacity(3);
for index in 0..3u64 {
- messages.push(IggyMessage2 {
- header: IggyMessage2Header {
+ messages.push(IggyMessage {
+ header: IggyMessageHeader {
origin_timestamp: origin_timestamp + index,
payload_length: 8,
..Default::default()
@@ -1480,7 +1482,7 @@
user_headers: None,
});
}
- let mut owned = SendMessages2Owned::from_messages(IggyNamespace::new(1, 1, 0), &messages)
+ let mut owned = SendMessagesOwned::from_messages(IggyNamespace::new(1, 1, 0), &messages)
.expect("build send_messages batch");
owned.header.base_timestamp = base_timestamp;
owned.header.batch_checksum = owned.header.checksum_for_blob(&owned.blob);
diff --git a/core/partitions/src/lib.rs b/core/partitions/src/lib.rs
index 81078ba..6d14d4b 100644
--- a/core/partitions/src/lib.rs
+++ b/core/partitions/src/lib.rs
@@ -44,7 +44,7 @@
pub use poll_plan::{AutoCommitApplied, PollPlan};
pub use segment::Segment;
use server_common::Message;
-pub use server_common::send_messages2::{IggyMessage2, IggyMessage2Header, IggyMessages2};
+pub use server_common::send_messages::{IggyMessage, IggyMessageHeader, IggyMessages};
pub use types::{
AppendResult, Fragment, PartitionOffsets, PartitionsConfig, PollFragments, PollQueryResult,
PollingArgs, PollingConsumer, REPAIR_RETRY_TICKS, RepairConclusion, RepairSession,
diff --git a/core/partitions/src/log.rs b/core/partitions/src/log.rs
index 574b371..fc3a774 100644
--- a/core/partitions/src/log.rs
+++ b/core/partitions/src/log.rs
@@ -20,10 +20,10 @@
use crate::messages_writer::MessagesWriter;
use crate::poll_plan::SealedSegmentHandle;
use crate::segment::Segment;
-use iggy_common::{IggyByteSize, IggyMessagesBatch};
+use iggy_common::IggyByteSize;
use journal::{Journal, Storage};
use ringbuffer::AllocRingBuffer;
-use server_common::{IggyMessagesBatchSetInFlight, SegmentStorage};
+use server_common::SegmentStorage;
use std::collections::VecDeque;
use std::fmt::Debug;
use std::rc::Rc;
@@ -166,7 +166,6 @@
// `SEALED_READ_STATE_CAP`. Keyed by offset (stable), not slot index (which
// shifts on retire). See `touch_sealed_read_state`.
sealed_lru: VecDeque<u64>,
- in_flight: IggyMessagesBatchSetInFlight,
}
impl<J, S> Default for SegmentedLog<J, S>
@@ -187,7 +186,6 @@
index_writers: Vec::with_capacity(SEGMENTS_CAPACITY),
sealed_read_state: Vec::with_capacity(SEGMENTS_CAPACITY),
sealed_lru: VecDeque::with_capacity(SEALED_READ_STATE_CAP + 1),
- in_flight: IggyMessagesBatchSetInFlight::default(),
}
}
}
@@ -431,22 +429,6 @@
*segment_indexes = Some(indexes);
}
}
-
- pub const fn in_flight(&self) -> &IggyMessagesBatchSetInFlight {
- &self.in_flight
- }
-
- pub const fn in_flight_mut(&mut self) -> &mut IggyMessagesBatchSetInFlight {
- &mut self.in_flight
- }
-
- pub fn set_in_flight(&mut self, batches: Vec<IggyMessagesBatch>) {
- self.in_flight.set(batches);
- }
-
- pub fn clear_in_flight(&mut self) {
- self.in_flight.clear();
- }
}
impl<J, S> SegmentedLog<J, S>
diff --git a/core/partitions/src/poll_plan.rs b/core/partitions/src/poll_plan.rs
index 928222d..3b19f0d 100644
--- a/core/partitions/src/poll_plan.rs
+++ b/core/partitions/src/poll_plan.rs
@@ -39,7 +39,7 @@
ConsumerGroupId, ConsumerGroupOffsets, ConsumerKind, ConsumerOffset, ConsumerOffsets, IggyError,
};
use server_common::iobuf::{Frozen, Owned};
-use server_common::send_messages2::{BatchIntegrity, COMMAND_HEADER_SIZE, decode_batch_slice_with};
+use server_common::send_messages::{BatchIntegrity, COMMAND_HEADER_SIZE, decode_batch_slice_with};
use std::cell::{Cell, RefCell};
use std::hash::Hash;
use std::rc::Rc;
@@ -137,7 +137,7 @@
/// Owned auto-commit input, applied off the partition borrow after a poll (see
/// module docs). Only the in-memory apply happens here; durability is the
/// replicated [`crate::iggy_partition::IggyPartition::apply_staged_consumer_offset_commit`]
-/// path's job on every node, driven by the `StoreConsumerOffset2` op the serving
+/// path's job on every node, driven by the `StoreConsumerOffset` op the serving
/// shard submits from [`AutoCommitApplied`]. A poll-local disk write would be
/// node-local only and diverge on failover.
pub struct AutoCommitCtx {
@@ -148,7 +148,7 @@
///
/// The serving shard replicates it through the partition consensus (the only
/// cross-node durable path); `kind` + `consumer_id` are the offset key the
-/// submitted `StoreConsumerOffset2` op must carry.
+/// submitted `StoreConsumerOffset` op must carry.
pub struct AutoCommitApplied {
pub kind: ConsumerKind,
pub consumer_id: u32,
@@ -455,7 +455,7 @@
impl DiskReadPlan {
/// Serve a poll from the on-disk segment files, off the partition borrow.
/// Reads from owned descriptors so no partition reference is held across
- /// the file IO. Walks stamped `[256B SendMessages2Header][blob]` batches in
+ /// the file IO. Walks stamped `[256B BatchHeader][blob]` batches in
/// chunked reads, re-reading a batch split across a chunk boundary in the
/// next chunk.
#[allow(clippy::cast_possible_truncation)]
@@ -783,7 +783,7 @@
impl AutoCommitCtx {
/// The offset key (kind + numeric id) this auto-commit targets, for the
- /// replicated `StoreConsumerOffset2` op the serving shard submits.
+ /// replicated `StoreConsumerOffset` op the serving shard submits.
pub(crate) const fn kind_and_id(&self) -> (ConsumerKind, u32) {
match &self.target {
AutoCommitTarget::Consumer { consumer_id, .. } => {
@@ -901,7 +901,7 @@
}
}
-/// Walk stamped `[256B SendMessages2Header][blob]` batches in one disk
+/// Walk stamped `[256B BatchHeader][blob]` batches in one disk
/// chunk, pushing matching fragments. Returns bytes consumed: the start
/// of the first batch that did not fully fit in the chunk (the caller
/// re-reads from there), or the chunk end when everything decoded.
diff --git a/core/partitions/src/state_transfer.rs b/core/partitions/src/state_transfer.rs
index 31802d2..1653b19 100644
--- a/core/partitions/src/state_transfer.rs
+++ b/core/partitions/src/state_transfer.rs
@@ -42,7 +42,7 @@
use journal::superblock::SuperblockStore;
use message_bus::MessageBus;
use server_common::SegmentStorage;
-use server_common::send_messages2::decode_batch_slice;
+use server_common::send_messages::decode_batch_slice;
use std::collections::HashSet;
use std::fmt;
use std::mem::size_of;
@@ -2263,18 +2263,8 @@
// sweep itself is right (a chain the live state does not know
// about would resurrect at boot), so one retry against a
// transient open failure is the only cheap save available.
- let enforce_fsync = self.effective_enforce_fsync(config);
- let open = || {
- SegmentStorage::new(
- &log_final,
- &index_final,
- meta.size,
- meta.index_size,
- enforce_fsync,
- enforce_fsync,
- true,
- )
- };
+ let open =
+ || SegmentStorage::new(&log_final, &index_final, meta.size, meta.index_size, true);
let storage = match open().await {
Ok(storage) => storage,
Err(_) => open()
diff --git a/core/sdk/Cargo.toml b/core/sdk/Cargo.toml
index 4ff5bdc..7d496c4 100644
--- a/core/sdk/Cargo.toml
+++ b/core/sdk/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "iggy"
-version = "0.11.0-edge.3"
+version = "0.11.0-edge.4"
description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
edition = "2024"
rust-version.workspace = true
diff --git a/core/sdk/src/vsr.rs b/core/sdk/src/vsr.rs
index b79f17a..4a6bda5 100644
--- a/core/sdk/src/vsr.rs
+++ b/core/sdk/src/vsr.rs
@@ -21,7 +21,7 @@
LOGIN_REGISTER_CODE, LOGIN_REGISTER_WITH_PAT_CODE, LOGOUT_USER_CODE,
};
use iggy_binary_protocol::consensus::{
- Command2, EvictionHeader, EvictionReason, GenericHeader, HEADER_SIZE, Operation, ReplyHeader,
+ Command, EvictionHeader, EvictionReason, GenericHeader, HEADER_SIZE, Operation, ReplyHeader,
RequestHeader, read_size_field, result_code, result_section_len,
};
use iggy_common::{IggyError, calculate_checksum, eviction_reason_to_error};
@@ -129,7 +129,7 @@
reserved[NON_REPLICATED_CODE_RANGE].copy_from_slice(&code.to_le_bytes());
}
let header = RequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation,
size,
client: session.client_id(),
@@ -184,8 +184,8 @@
.try_into()
.map_err(|_| IggyError::InvalidCommand)?;
match peek_command(header_bytes) {
- Command2::Eviction => Err(decode_eviction(header_bytes)),
- Command2::Reply => {
+ Command::Eviction => Err(decode_eviction(header_bytes)),
+ Command::Reply => {
let total_size = response_size(header_bytes)?;
if response.len() < total_size {
return Err(IggyError::InvalidCommand);
@@ -204,18 +204,18 @@
/// buffers. Saves the 64B header `put_slice` that `decode_response` would
/// otherwise perform when callers concatenate header + body before decoding.
///
-/// Also surfaces session-terminal `Command2::Eviction` frames as typed
+/// Also surfaces session-terminal `Command::Eviction` frames as typed
/// errors: callers waiting on a Reply for an unbound session would otherwise
/// hit a read-timeout because the SDK previously only accepted
-/// `Command2::Reply`. Returns the body slice on a normal Reply, or maps the
+/// `Command::Reply`. Returns the body slice on a normal Reply, or maps the
/// eviction reason to an `IggyError` so the request fails fast.
pub(crate) fn decode_response_split(
header_bytes: &[u8; HEADER_SIZE],
body: Bytes,
) -> Result<Bytes, IggyError> {
match peek_command(header_bytes) {
- Command2::Eviction => Err(decode_eviction(header_bytes)),
- Command2::Reply => {
+ Command::Eviction => Err(decode_eviction(header_bytes)),
+ Command::Reply => {
let expected_body = response_size(header_bytes)? - HEADER_SIZE;
if body.len() < expected_body {
return Err(IggyError::InvalidCommand);
@@ -301,15 +301,15 @@
}
}
-/// `Command2` lives at a fixed offset shared by every consensus header
+/// `Command` lives at a fixed offset shared by every consensus header
/// (Reply, Eviction, Prepare, ...), so a byte read is enough to discriminate
/// the frame.
-fn peek_command(header_bytes: &[u8; HEADER_SIZE]) -> Command2 {
+fn peek_command(header_bytes: &[u8; HEADER_SIZE]) -> Command {
const COMMAND_OFFSET: usize = std::mem::offset_of!(GenericHeader, command);
match header_bytes[COMMAND_OFFSET] {
- x if x == Command2::Reply as u8 => Command2::Reply,
- x if x == Command2::Eviction as u8 => Command2::Eviction,
- _ => Command2::Reserved,
+ x if x == Command::Reply as u8 => Command::Reply,
+ x if x == Command::Eviction as u8 => Command::Eviction,
+ _ => Command::Reserved,
}
}
@@ -441,7 +441,7 @@
// decode funnel surfaces it as the typed error before any body decode,
// even though the deny body is empty.
let header = ReplyHeader {
- command: Command2::Reply,
+ command: Command::Reply,
size: HEADER_SIZE as u32,
status: IggyError::Unauthorized.as_code(),
..Default::default()
@@ -456,7 +456,7 @@
fn reply_with_zero_status_passes_body_through() {
// status 0 is the ok channel: a non-metadata reply returns its body.
let header = ReplyHeader {
- command: Command2::Reply,
+ command: Command::Reply,
operation: Operation::NonReplicated,
size: (HEADER_SIZE + 3) as u32,
..Default::default()
@@ -679,7 +679,7 @@
#[test]
fn store_consumer_offset_rejection_decodes_to_terminal_error() {
let code = IggyError::InvalidOffset(42).as_code();
- let result = split_metadata_result(Operation::StoreConsumerOffset2, rejection_body(code));
+ let result = split_metadata_result(Operation::StoreConsumerOffset, rejection_body(code));
assert_eq!(result.unwrap_err().as_code(), code);
}
@@ -688,7 +688,7 @@
let out = split_metadata_result(Operation::StoreConsumerOffset, success_body(b"")).unwrap();
assert!(out.is_empty());
let out =
- split_metadata_result(Operation::DeleteConsumerOffset2, success_body(b"")).unwrap();
+ split_metadata_result(Operation::DeleteConsumerOffset, success_body(b"")).unwrap();
assert!(out.is_empty());
}
diff --git a/core/server/Cargo.toml b/core/server/Cargo.toml
index 1376a34..35cd5e5 100644
--- a/core/server/Cargo.toml
+++ b/core/server/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "server"
-version = "0.9.0-edge.4"
+version = "0.9.0-edge.5"
edition = "2024"
license = "Apache-2.0"
publish = false
diff --git a/core/server/src/auth.rs b/core/server/src/auth.rs
index 7af2e8c..f752df6 100644
--- a/core/server/src/auth.rs
+++ b/core/server/src/auth.rs
@@ -292,7 +292,7 @@
/// Terminal auth errors (`InvalidCredentials` / `InvalidToken` /
/// `UserInactive` / `Session`) fast-fail with a typed `Eviction` frame so the
/// SDK surfaces the real reason (every frame transport decodes
-/// `Command2::Eviction`) instead of a decode error or a timeout.
+/// `Command::Eviction`) instead of a decode error or a timeout.
#[allow(clippy::future_not_send)]
pub(crate) async fn surface_login_failure<B, MJ, S, SB>(
shard: &Rc<ShellShard<B, MJ, S, SB>>,
diff --git a/core/server/src/bootstrap.rs b/core/server/src/bootstrap.rs
index 03817cd..a78cc84 100644
--- a/core/server/src/bootstrap.rs
+++ b/core/server/src/bootstrap.rs
@@ -2436,16 +2436,12 @@
let segment_size = runtime_options
.segment_size
.unwrap_or_else(|| IggyByteSize::from(iggy_common::DEFAULT_SEGMENT_SIZE));
- let enforce_fsync = runtime_options
- .enforce_fsync
- .unwrap_or(iggy_common::DEFAULT_ENFORCE_FSYNC);
load_persisted_segments(
config,
stream_id,
topic_id,
partition_id,
segment_size,
- enforce_fsync,
stats,
)
.await
diff --git a/core/server/src/consumer_group.rs b/core/server/src/consumer_group.rs
index fd93e35..5eebb3e 100644
--- a/core/server/src/consumer_group.rs
+++ b/core/server/src/consumer_group.rs
@@ -37,8 +37,7 @@
LeaveConsumerGroupRequest as WireLeaveConsumerGroupRequest,
};
use iggy_binary_protocol::requests::consumer_offsets::{
- DeleteConsumerOffset2Request, DeleteConsumerOffsetRequest, StoreConsumerOffset2Request,
- StoreConsumerOffsetRequest,
+ DeleteConsumerOffsetRequest, StoreConsumerOffsetRequest,
};
use iggy_binary_protocol::{KIND_CONSUMER_GROUP, Operation, RoutedRequestHeader, WireIdentifier};
use iggy_common::IggyError;
@@ -220,18 +219,15 @@
let operation = request.header().operation;
if !matches!(
operation,
- Operation::StoreConsumerOffset
- | Operation::StoreConsumerOffset2
- | Operation::DeleteConsumerOffset
- | Operation::DeleteConsumerOffset2
+ Operation::StoreConsumerOffset | Operation::DeleteConsumerOffset
) {
return Ok(request);
}
let body = request_body(&request);
- // The 4 store/delete (v1 + v2) ops differ only in the decode type; this
- // collapses their identical decode -> resolve group id -> rewrite consumer
- // id -> re-encode bodies. A non-group consumer or unresolved group returns
- // the request untouched (the apply/read path handles the miss).
+ // The store/delete ops differ only in the decode type; this collapses
+ // their identical decode -> resolve group id -> rewrite consumer id ->
+ // re-encode bodies. A non-group consumer or unresolved group returns the
+ // request untouched (the apply/read path handles the miss).
macro_rules! rewrite_group_offset {
($ty:ty) => {{
let mut wire = <$ty>::decode_from(body).map_err(|_| IggyError::InvalidCommand)?;
@@ -250,10 +246,8 @@
}
let rewritten = match operation {
Operation::StoreConsumerOffset => rewrite_group_offset!(StoreConsumerOffsetRequest),
- Operation::StoreConsumerOffset2 => rewrite_group_offset!(StoreConsumerOffset2Request),
Operation::DeleteConsumerOffset => rewrite_group_offset!(DeleteConsumerOffsetRequest),
- Operation::DeleteConsumerOffset2 => rewrite_group_offset!(DeleteConsumerOffset2Request),
- // The outer `matches!` already filtered to the 4 ops above, but the
+ // The outer `matches!` already filtered to the 2 ops above, but the
// match is over the 37-variant `Operation`, so a catch-all is required.
_ => return Ok(request),
};
diff --git a/core/server/src/dispatch.rs b/core/server/src/dispatch.rs
index 4600ca0..9251bf9 100644
--- a/core/server/src/dispatch.rs
+++ b/core/server/src/dispatch.rs
@@ -68,7 +68,7 @@
use iggy_binary_protocol::primitives::polling_strategy::WirePollingStrategy;
use iggy_binary_protocol::requests::consumer_groups::SyncConsumerGroupRequest;
use iggy_binary_protocol::requests::consumer_offsets::{
- GetConsumerOffsetRequest, StoreConsumerOffset2Request,
+ GetConsumerOffsetRequest, StoreConsumerOffsetRequest,
};
use iggy_binary_protocol::requests::messages::PollMessagesRequest;
use iggy_binary_protocol::requests::partitions::{
@@ -88,7 +88,7 @@
use iggy_binary_protocol::responses::consumer_groups::SyncConsumerGroupResponse;
use iggy_binary_protocol::responses::system::get_snapshot::GetSnapshotResponse;
use iggy_binary_protocol::{
- AckLevel, ClientVersionInfo, Command2, ConsensusHeader, EvictionReason, ForwardLogoutHeader,
+ AckLevel, ClientVersionInfo, Command, ConsensusHeader, EvictionReason, ForwardLogoutHeader,
ForwardLogoutOutcome, ForwardLogoutResultHeader, ForwardRegisterHeader, ForwardRegisterOutcome,
ForwardRegisterResultHeader, GenericHeader, HEADER_SIZE, KIND_CONSUMER_GROUP,
MAX_PARTITIONS_PER_REQUEST, Operation, ProtocolVersion, RequestHeader, RoutedRequestHeader,
@@ -404,7 +404,7 @@
shard.dispatch(message.into_generic());
}
-/// Build the synthetic `StoreConsumerOffset2` request for an auto-commit, keyed
+/// Build the synthetic `StoreConsumerOffset` request for an auto-commit, keyed
/// to the resolved numeric consumer/group id and stamped with the reserved
/// [`AUTO_COMMIT_CLIENT_ID`] so the commit path skips the (unwaited) reply. The
/// wire stream/topic ids are cosmetic here -- admission and apply key off the
@@ -414,7 +414,7 @@
namespace: IggyNamespace,
applied: &AutoCommitApplied,
) -> Result<Message<RoutedRequestHeader>, IggyError> {
- let request = StoreConsumerOffset2Request {
+ let request = StoreConsumerOffsetRequest {
consumer: WireConsumer {
kind: applied.kind.as_code(),
id: WireIdentifier::Numeric(applied.consumer_id),
@@ -434,8 +434,8 @@
Ok(
message.transmute_header(|_, header: &mut RoutedRequestHeader| {
*header = RoutedRequestHeader {
- command: Command2::Request,
- operation: Operation::StoreConsumerOffset2,
+ command: Command::Request,
+ operation: Operation::StoreConsumerOffset,
size,
client: AUTO_COMMIT_CLIENT_ID,
// The partition plane is sessionless (no `ClientTable` dedup); a
@@ -2840,7 +2840,7 @@
) -> Message<ForwardRegisterHeader> {
Message::<ForwardRegisterHeader>::new(HEADER_SIZE).transmute_header(
|_, header: &mut ForwardRegisterHeader| {
- header.command = Command2::ForwardRegister;
+ header.command = Command::ForwardRegister;
header.cluster = cluster;
header.view = view;
header.replica = replica;
@@ -2865,7 +2865,7 @@
let (session, outcome) = forward_register_outcome(bound);
Message::<ForwardRegisterResultHeader>::new(HEADER_SIZE).transmute_header(
|_, header: &mut ForwardRegisterResultHeader| {
- header.command = Command2::ForwardRegisterResult;
+ header.command = Command::ForwardRegisterResult;
header.cluster = cluster;
header.view = view;
header.replica = replica;
@@ -3029,7 +3029,7 @@
) -> Message<ForwardLogoutHeader> {
Message::<ForwardLogoutHeader>::new(HEADER_SIZE).transmute_header(
|_, header: &mut ForwardLogoutHeader| {
- header.command = Command2::ForwardLogout;
+ header.command = Command::ForwardLogout;
header.cluster = cluster;
header.view = view;
header.replica = replica;
@@ -3055,7 +3055,7 @@
let (commit, outcome) = forward_logout_outcome(result);
Message::<ForwardLogoutResultHeader>::new(HEADER_SIZE).transmute_header(
|_, header: &mut ForwardLogoutResultHeader| {
- header.command = Command2::ForwardLogoutResult;
+ header.command = Command::ForwardLogoutResult;
header.cluster = cluster;
header.view = view;
header.replica = replica;
@@ -4028,7 +4028,7 @@
client,
request: 0,
commit: session,
- command: Command2::Reply,
+ command: Command::Reply,
operation: Operation::Register,
..Default::default()
};
@@ -4051,7 +4051,7 @@
let header =
bytemuck::checked::from_bytes_mut::<RoutedRequestHeader>(&mut slice[..header_size]);
*header = RoutedRequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation,
size: u32::try_from(total).expect("test request fits u32"),
client,
@@ -4083,7 +4083,7 @@
let header =
bytemuck::checked::from_bytes_mut::<PrepareHeader>(&mut slice[..header_size]);
*header = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
operation,
size: u32::try_from(total).expect("test prepare fits u32"),
op: 1,
@@ -4559,7 +4559,7 @@
await_forward(&bus).await;
let (target, forward) = bus.sole_replica_send::<ForwardRegisterHeader>();
assert_eq!(target, 0, "forward must address the view's primary");
- assert_eq!(forward.command, Command2::ForwardRegister);
+ assert_eq!(forward.command, Command::ForwardRegister);
assert_eq!(forward.client, CLIENT);
assert_eq!(
forward.user_id, USER,
@@ -4605,7 +4605,7 @@
await_forward(&bus).await;
let (target, forward) = bus.sole_replica_send::<ForwardLogoutHeader>();
assert_eq!(target, 0, "forward must address the view's primary");
- assert_eq!(forward.command, Command2::ForwardLogout);
+ assert_eq!(forward.command, Command::ForwardLogout);
assert_eq!(forward.client, CLIENT);
assert_eq!(forward.session, SESSION);
assert_eq!(forward.request, REQUEST);
@@ -4944,7 +4944,7 @@
&mut message.as_mut_slice()[..header_size],
);
*header = RequestHeader {
- command: Command2::Request,
+ command: Command::Request,
operation: Operation::NonReplicated,
size: u32::try_from(header_size).expect("header fits u32"),
client: TRANSPORT,
@@ -5002,7 +5002,7 @@
assert_eq!(*client, TRANSPORT);
assert_eq!(
frame[COMMAND_OFFSET],
- Command2::Reply as u8,
+ Command::Reply as u8,
"an unbound cluster-metadata read must be denied with a Reply, not evicted"
);
let status =
diff --git a/core/server/src/dispatch/authz.rs b/core/server/src/dispatch/authz.rs
index 424572c..73cfc09 100644
--- a/core/server/src/dispatch/authz.rs
+++ b/core/server/src/dispatch/authz.rs
@@ -87,13 +87,13 @@
Operation::SendMessages => {
permissioner.append_messages(user_id, stream_id, topic_id)
}
- Operation::StoreConsumerOffset | Operation::StoreConsumerOffset2 => {
+ Operation::StoreConsumerOffset => {
permissioner.store_consumer_offset(user_id, stream_id, topic_id)
}
- Operation::DeleteConsumerOffset | Operation::DeleteConsumerOffset2 => {
+ Operation::DeleteConsumerOffset => {
permissioner.delete_consumer_offset(user_id, stream_id, topic_id)
}
- // The caller only routes the five partition ops above here. The
+ // The caller only routes the three partition ops above here. The
// rest are listed exhaustively (no `_`) so a newly added op
// forces a gate decision at compile time instead of silently
// slipping through ungated.
diff --git a/core/server/src/http/handlers.rs b/core/server/src/http/handlers.rs
index e138249..c1172a4 100644
--- a/core/server/src/http/handlers.rs
+++ b/core/server/src/http/handlers.rs
@@ -1346,7 +1346,7 @@
///
/// Data plane like a produce: the offset write is a replicated op on the
/// partition group's own consensus, awaited through the session's in-process
-/// reply slot ([`partition_write_replicated`]). The v2 wire op is pinned to
+/// reply slot ([`partition_write_replicated`]). The wire op is pinned to
/// `ack = Quorum` - `?ack=none` is a produce-only surface. The consumer
/// identifier passes through on the wire; the dispatch resolvers hash named
/// consumers and rewrite group ids server-side, identically to TCP.
@@ -1374,7 +1374,7 @@
SendWrapper::new(partition_write_replicated(
&state,
&identity.session,
- Operation::StoreConsumerOffset2,
+ Operation::StoreConsumerOffset,
&body,
))
.await?;
@@ -1417,7 +1417,7 @@
SendWrapper::new(partition_write_replicated(
&state,
&identity.session,
- Operation::DeleteConsumerOffset2,
+ Operation::DeleteConsumerOffset,
&body,
))
.await?;
diff --git a/core/server/src/http/reply.rs b/core/server/src/http/reply.rs
index 9c3a59f..c17f5ce 100644
--- a/core/server/src/http/reply.rs
+++ b/core/server/src/http/reply.rs
@@ -20,7 +20,7 @@
//! login error mappings the write and auth paths render through.
use iggy_binary_protocol::consensus::{
- Command2, EvictionHeader, HEADER_SIZE, result_code, result_section_len,
+ Command, EvictionHeader, HEADER_SIZE, result_code, result_section_len,
};
use iggy_binary_protocol::responses::consumer_groups::get_consumer_group::ConsumerGroupDetailsResponse;
use iggy_binary_protocol::responses::messages::SendMessagesResponse;
@@ -67,7 +67,7 @@
.get(..HEADER_SIZE)
.and_then(|bytes| bytemuck::checked::try_from_bytes::<ReplyHeader>(bytes).ok())
.ok_or(PartitionWriteError::Rejected(IggyError::InvalidCommand))?;
- if header.command != Command2::Reply {
+ if header.command != Command::Reply {
return Err(PartitionWriteError::Rejected(IggyError::InvalidCommand));
}
if header.status != 0 {
@@ -296,7 +296,7 @@
"status carries the deny code"
);
assert_ne!(status, 0, "a deny status is nonzero so the SDK peek fires");
- assert_eq!(reply.header().command, Command2::Reply);
+ assert_eq!(reply.header().command, Command::Reply);
// Echoes the request so the SDK routes it back to the waiting slot.
assert_eq!(reply.header().request, request.header().request);
assert_eq!(reply.header().operation, request.header().operation);
@@ -338,7 +338,7 @@
// The consensus plane's committed-reply builder (plane_helpers path).
let prepare = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
operation: Operation::SendMessages,
client: 42,
op: 1,
@@ -369,7 +369,7 @@
#[test]
fn committed_partition_reply_classifies_as_success() {
let prepare = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
operation: Operation::SendMessages,
client: 42,
op: 1,
@@ -381,12 +381,12 @@
}
/// The 204 path of the offset write routes: a committed
- /// `StoreConsumerOffset2` reply (op >= 1) classifies as success.
+ /// `StoreConsumerOffset` reply (op >= 1) classifies as success.
#[test]
fn committed_offset_write_reply_classifies_as_success() {
let prepare = PrepareHeader {
- command: Command2::Prepare,
- operation: Operation::StoreConsumerOffset2,
+ command: Command::Prepare,
+ operation: Operation::StoreConsumerOffset,
client: 42,
op: 3,
request: 2,
@@ -488,7 +488,7 @@
fn send_reply(body: &Bytes) -> BusMessage {
let prepare = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
operation: Operation::SendMessages,
client: 42,
op: 1,
@@ -554,7 +554,7 @@
/// mapped `IggyError`, not as the generic op-0 not-found.
#[test]
fn status_bearing_deny_reply_classifies_as_typed_rejection() {
- let request = build_request_message(Operation::DeleteConsumerOffset2, 42, 7, 1, &[]);
+ let request = build_request_message(Operation::DeleteConsumerOffset, 42, 7, 1, &[]);
let mut deny = build_empty_reply(request.header(), 42, 0, 9);
let header = bytemuck::checked::try_from_bytes_mut::<ReplyHeader>(
&mut deny.as_mut_slice()[..HEADER_SIZE],
diff --git a/core/server/src/http/submit.rs b/core/server/src/http/submit.rs
index f9543cd..ca763d8 100644
--- a/core/server/src/http/submit.rs
+++ b/core/server/src/http/submit.rs
@@ -25,7 +25,7 @@
use bytes::Bytes;
use consensus::MetadataHandle;
use futures::channel::oneshot;
-use iggy_binary_protocol::consensus::Command2;
+use iggy_binary_protocol::consensus::Command;
use iggy_binary_protocol::{GenericHeader, Operation, ReplyHeader, RoutedRequestHeader};
use iggy_common::IggyError;
use message_bus::BusMessage;
@@ -247,7 +247,7 @@
let Some(reply) = submit_client_request_on_owner(shard, request).await else {
return Err(WriteError::Unavailable);
};
- let transient = (reply.header().command == Command2::Reply)
+ let transient = (reply.header().command == Command::Reply)
.then(|| transient_code(&reply))
.flatten();
let Some(transient) = transient else {
@@ -283,13 +283,13 @@
};
match reply.header().command {
- Command2::Reply => {
+ Command::Reply => {
// Already burned at stamp time; release the gate so the next
// write on this session can take its turn.
drop(next_request_id);
Ok((request_header, reply, raw_token))
}
- Command2::Eviction => Err(WriteError::Evicted(eviction_error(&reply))),
+ Command::Eviction => Err(WriteError::Evicted(eviction_error(&reply))),
_ => Err(WriteError::Rejected(IggyError::InvalidCommand)),
}
}
diff --git a/core/server/src/http/wire.rs b/core/server/src/http/wire.rs
index c7026d0..20ada0c 100644
--- a/core/server/src/http/wire.rs
+++ b/core/server/src/http/wire.rs
@@ -19,11 +19,11 @@
//! the control-plane [`Message<RoutedRequestHeader>`] builder shared by the write path.
use bytes::{Bytes, BytesMut};
-use iggy_binary_protocol::consensus::{Command2, HEADER_SIZE};
+use iggy_binary_protocol::consensus::{Command, HEADER_SIZE};
use iggy_binary_protocol::primitives::consumer::WireConsumer;
use iggy_binary_protocol::primitives::polling_strategy::WirePollingStrategy;
use iggy_binary_protocol::requests::consumer_offsets::{
- DeleteConsumerOffset2Request, GetConsumerOffsetRequest, StoreConsumerOffset2Request,
+ DeleteConsumerOffsetRequest, GetConsumerOffsetRequest, StoreConsumerOffsetRequest,
};
use iggy_binary_protocol::requests::messages::{
PollMessagesRequest, RawMessage, SendMessagesEncoder,
@@ -58,7 +58,14 @@
let raw_messages: Vec<RawMessage<'_>> = views
.iter()
.map(|view| RawMessage {
- id: view.header().id(),
+ // HTTP producers send no id; the producer side owns minting now
+ // (the id sits under the frame checksum), and for JSON bodies
+ // this handler IS the producer encoder.
+ id: if view.header().id() == 0 {
+ iggy_common::random_id::get_uuid()
+ } else {
+ view.header().id()
+ },
origin_timestamp: view.header().origin_timestamp(),
headers: view.user_headers(),
payload: view.payload(),
@@ -77,7 +84,13 @@
&wire_topic_id,
&wire_partitioning,
&raw_messages,
- );
+ )
+ .map_err(|error| match error {
+ iggy_binary_protocol::WireError::InvalidMessageTimestampDelta(delta) => {
+ IggyError::InvalidMessageTimestampDelta(delta)
+ }
+ _ => IggyError::InvalidCommand,
+ })?;
Ok(buf.freeze())
}
@@ -128,8 +141,8 @@
})
}
-/// Map a validated HTTP store-offset body onto the v2 wire request
-/// (`StoreConsumerOffset2Request`), `ack` pinned to `Quorum` so the route can
+/// Map a validated HTTP store-offset body onto the wire request
+/// (`StoreConsumerOffsetRequest`), `ack` pinned to `Quorum` so the route can
/// await the committed reply. The body's consumer kind is structurally always
/// `Consumer` (`Consumer::kind` is `#[serde(skip)]`), matching the legacy HTTP
/// server; `partition_id` passes through as the wire `Option` (flag byte +
@@ -138,8 +151,8 @@
stream_id: &Identifier,
topic_id: &Identifier,
command: &StoreConsumerOffset,
-) -> Result<StoreConsumerOffset2Request, IggyError> {
- Ok(StoreConsumerOffset2Request {
+) -> Result<StoreConsumerOffsetRequest, IggyError> {
+ Ok(StoreConsumerOffsetRequest {
consumer: consumer_to_wire(&command.consumer)?,
stream_id: identifier_to_wire(stream_id)?,
topic_id: identifier_to_wire(topic_id)?,
@@ -149,16 +162,16 @@
})
}
-/// Map a validated HTTP delete-offset request onto the v2 wire request
-/// (`DeleteConsumerOffset2Request`), `ack` pinned to `Quorum` like
+/// Map a validated HTTP delete-offset request onto the wire request
+/// (`DeleteConsumerOffsetRequest`), `ack` pinned to `Quorum` like
/// [`store_offset_wire_request`].
pub(in crate::http) fn delete_offset_wire_request(
stream_id: &Identifier,
topic_id: &Identifier,
consumer: &Consumer,
partition_id: Option<u32>,
-) -> Result<DeleteConsumerOffset2Request, IggyError> {
- Ok(DeleteConsumerOffset2Request {
+) -> Result<DeleteConsumerOffsetRequest, IggyError> {
+ Ok(DeleteConsumerOffsetRequest {
consumer: consumer_to_wire(consumer)?,
stream_id: identifier_to_wire(stream_id)?,
topic_id: identifier_to_wire(topic_id)?,
@@ -197,7 +210,7 @@
&mut message.as_mut_slice()[..HEADER_SIZE],
)
.expect("zeroed bytes form a valid RoutedRequestHeader");
- header.command = Command2::Request;
+ header.command = Command::Request;
header.operation = operation;
header.client = client_id;
header.session = session_id;
@@ -214,34 +227,31 @@
use iggy_binary_protocol::WireDecode;
use iggy_binary_protocol::WireEncode;
- use iggy_binary_protocol::WireMessageIterator;
- use iggy_binary_protocol::message_layout::WIRE_MESSAGE_INDEX_SIZE;
- use iggy_binary_protocol::requests::messages::SendMessagesHeader;
use iggy_common::delete_consumer_offset::DeleteConsumerOffset;
use iggy_common::{
- Consumer, ConsumerKind, IggyMessage, IggyMessagesBatch, IggyTimestamp, Partitioning,
- PartitioningKind, PollingKind, PollingStrategy, Validatable,
+ Consumer, ConsumerKind, IggyMessagesBatch, IggyTimestamp, Partitioning, PartitioningKind,
+ PollingKind, PollingStrategy, Validatable,
};
use partitions::{Fragment, PollFragments};
use server_common::MESSAGE_ALIGN;
use server_common::iobuf::Owned;
- use server_common::send_messages2::{
- COMMAND_HEADER_SIZE, IggyMessage2, IggyMessage2Header, IggyMessages2, SendMessages2Header,
- SendMessages2Owned,
+ use server_common::send_messages::{
+ BatchHeader, COMMAND_HEADER_SIZE, IggyMessage, IggyMessageHeader, IggyMessages,
+ SendMessagesOwned,
};
use crate::http::error::{Consistency, ConsistencyQuery};
use crate::responses::build_polled_messages_body;
fn produce_command(partitioning: Partitioning) -> SendMessages {
- let first = IggyMessage::builder()
+ let first = iggy_common::IggyMessage::builder()
.id(7)
.payload(Bytes::from_static(b"first"))
.build()
.expect("valid message");
// Raw pre-encoded user headers, mirroring the HTTP deserializer's
// base64 branch.
- let mut second = IggyMessage::builder()
+ let mut second = iggy_common::IggyMessage::builder()
.id(8)
.payload(Bytes::from_static(b"second"))
.build()
@@ -274,7 +284,10 @@
let metadata_length =
u32::from_le_bytes(bytes[..4].try_into().expect("length prefix")) as usize;
let (header, consumed) =
- SendMessagesHeader::decode(&bytes[4..4 + metadata_length]).expect("valid metadata");
+ iggy_binary_protocol::requests::messages::SendMessagesHeader::decode(
+ &bytes[4..4 + metadata_length],
+ )
+ .expect("valid metadata");
assert_eq!(consumed, metadata_length);
assert_eq!(header.stream_id, identifier_to_wire(&stream_id).unwrap());
assert_eq!(header.topic_id, identifier_to_wire(&topic_id).unwrap());
@@ -284,18 +297,53 @@
);
assert_eq!(header.messages_count, 2);
- let data_offset = 4 + metadata_length + 2 * WIRE_MESSAGE_INDEX_SIZE;
- let views: Vec<_> = WireMessageIterator::new(&bytes[data_offset..], 2)
- .collect::<Result<Vec<_>, _>>()
- .expect("valid message frames");
- assert_eq!(views[0].id(), 7);
- assert_eq!(views[0].payload(), b"first");
- assert_eq!(views[0].user_headers(), b"");
- assert_eq!(views[0].origin_timestamp(), origin_timestamps[0]);
- assert_eq!(views[1].id(), 8);
- assert_eq!(views[1].payload(), b"second");
- assert_eq!(views[1].user_headers(), b"raw-header-bytes");
- assert_eq!(views[1].origin_timestamp(), origin_timestamps[1]);
+ let batch = iggy_binary_protocol::batch::decode_batch_slice(&bytes[4 + metadata_length..])
+ .expect("valid producer batch");
+ assert_eq!(batch.header.partition_id, 0);
+ assert_eq!(batch.message_count(), 2);
+ let views: Vec<_> = batch.iter().collect();
+ let batch_origin = batch.header.origin_timestamp;
+ assert_eq!(views[0].header.id, 7);
+ assert_eq!(views[0].payload, b"first");
+ assert_eq!(views[0].user_headers, b"");
+ assert_eq!(
+ batch_origin + u64::from(views[0].header.timestamp_delta),
+ origin_timestamps[0]
+ );
+ assert_eq!(views[1].header.id, 8);
+ assert_eq!(views[1].payload, b"second");
+ assert_eq!(views[1].user_headers, b"raw-header-bytes");
+ assert_eq!(
+ batch_origin + u64::from(views[1].header.timestamp_delta),
+ origin_timestamps[1]
+ );
+ }
+
+ #[test]
+ fn encode_send_messages_mints_ids_for_zero_id_messages() {
+ // JSON producers send no id; the frame checksum covers the id field,
+ // so this handler must mint before encoding - the server no longer
+ // assigns ids at admission.
+ let stream_id = Identifier::from_str_value("1").expect("valid stream id");
+ let topic_id = Identifier::from_str_value("orders").expect("valid topic id");
+ let message = iggy_common::IggyMessage::builder()
+ .payload(Bytes::from_static(b"no-id"))
+ .build()
+ .expect("valid message");
+ assert_eq!(message.header.id, 0, "builder default id must be zero");
+ let command = SendMessages {
+ partitioning: Partitioning::partition_id(1),
+ batch: IggyMessagesBatch::from(&vec![message]),
+ ..Default::default()
+ };
+
+ let bytes = encode_send_messages(&stream_id, &topic_id, &command).expect("encodes");
+ let metadata_length =
+ u32::from_le_bytes(bytes[..4].try_into().expect("length prefix")) as usize;
+ let batch = iggy_binary_protocol::batch::decode_batch_slice(&bytes[4 + metadata_length..])
+ .expect("valid producer batch");
+ let views: Vec<_> = batch.iter().collect();
+ assert_ne!(views[0].header.id, 0, "zero id must be minted at encode");
}
#[test]
@@ -402,7 +450,7 @@
let bytes = request.to_bytes();
assert_eq!(*bytes.last().expect("non-empty"), AckLevel::Quorum.as_u8());
let (decoded, consumed) =
- StoreConsumerOffset2Request::decode(&bytes).expect("decodes as the server does");
+ StoreConsumerOffsetRequest::decode(&bytes).expect("decodes as the server does");
assert_eq!(consumed, bytes.len());
assert_eq!(decoded, request);
assert_eq!(decoded.consumer.kind, 1);
@@ -423,7 +471,7 @@
let request = store_offset_wire_request(&stream_id, &topic_id, &command).expect("maps");
let bytes = request.to_bytes();
let (decoded, consumed) =
- StoreConsumerOffset2Request::decode(&bytes).expect("decodes as the server does");
+ StoreConsumerOffsetRequest::decode(&bytes).expect("decodes as the server does");
assert_eq!(consumed, bytes.len());
assert_eq!(decoded.partition_id, None);
assert_eq!(decoded.offset, u64::MAX);
@@ -445,7 +493,7 @@
let bytes = request.to_bytes();
assert_eq!(*bytes.last().expect("non-empty"), AckLevel::Quorum.as_u8());
let (decoded, consumed) =
- DeleteConsumerOffset2Request::decode(&bytes).expect("decodes as the server does");
+ DeleteConsumerOffsetRequest::decode(&bytes).expect("decodes as the server does");
assert_eq!(consumed, bytes.len());
assert_eq!(decoded, request);
assert_eq!(decoded.consumer.kind, 1);
@@ -464,10 +512,10 @@
assert_eq!(query.partition_id, None);
}
- /// Wrap one stored `SendMessages2` batch (`[256B header][blob]`) as the
+ /// Wrap one stored `SendMessages` batch (`[256B header][blob]`) as the
/// poll fragment the owning shard replies, the shape
/// `build_polled_messages_body` consumes.
- fn fragment_from_stored_batch(header: &SendMessages2Header, blob: &[u8]) -> PollFragments {
+ fn fragment_from_stored_batch(header: &BatchHeader, blob: &[u8]) -> PollFragments {
let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(COMMAND_HEADER_SIZE + blob.len());
header.encode_into(buffer.as_mut_slice());
buffer.as_mut_slice()[COMMAND_HEADER_SIZE..].copy_from_slice(blob);
@@ -477,14 +525,14 @@
}
/// Round-trip the poll route's encode/decode seam: the store's own batch
- /// writer (`SendMessages2Owned::from_messages`) is the encoder oracle,
+ /// writer (`SendMessagesOwned::from_messages`) is the encoder oracle,
/// `build_polled_messages_body` re-encodes to the legacy wire body, and
/// the SDK's `PolledMessages::from_bytes` must read back every field.
#[test]
fn polled_messages_body_decodes_into_common_polled_messages() {
- let mut messages = IggyMessages2::with_capacity(2);
- messages.push(IggyMessage2 {
- header: IggyMessage2Header {
+ let mut messages = IggyMessages::with_capacity(2);
+ messages.push(IggyMessage {
+ header: IggyMessageHeader {
id: 7,
origin_timestamp: 1_000,
..Default::default()
@@ -492,8 +540,8 @@
payload: Bytes::from_static(b"first"),
user_headers: None,
});
- messages.push(IggyMessage2 {
- header: IggyMessage2Header {
+ messages.push(IggyMessage {
+ header: IggyMessageHeader {
id: 8,
origin_timestamp: 1_050,
..Default::default()
@@ -503,7 +551,7 @@
});
let namespace = server_common::sharding::IggyNamespace::new(0, 0, 3);
let stored =
- SendMessages2Owned::from_messages(namespace, &messages).expect("encodes stored batch");
+ SendMessagesOwned::from_messages(namespace, &messages).expect("encodes stored batch");
// The store stamps these on append; `from_messages` leaves them zero.
let mut header = stored.header;
header.base_offset = 41;
diff --git a/core/server/src/partition_helpers.rs b/core/server/src/partition_helpers.rs
index 76ddab4..8fb4a0e 100644
--- a/core/server/src/partition_helpers.rs
+++ b/core/server/src/partition_helpers.rs
@@ -341,26 +341,18 @@
// this with a stale `.index` at offset 0 on disk. The `partitions`-side
// writers with the same names do NOT truncate, so opening them directly
// instead would read index entries from a previous generation.
- let storage = SegmentStorage::new(
- &messages_path,
- &index_path,
- 0,
- 0,
- enforce_fsync,
- enforce_fsync,
- false,
- )
- .await
- .map_err(|source| {
- error!(
- stream_id,
- topic_id,
- partition_id,
- error = %source,
- "failed to create initial segment storage"
- );
- source
- })?;
+ let storage = SegmentStorage::new(&messages_path, &index_path, 0, 0, false)
+ .await
+ .map_err(|source| {
+ error!(
+ stream_id,
+ topic_id,
+ partition_id,
+ error = %source,
+ "failed to create initial segment storage"
+ );
+ source
+ })?;
// Share the storage's size counters so reads observe persisted bytes;
// a writer with a private counter grows the file invisibly to readers.
let messages_size_counter = storage
diff --git a/core/server/src/partition_reconciler.rs b/core/server/src/partition_reconciler.rs
index 4c3aeec..b456a30 100644
--- a/core/server/src/partition_reconciler.rs
+++ b/core/server/src/partition_reconciler.rs
@@ -1183,7 +1183,7 @@
PurgeTopicRequest,
};
use iggy_binary_protocol::{
- Command2, Operation, PrepareHeader, ReplyHeader, RoutedRequestHeader, WireIdentifier,
+ Command, Operation, PrepareHeader, ReplyHeader, RoutedRequestHeader, WireIdentifier,
WireOptions,
};
use message_bus::IggyMessageBus;
@@ -1250,7 +1250,7 @@
&mut msg.as_mut_slice()[..header_size],
)
.expect("zeroed bytes form a valid PrepareHeader");
- header.command = Command2::Prepare;
+ header.command = Command::Prepare;
header.size = u32::try_from(total_size).expect("prepare size fits u32");
header.op = op;
header.operation = operation;
@@ -1267,7 +1267,7 @@
&mut msg.as_mut_slice()[..header_size],
)
.expect("zeroed bytes form a valid PrepareHeader");
- header.command = Command2::Prepare;
+ header.command = Command::Prepare;
header.size = u32::try_from(header_size).expect("prepare size fits u32");
header.operation = Operation::SendMessages;
header.group = namespace.inner();
@@ -1306,7 +1306,7 @@
&mut msg.as_mut_slice()[..header_size],
)
.expect("zeroed bytes form a valid RoutedRequestHeader");
- header.command = Command2::Request;
+ header.command = Command::Request;
header.size = u32::try_from(total_size).expect("request size fits u32");
header.operation = Operation::SendMessages;
header.group = namespace.inner();
diff --git a/core/server/src/responses.rs b/core/server/src/responses.rs
index f0edd5d..6b7fc4e 100644
--- a/core/server/src/responses.rs
+++ b/core/server/src/responses.rs
@@ -42,8 +42,7 @@
GetConsumerGroupRequest, GetConsumerGroupsRequest,
};
use iggy_binary_protocol::requests::consumer_offsets::{
- DeleteConsumerOffset2Request, DeleteConsumerOffsetRequest, StoreConsumerOffset2Request,
- StoreConsumerOffsetRequest,
+ DeleteConsumerOffsetRequest, StoreConsumerOffsetRequest,
};
use iggy_binary_protocol::requests::messages::SendMessagesHeader;
use iggy_binary_protocol::requests::personal_access_tokens::GetPersonalAccessTokensRequest;
@@ -79,7 +78,7 @@
use iggy_binary_protocol::responses::users::get_users::GetUsersResponse;
use iggy_binary_protocol::responses::users::user_response::UserResponse;
use iggy_binary_protocol::{
- Command2, GenericHeader, IGGY_PROTOCOL_VERSION, KIND_CONSUMER_GROUP, Operation, ReplyHeader,
+ Command, GenericHeader, IGGY_PROTOCOL_VERSION, KIND_CONSUMER_GROUP, Operation, ReplyHeader,
RoutedRequestHeader, WireDecode, WireEncode, WireIdentifier, WireName, WirePartitioning,
};
use iggy_common::wire_conversions::{resource_options_to_wire, resource_options_to_wire_split};
@@ -92,7 +91,7 @@
use metadata::impls::metadata::StreamsFrontend;
use partitions::PollFragments;
use server_common::Message;
-use server_common::send_messages2::{COMMAND_HEADER_SIZE, SendMessages2Header};
+use server_common::send_messages;
use server_common::sharding::IggyNamespace;
use shard::ConnectedClientInfo;
use std::cell::RefCell;
@@ -381,30 +380,6 @@
client_id,
)?
}
- Operation::StoreConsumerOffset2 => {
- let request = StoreConsumerOffset2Request::decode_from(body)
- .map_err(|_| IggyError::InvalidCommand)?;
- fence_and_resolve_offset_namespace(
- shard,
- &request.consumer,
- &request.stream_id,
- &request.topic_id,
- request.partition_id,
- client_id,
- )?
- }
- Operation::DeleteConsumerOffset2 => {
- let request = DeleteConsumerOffset2Request::decode_from(body)
- .map_err(|_| IggyError::InvalidCommand)?;
- fence_and_resolve_offset_namespace(
- shard,
- &request.consumer,
- &request.stream_id,
- &request.topic_id,
- request.partition_id,
- client_id,
- )?
- }
Operation::DeleteSegments => {
let request =
DeleteSegmentsRequest::decode_from(body).map_err(|_| IggyError::InvalidCommand)?;
@@ -1549,7 +1524,7 @@
// both swallow the eviction and ship a raw token whose hash never
// committed. Only rewrite a genuine committed `Reply`; pass anything else
// (the eviction) through untouched so the client learns its session died.
- if committed.header().command != Command2::Reply {
+ if committed.header().command != Command::Reply {
return Ok(committed);
}
let header_len = std::mem::size_of::<ReplyHeader>();
@@ -1609,7 +1584,7 @@
size: header_size,
view: request_header.view,
release: request_header.release,
- command: Command2::Reply,
+ command: Command::Reply,
replica: request_header.replica,
request_checksum: request_header.request_checksum,
client: client_id,
@@ -1640,24 +1615,17 @@
.map_or(0, VsrConsensus::commit_max)
}
-/// Size of the in-storage (`IggyMessage2`) per-message header inside a
-/// `SendMessages2` batch blob: `checksum`(8) + `id`(16) + `offset_delta`(4)
-/// + `timestamp_delta`(4) + `user_headers_length`(4) + `payload_length`(4)
-/// + reserved(8). See `server_common::send_messages2::SendMessages2Owned::from_messages`.
-const STORED_MESSAGE_HEADER_SIZE: usize = 48;
-
/// Build the `PolledMessages` reply body from the owning shard's poll
/// fragments.
///
-/// Fragments carry the stored `SendMessages2` batches: a 256-byte command
-/// header followed by `IggyMessage2`-format messages
-/// (`[48B header][payload][user_headers]`, offsets/timestamps delta-encoded
-/// against the batch). The SDK decodes the legacy wire format
-/// (`[64B header][payload][user_headers]`, absolute offsets); the message
-/// sections share the legacy order, so only the header is re-encoded here
-/// and the section bytes copy through contiguously.
+/// Fragments carry the stored batch records (a 256-byte batch header plus
+/// `[48B header][payload][user_headers]` frames, deltas resolved against the
+/// stamped bases) and are served to the client as they are - the reply's
+/// message encoding IS the storage encoding. The one rewrite left is at-rest
+/// decryption: stored sections are ciphertext, and this reply is the single
+/// decrypt point, so encrypted records are rebuilt over the plaintext.
///
-/// Body layout: `[partition_id:4][current_offset:8][count:4][messages...]`.
+/// Body layout: `[partition_id:4][current_offset:8][count:4][batch records...]`.
pub(crate) fn build_polled_messages_body(
partition_id: u32,
current_offset: u64,
@@ -1668,17 +1636,14 @@
// COUNT_OFFSET and is backpatched once the walk below knows it.
const HEAD_LEN: usize = 16;
const COUNT_OFFSET: usize = 12;
- // Batches may arrive split across fragments (rewritten command header +
- // sliced blob); concatenate into one stream before walking batches.
+ // Batches may arrive split across fragments (rewritten batch header +
+ // sliced blob); concatenate into one stream before walking records.
let mut stream: Vec<u8> = Vec::new();
for fragment in fragments {
let frozen = fragment.into_frozen();
stream.extend_from_slice(frozen.as_slice());
}
- // Reserve the head and encode the messages straight into `body`; encoding
- // directly avoids a separate messages buffer and its copy-through into the
- // head.
let mut body: Vec<u8> = Vec::with_capacity(HEAD_LEN + stream.len());
body.extend_from_slice(&partition_id.to_le_bytes());
body.extend_from_slice(¤t_offset.to_le_bytes());
@@ -1686,104 +1651,24 @@
let mut count: u32 = 0;
let mut position = 0usize;
while position < stream.len() {
- let batch = SendMessages2Header::decode(&stream[position..])?;
+ let batch = send_messages::BatchHeader::decode(&stream[position..])
+ .map_err(|_| IggyError::InvalidCommand)?;
let batch_end = position
- .checked_add(
- usize::try_from(batch.batch_length).map_err(|_| IggyError::InvalidCommand)?,
- )
+ .checked_add(batch.total_size())
.ok_or(IggyError::InvalidCommand)?;
if batch_end > stream.len() {
return Err(IggyError::InvalidCommand);
}
- let mut cursor = position + COMMAND_HEADER_SIZE;
- while cursor < batch_end {
- if cursor + STORED_MESSAGE_HEADER_SIZE > batch_end {
- return Err(IggyError::InvalidCommand);
- }
- let header = &stream[cursor..cursor + STORED_MESSAGE_HEADER_SIZE];
- let checksum = &header[0..8];
- let id = &header[8..24];
- let offset_delta = u32::from_le_bytes(header[24..28].try_into().expect("4-byte slice"));
- let timestamp_delta =
- u32::from_le_bytes(header[28..32].try_into().expect("4-byte slice"));
- let user_headers_length =
- u32::from_le_bytes(header[32..36].try_into().expect("4-byte slice")) as usize;
- let payload_length =
- u32::from_le_bytes(header[36..40].try_into().expect("4-byte slice")) as usize;
-
- let sections_start = cursor + STORED_MESSAGE_HEADER_SIZE;
- let sections_end = sections_start + payload_length + user_headers_length;
- if sections_end > batch_end {
- return Err(IggyError::InvalidCommand);
- }
-
- let offset = batch.base_offset + u64::from(offset_delta);
- // `base_timestamp` is the flat broker append time stamped once per
- // batch; `timestamp_delta` is a per-message delta against the
- // producer origin, so it only applies to `origin_timestamp`. Adding
- // it to the broker base would mix two clocks.
- let timestamp = batch.base_timestamp;
- let origin_timestamp = batch.origin_timestamp + u64::from(timestamp_delta);
-
- body.extend_from_slice(checksum);
- body.extend_from_slice(id);
- body.extend_from_slice(&offset.to_le_bytes());
- body.extend_from_slice(×tamp.to_le_bytes());
- body.extend_from_slice(&origin_timestamp.to_le_bytes());
- if let Some(encryptor) = encryptor {
- // At-rest encryption: stored sections are ciphertext (encrypted
- // once at ingestion, replicated verbatim); this reply is the
- // single decrypt point, so lengths are rewritten to the
- // plaintext sizes. The stored per-message checksum still covers
- // the ciphertext and is passed through untouched (the SDK does
- // not re-validate it against the reply layout).
- let payload_end = sections_start + payload_length;
- let payload = encryptor
- .decrypt(&stream[sections_start..payload_end])
- .map_err(|_| IggyError::CannotDecryptData)?;
- let user_headers = if user_headers_length > 0 {
- Some(
- encryptor
- .decrypt(&stream[payload_end..sections_end])
- .map_err(|_| IggyError::CannotDecryptData)?,
- )
- } else {
- None
- };
- let user_headers_bytes: &[u8] = user_headers.as_deref().unwrap_or_default();
- body.extend_from_slice(
- &u32::try_from(user_headers_bytes.len())
- .map_err(|_| IggyError::InvalidCommand)?
- .to_le_bytes(),
- );
- body.extend_from_slice(
- &u32::try_from(payload.len())
- .map_err(|_| IggyError::InvalidCommand)?
- .to_le_bytes(),
- );
- body.extend_from_slice(&0u64.to_le_bytes()); // reserved
- body.extend_from_slice(&payload);
- body.extend_from_slice(user_headers_bytes);
- } else {
- body.extend_from_slice(
- &u32::try_from(user_headers_length)
- .expect("length came from u32")
- .to_le_bytes(),
- );
- body.extend_from_slice(
- &u32::try_from(payload_length)
- .expect("length came from u32")
- .to_le_bytes(),
- );
- body.extend_from_slice(&0u64.to_le_bytes()); // reserved
- // Stored sections are already in legacy order
- // (`[payload][user_headers]`): copy through contiguously.
- body.extend_from_slice(&stream[sections_start..sections_end]);
- }
-
- count += 1;
- cursor = sections_end;
+ let record = &stream[position..batch_end];
+ if let Some(encryptor) = encryptor {
+ let decrypted = send_messages::decrypt_batch_record(record, encryptor)?;
+ body.extend_from_slice(&decrypted);
+ } else {
+ body.extend_from_slice(record);
}
+ count = count
+ .checked_add(batch.message_count)
+ .ok_or(IggyError::InvalidCommand)?;
position = batch_end;
}
@@ -1813,7 +1698,7 @@
let zeroed = [0u8; std::mem::size_of::<RoutedRequestHeader>()];
let mut header = *bytemuck::checked::try_from_bytes::<RoutedRequestHeader>(&zeroed)
.expect("zeroed bytes form a valid RoutedRequestHeader");
- header.command = Command2::Request;
+ header.command = Command::Request;
header.operation = Operation::CreatePersonalAccessToken;
header.client = 42;
header.session = 7;
diff --git a/core/server/src/segment_recovery.rs b/core/server/src/segment_recovery.rs
index b909b78..fcaa57a 100644
--- a/core/server/src/segment_recovery.rs
+++ b/core/server/src/segment_recovery.rs
@@ -33,7 +33,7 @@
use partitions::state_transfer::STAGING_SUFFIX;
use partitions::{IggyIndexReader, Segment};
use server_common::SegmentStorage;
-use server_common::send_messages2::{COMMAND_HEADER_SIZE, SendMessages2Header, decode_batch_slice};
+use server_common::send_messages::{BatchHeader, COMMAND_HEADER_SIZE, decode_batch_slice};
use std::fs;
use std::os::unix::fs::FileExt;
use std::path::PathBuf;
@@ -66,7 +66,6 @@
topic_id: usize,
partition_id: usize,
segment_size: IggyByteSize,
- enforce_fsync: bool,
stats: &PartitionStats,
) -> Result<Vec<RecoveredSegment>, ServerError> {
let partition_path = config
@@ -140,8 +139,6 @@
&index_path,
effective_messages_size,
effective_index_size,
- enforce_fsync,
- enforce_fsync,
true,
)
.await
@@ -521,7 +518,7 @@
fn batch_verifies(
messages: &fs::File,
position: u64,
- header: &SendMessages2Header,
+ header: &BatchHeader,
scratch: &mut Vec<u8>,
) -> bool {
scratch.clear();
@@ -536,11 +533,11 @@
messages: &fs::File,
position: u64,
messages_size: u64,
-) -> Option<SendMessages2Header> {
+) -> Option<BatchHeader> {
if position.checked_add(COMMAND_HEADER_SIZE as u64)? > messages_size {
return None;
}
let mut header_bytes = [0u8; COMMAND_HEADER_SIZE];
messages.read_exact_at(&mut header_bytes, position).ok()?;
- SendMessages2Header::decode(&header_bytes).ok()
+ BatchHeader::decode(&header_bytes).ok()
}
diff --git a/core/server/src/wire.rs b/core/server/src/wire.rs
index 02e21a5..d869a21 100644
--- a/core/server/src/wire.rs
+++ b/core/server/src/wire.rs
@@ -100,7 +100,7 @@
mod tests {
use super::{request_body, rewrite_request_body};
use bytes::Bytes;
- use iggy_binary_protocol::{Command2, Operation, RoutedRequestHeader};
+ use iggy_binary_protocol::{Command, Operation, RoutedRequestHeader};
use server_common::Message;
use std::mem::size_of;
@@ -108,7 +108,7 @@
let total_size = size_of::<RoutedRequestHeader>() + body.len();
let mut message = Message::<RoutedRequestHeader>::new(total_size).transmute_header(
|_, header: &mut RoutedRequestHeader| {
- header.command = Command2::Request;
+ header.command = Command::Request;
header.operation = Operation::CreateStream;
header.client = 1;
header.session = 1;
diff --git a/core/server_common/src/consensus_message.rs b/core/server_common/src/consensus_message.rs
index 79cd2f4..cd4cd91 100644
--- a/core/server_common/src/consensus_message.rs
+++ b/core/server_common/src/consensus_message.rs
@@ -18,7 +18,7 @@
use crate::iobuf::{Frozen, Owned};
use crate::sharding::METADATA_GROUP;
use iggy_binary_protocol::{
- Command2, CommitHeader, ConsensusError, ConsensusHeader, DoViewChangeHeader,
+ Command, CommitHeader, ConsensusError, ConsensusHeader, DoViewChangeHeader,
ForwardLogoutHeader, ForwardLogoutResultHeader, ForwardRegisterHeader,
ForwardRegisterResultHeader, GenericHeader, Operation, PrepareHeader, PrepareOkHeader,
RepairPrepareHeader, RepairRangeReplyHeader, RequestHeader, RequestPreparesHeader,
@@ -228,7 +228,7 @@
if self.total_len() < size_of::<T>() {
return Err(ConsensusError::InvalidCommand {
expected: T::COMMAND,
- found: Command2::Reserved,
+ found: Command::Reserved,
});
}
@@ -267,7 +267,7 @@
if self.total_len() < size_of::<T>() {
return Err(ConsensusError::InvalidCommand {
expected: T::COMMAND,
- found: Command2::Reserved,
+ found: Command::Reserved,
});
}
@@ -472,7 +472,7 @@
if bytes.len() < size_of::<H>() {
return Err(ConsensusError::InvalidCommand {
expected: H::COMMAND,
- found: Command2::Reserved,
+ found: Command::Reserved,
});
}
@@ -487,14 +487,14 @@
if (header.size() as usize) < size_of::<H>() {
return Err(ConsensusError::InvalidCommand {
expected: H::COMMAND,
- found: Command2::Reserved,
+ found: Command::Reserved,
});
}
if bytes.len() < header.size() as usize {
return Err(ConsensusError::InvalidCommand {
expected: H::COMMAND,
- found: Command2::Reserved,
+ found: Command::Reserved,
});
}
@@ -512,14 +512,14 @@
let Some(first) = fragments.first() else {
return Err(ConsensusError::InvalidCommand {
expected: H::COMMAND,
- found: Command2::Reserved,
+ found: Command::Reserved,
});
};
if first.len() < size_of::<H>() {
return Err(ConsensusError::InvalidCommand {
expected: H::COMMAND,
- found: Command2::Reserved,
+ found: Command::Reserved,
});
}
@@ -532,7 +532,7 @@
if (header.size() as usize) < size_of::<H>() {
return Err(ConsensusError::InvalidCommand {
expected: H::COMMAND,
- found: Command2::Reserved,
+ found: Command::Reserved,
});
}
@@ -540,7 +540,7 @@
if total_len < header.size() as usize {
return Err(ConsensusError::InvalidCommand {
expected: H::COMMAND,
- found: Command2::Reserved,
+ found: Command::Reserved,
});
}
@@ -699,7 +699,7 @@
}
#[must_use]
- pub fn command(&self) -> Command2 {
+ pub fn command(&self) -> Command {
match self {
Self::Request(message) => message.header().command,
Self::Prepare(message) => message.header().command,
@@ -788,66 +788,64 @@
let command = value.as_generic().header().command;
match command {
- Command2::Prepare => Ok(Self::Prepare(value.try_into_typed::<PrepareHeader>()?)),
- Command2::Request => Ok(Self::Request(
+ Command::Prepare => Ok(Self::Prepare(value.try_into_typed::<PrepareHeader>()?)),
+ Command::Request => Ok(Self::Request(
value.try_into_typed::<RoutedRequestHeader>()?,
)),
- Command2::PrepareOk => Ok(Self::PrepareOk(value.try_into_typed::<PrepareOkHeader>()?)),
- Command2::StartViewChange => Ok(Self::StartViewChange(
+ Command::PrepareOk => Ok(Self::PrepareOk(value.try_into_typed::<PrepareOkHeader>()?)),
+ Command::StartViewChange => Ok(Self::StartViewChange(
value.try_into_typed::<StartViewChangeHeader>()?,
)),
- Command2::DoViewChange => Ok(Self::DoViewChange(
+ Command::DoViewChange => Ok(Self::DoViewChange(
value.try_into_typed::<DoViewChangeHeader>()?,
)),
- Command2::StartView => Ok(Self::StartView(value.try_into_typed::<StartViewHeader>()?)),
- Command2::Commit => Ok(Self::Commit(value.try_into_typed::<CommitHeader>()?)),
- Command2::RequestStartView => Ok(Self::RequestStartView(
+ Command::StartView => Ok(Self::StartView(value.try_into_typed::<StartViewHeader>()?)),
+ Command::Commit => Ok(Self::Commit(value.try_into_typed::<CommitHeader>()?)),
+ Command::RequestStartView => Ok(Self::RequestStartView(
value.try_into_typed::<RequestStartViewHeader>()?,
)),
- Command2::RequestPrepares => Ok(Self::RequestPrepares(
+ Command::RequestPrepares => Ok(Self::RequestPrepares(
value.try_into_typed::<RequestPreparesHeader>()?,
)),
// A repaired prepare is a stored PrepareHeader frame whose command
// byte was rewritten; typed validation would reject the byte, so
// parse through the generic backing and trust the prepare-shaped
// layout the way the journal that produced it did.
- Command2::RepairPrepare => Ok(Self::RepairPrepare(
+ Command::RepairPrepare => Ok(Self::RepairPrepare(
value.try_into_typed::<RepairPrepareHeader>()?,
)),
- Command2::RepairDone | Command2::RangeEvicted => Ok(Self::RepairRangeReply(
+ Command::RepairDone | Command::RangeEvicted => Ok(Self::RepairRangeReply(
value.try_into_typed::<RepairRangeReplyHeader>()?,
)),
- Command2::RequestStateTransfer => Ok(Self::RequestStateTransfer(
+ Command::RequestStateTransfer => Ok(Self::RequestStateTransfer(
value.try_into_typed::<RequestStateTransferHeader>()?,
)),
- Command2::StateTransferTarget => Ok(Self::StateTransferTarget(
+ Command::StateTransferTarget => Ok(Self::StateTransferTarget(
value.try_into_typed::<StateTransferTargetHeader>()?,
)),
- Command2::RequestStateChunk => Ok(Self::RequestStateChunk(
+ Command::RequestStateChunk => Ok(Self::RequestStateChunk(
value.try_into_typed::<RequestStateChunkHeader>()?,
)),
- Command2::StateChunk => Ok(Self::StateChunk(
+ Command::StateChunk => Ok(Self::StateChunk(
value.try_into_typed::<StateChunkHeader>()?,
)),
- Command2::ForwardRegister => Ok(Self::ForwardRegister(
+ Command::ForwardRegister => Ok(Self::ForwardRegister(
value.try_into_typed::<ForwardRegisterHeader>()?,
)),
- Command2::ForwardRegisterResult => Ok(Self::ForwardRegisterResult(
+ Command::ForwardRegisterResult => Ok(Self::ForwardRegisterResult(
value.try_into_typed::<ForwardRegisterResultHeader>()?,
)),
- Command2::ForwardLogout => Ok(Self::ForwardLogout(
+ Command::ForwardLogout => Ok(Self::ForwardLogout(
value.try_into_typed::<ForwardLogoutHeader>()?,
)),
- Command2::ForwardLogoutResult => Ok(Self::ForwardLogoutResult(
+ Command::ForwardLogoutResult => Ok(Self::ForwardLogoutResult(
value.try_into_typed::<ForwardLogoutResultHeader>()?,
)),
// Reply / Eviction are server-to-client frames; they do not
// appear on the inbound dispatch path.
- Command2::Reply | Command2::Eviction => {
- Err(ConsensusError::ClientBoundCommand(command))
- }
+ Command::Reply | Command::Eviction => Err(ConsensusError::ClientBoundCommand(command)),
other => Err(ConsensusError::InvalidCommand {
- expected: Command2::Reserved,
+ expected: Command::Reserved,
found: other,
}),
}
@@ -872,11 +870,11 @@
const REQUEST_SESSION_OFF: usize = std::mem::offset_of!(RoutedRequestHeader, session);
const REQUEST_REQUEST_OFF: usize = std::mem::offset_of!(RoutedRequestHeader, request);
- fn header_bytes(command: Command2, size: u32) -> Owned<MESSAGE_ALIGN> {
+ fn header_bytes(command: Command, size: u32) -> Owned<MESSAGE_ALIGN> {
header_bytes_sized(command, size, 256)
}
- fn header_bytes_sized(command: Command2, size: u32, buffer_len: usize) -> Owned<MESSAGE_ALIGN> {
+ fn header_bytes_sized(command: Command, size: u32, buffer_len: usize) -> Owned<MESSAGE_ALIGN> {
let mut o = Owned::<MESSAGE_ALIGN>::zeroed(buffer_len);
{
let buf = o.as_mut_slice();
@@ -915,7 +913,7 @@
{
let buf = owned.as_mut_slice();
buf[SIZE_OFF..SIZE_OFF + 4].copy_from_slice(&(DVC_SIZE as u32).to_le_bytes());
- buf[COMMAND_OFF] = Command2::DoViewChange as u8;
+ buf[COMMAND_OFF] = Command::DoViewChange as u8;
seal_header_bytes(buf);
}
owned
@@ -960,14 +958,14 @@
#[test]
fn probe_and_repair_commands_round_trip_into_bag() {
for command in [
- Command2::RequestStartView,
- Command2::RequestPrepares,
- Command2::RepairPrepare,
- Command2::RepairDone,
- Command2::RangeEvicted,
+ Command::RequestStartView,
+ Command::RequestPrepares,
+ Command::RepairPrepare,
+ Command::RepairDone,
+ Command::RangeEvicted,
] {
let mut owned = header_bytes(command, 256);
- if command == Command2::RequestPrepares {
+ if command == Command::RequestPrepares {
// validate() demands a non-empty 1-based range.
const FROM_OP_OFF: usize =
std::mem::offset_of!(iggy_binary_protocol::RequestPreparesHeader, from_op);
@@ -985,12 +983,12 @@
.unwrap_or_else(|e| panic!("{command:?} failed bag parse: {e}"));
let routed = matches!(
(&bag, command),
- (MessageBag::RequestStartView(_), Command2::RequestStartView)
- | (MessageBag::RequestPrepares(_), Command2::RequestPrepares)
- | (MessageBag::RepairPrepare(_), Command2::RepairPrepare)
+ (MessageBag::RequestStartView(_), Command::RequestStartView)
+ | (MessageBag::RequestPrepares(_), Command::RequestPrepares)
+ | (MessageBag::RepairPrepare(_), Command::RepairPrepare)
| (
MessageBag::RepairRangeReply(_),
- Command2::RepairDone | Command2::RangeEvicted
+ Command::RepairDone | Command::RangeEvicted
)
);
assert!(routed, "{command:?} parsed into the wrong bag variant");
@@ -1002,7 +1000,7 @@
fn forward_logout_commands_round_trip_into_bag() {
let forward = Message::<ForwardLogoutHeader>::new(HEADER_SIZE).transmute_header(
|_, header: &mut ForwardLogoutHeader| {
- header.command = Command2::ForwardLogout;
+ header.command = Command::ForwardLogout;
header.size = HEADER_SIZE as u32;
header.client = 7;
header.nonce = 8;
@@ -1013,7 +1011,7 @@
);
let result = Message::<ForwardLogoutResultHeader>::new(HEADER_SIZE).transmute_header(
|_, header: &mut ForwardLogoutResultHeader| {
- header.command = Command2::ForwardLogoutResult;
+ header.command = Command::ForwardLogoutResult;
header.size = HEADER_SIZE as u32;
header.client = 7;
header.nonce = 8;
@@ -1028,8 +1026,8 @@
MessageBag::try_from(result.into_generic()).expect("parse ForwardLogoutResult");
assert!(matches!(forward, MessageBag::ForwardLogout(_)));
assert!(matches!(result, MessageBag::ForwardLogoutResult(_)));
- assert_eq!(forward.command(), Command2::ForwardLogout);
- assert_eq!(result.command(), Command2::ForwardLogoutResult);
+ assert_eq!(forward.command(), Command::ForwardLogout);
+ assert_eq!(result.command(), Command::ForwardLogoutResult);
assert_eq!(forward.operation(), Operation::Reserved);
assert_eq!(result.size(), HEADER_SIZE as u32);
}
@@ -1054,7 +1052,7 @@
#[test]
fn try_from_owned_invalid_bit_pattern_returns_err() {
let mut owned = Owned::<MESSAGE_ALIGN>::zeroed(256);
- owned.as_mut_slice()[COMMAND_OFF] = 99; // outside Command2's discriminant range
+ owned.as_mut_slice()[COMMAND_OFF] = 99; // outside Command's discriminant range
let result = Message::<RoutedRequestHeader>::try_from(owned);
assert!(matches!(result, Err(ConsensusError::InvalidBitPattern)));
}
@@ -1068,7 +1066,7 @@
// "buffer shorter than `size_of::<H>`" case currently surface as
// the same `InvalidCommand` variant; promoting them to distinct
// `ConsensusError` variants is a separate hardening pass.)
- let owned = header_bytes(Command2::Request, 999);
+ let owned = header_bytes(Command::Request, 999);
// header_bytes already produces a 256-byte buffer; size=999 > 256,
// so try_from rejects via `bytes.len() < header.size()`.
let result = Message::<RoutedRequestHeader>::try_from(owned);
@@ -1082,7 +1080,7 @@
// buffer-length check passes). Guards the `[size_of::<H>()..size]`
// underflow at every downstream call site.
let owned = header_bytes(
- Command2::Request,
+ Command::Request,
size_of::<RoutedRequestHeader>() as u32 - 1,
);
let result = Message::<RoutedRequestHeader>::try_from(owned);
@@ -1093,10 +1091,10 @@
#[test]
fn as_generic_view_reads_command_byte() {
- let owned = header_bytes(Command2::Request, 256);
+ let owned = header_bytes(Command::Request, 256);
let typed = Message::<RoutedRequestHeader>::try_from(owned).expect("valid");
let generic = typed.as_generic();
- assert_eq!(generic.header().command, Command2::Request);
+ assert_eq!(generic.header().command, Command::Request);
assert_eq!(generic.total_len(), 256);
}
@@ -1108,7 +1106,7 @@
// header's `size`, never the buffer that happens to hold it.
const BODY: [u8; 4] = [1, 2, 3, 4];
let frame_size = size_of::<GenericHeader>() + BODY.len();
- let mut owned = header_bytes_sized(Command2::Prepare, frame_size as u32, 512);
+ let mut owned = header_bytes_sized(Command::Prepare, frame_size as u32, 512);
owned.as_mut_slice()[size_of::<GenericHeader>()..frame_size].copy_from_slice(&BODY);
let message = Message::<GenericHeader>::try_from(owned).expect("valid generic");
@@ -1119,7 +1117,7 @@
fn body_is_empty_for_a_header_only_frame() {
// Header-only commands go through the same accessor, so `size` equal to the
// header must yield an empty slice rather than an inverted-range panic.
- let owned = header_bytes_sized(Command2::Prepare, size_of::<GenericHeader>() as u32, 512);
+ let owned = header_bytes_sized(Command::Prepare, size_of::<GenericHeader>() as u32, 512);
let message = Message::<GenericHeader>::try_from(owned).expect("valid generic");
assert!(message.body().is_empty());
}
@@ -1130,14 +1128,14 @@
fn try_as_typed_command_mismatch_returns_err_without_unsafe_cast() {
// bytes are a valid Prepare; asking for RoutedRequestHeader must fail
// *before* the unsafe ptr-cast inside try_as_typed.
- let owned = header_bytes(Command2::Prepare, 256);
+ let owned = header_bytes(Command::Prepare, 256);
let generic = Message::<GenericHeader>::try_from(owned).expect("valid");
let result = generic.try_as_typed::<RoutedRequestHeader>();
assert!(matches!(
result,
Err(ConsensusError::InvalidCommand {
- expected: Command2::Request,
- found: Command2::Prepare,
+ expected: Command::Request,
+ found: Command::Prepare,
})
));
}
@@ -1146,7 +1144,7 @@
fn try_as_typed_invalid_validation_returns_err() {
// `RequestHeader::validate` rejects operation=Register with non-zero
// session; the routed shape shares the same field rules.
- let mut owned = header_bytes(Command2::Request, 256);
+ let mut owned = header_bytes(Command::Request, 256);
{
let buf = owned.as_mut_slice();
buf[REQUEST_OPERATION_OFF] = Operation::Register as u8;
@@ -1161,21 +1159,21 @@
#[test]
fn try_into_typed_command_mismatch_returns_err() {
- let owned = header_bytes(Command2::Prepare, 256);
+ let owned = header_bytes(Command::Prepare, 256);
let generic = Message::<GenericHeader>::try_from(owned).expect("valid");
let result = generic.try_into_typed::<RoutedRequestHeader>();
assert!(matches!(
result,
Err(ConsensusError::InvalidCommand {
- expected: Command2::Request,
- found: Command2::Prepare,
+ expected: Command::Request,
+ found: Command::Prepare,
})
));
}
// MessageBag dispatch: 7 unsafe `from_backing_unchecked` arms
- fn dispatch(command: Command2, size: u32) -> Result<MessageBag, ConsensusError> {
+ fn dispatch(command: Command, size: u32) -> Result<MessageBag, ConsensusError> {
let owned = header_bytes(command, size);
let generic = Message::<GenericHeader>::try_from(owned).expect("valid generic");
MessageBag::try_from(generic)
@@ -1183,8 +1181,8 @@
#[test]
fn messagebag_dispatch_unsupported_command_returns_err() {
- // Ping is a valid Command2 bit pattern but is not a MessageBag variant.
- let owned = header_bytes(Command2::Ping, 256);
+ // Ping is a valid Command bit pattern but is not a MessageBag variant.
+ let owned = header_bytes(Command::Ping, 256);
let generic = Message::<GenericHeader>::try_from(owned).expect("valid generic");
let result = MessageBag::try_from(generic);
assert!(matches!(result, Err(ConsensusError::InvalidCommand { .. })));
@@ -1193,13 +1191,13 @@
#[test]
fn messagebag_command_method_round_trips() {
for cmd in [
- Command2::Request,
- Command2::Prepare,
- Command2::PrepareOk,
- Command2::StartViewChange,
- Command2::DoViewChange,
- Command2::StartView,
- Command2::Commit,
+ Command::Request,
+ Command::Prepare,
+ Command::PrepareOk,
+ Command::StartViewChange,
+ Command::DoViewChange,
+ Command::StartView,
+ Command::Commit,
] {
let bag = dispatch(cmd, 256).expect("dispatch");
assert_eq!(bag.command(), cmd, "round-trip for {cmd:?}");
@@ -1217,7 +1215,7 @@
// `size` floor, reaching typed dispatch. A size below the header size is
// now rejected earlier by the floor (see
// `try_from_owned_size_below_header_size_returns_err`).
- let owned = header_bytes_sized(Command2::Commit, 300, 512);
+ let owned = header_bytes_sized(Command::Commit, 300, 512);
let generic = Message::<GenericHeader>::try_from(owned).expect("valid generic");
let result = MessageBag::try_from(generic);
assert!(matches!(
@@ -1229,7 +1227,7 @@
#[test]
fn client_wire_decode_of_request_with_invalid_register_session_returns_err() {
// `RequestHeader::validate` rejects Register with non-zero session.
- let mut owned = header_bytes(Command2::Request, 256);
+ let mut owned = header_bytes(Command::Request, 256);
{
let buf = owned.as_mut_slice();
buf[REQUEST_OPERATION_OFF] = Operation::Register as u8;
@@ -1266,7 +1264,7 @@
];
for (operation, session, request, must_pass) in cases {
- let mut owned = header_bytes(Command2::Request, 256);
+ let mut owned = header_bytes(Command::Request, 256);
{
let buf = owned.as_mut_slice();
buf[REQUEST_OPERATION_OFF] = operation as u8;
@@ -1288,7 +1286,7 @@
// it, so ingress must reject it before it can reach the preflight.
#[test]
fn ingress_validation_rejects_zero_client() {
- let mut owned = header_bytes(Command2::Request, 256);
+ let mut owned = header_bytes(Command::Request, 256);
{
let buf = owned.as_mut_slice();
buf[REQUEST_OPERATION_OFF] = Operation::CreateStream as u8;
@@ -1307,7 +1305,7 @@
#[test]
fn request_message_deep_copy_independent() {
- let owned = header_bytes(Command2::Request, 256);
+ let owned = header_bytes(Command::Request, 256);
let mut msg = Message::<RoutedRequestHeader>::try_from(owned).expect("valid");
let copy = msg.deep_copy();
// Mutate the original's bytes; the deep copy must be untouched.
@@ -1320,14 +1318,14 @@
#[test]
fn transmute_header_request_to_prepare() {
- let owned = header_bytes(Command2::Request, 256);
+ let owned = header_bytes(Command::Request, 256);
let msg = Message::<RoutedRequestHeader>::try_from(owned).expect("valid");
let prepared: Message<PrepareHeader> =
msg.transmute_header::<PrepareHeader>(|_old, new| {
- new.command = Command2::Prepare;
+ new.command = Command::Prepare;
new.size = 256;
});
- assert_eq!(prepared.header().command, Command2::Prepare);
+ assert_eq!(prepared.header().command, Command::Prepare);
}
// into_routed: in-place client-wire -> routed retype
@@ -1339,7 +1337,7 @@
fn into_routed_keeps_reserved_prefix_and_unsets_group() {
const RESERVED_OFF: usize = std::mem::offset_of!(RequestHeader, reserved);
- let mut owned = header_bytes(Command2::Request, 256);
+ let mut owned = header_bytes(Command::Request, 256);
{
let buf = owned.as_mut_slice();
for (index, byte) in buf[RESERVED_OFF..RESERVED_OFF + 60].iter_mut().enumerate() {
@@ -1367,14 +1365,14 @@
assert_eq!(header.user_id, client_header.user_id);
}
- // A peer-wire `Command2::Request` decodes as `RoutedRequestHeader`, so its
+ // A peer-wire `Command::Request` decodes as `RoutedRequestHeader`, so its
// validate must enforce the client-boundary field rules: a forged
// `client = 0` frame would otherwise reach the client table's hard assert
// and abort the metadata primary, and a `Reserved` operation would replay
// that client's cached register reply.
#[test]
fn messagebag_dispatch_rejects_request_with_zero_client() {
- let mut owned = header_bytes(Command2::Request, 256);
+ let mut owned = header_bytes(Command::Request, 256);
owned.as_mut_slice()[REQUEST_CLIENT_OFF..REQUEST_CLIENT_OFF + 16]
.copy_from_slice(&0u128.to_le_bytes());
let generic = Message::<GenericHeader>::try_from(owned).expect("valid generic");
@@ -1386,7 +1384,7 @@
#[test]
fn messagebag_dispatch_rejects_request_with_reserved_operation() {
- let mut owned = header_bytes(Command2::Request, 256);
+ let mut owned = header_bytes(Command::Request, 256);
owned.as_mut_slice()[REQUEST_OPERATION_OFF] = Operation::Reserved as u8;
let generic = Message::<GenericHeader>::try_from(owned).expect("valid generic");
assert!(matches!(
@@ -1399,11 +1397,11 @@
#[test]
fn response_backing_single_fragment_roundtrip() {
- let owned = header_bytes(Command2::Reply, 256);
+ let owned = header_bytes(Command::Reply, 256);
let frozen: Frozen<MESSAGE_ALIGN> = owned.into();
let fragments: smallvec::SmallVec<[Frozen<MESSAGE_ALIGN>; 4]> = smallvec![frozen];
let msg = Message::<ReplyHeader, ResponseBacking>::try_from(fragments).expect("valid");
- assert_eq!(msg.header().command, Command2::Reply);
+ assert_eq!(msg.header().command, Command::Reply);
assert_eq!(msg.fragments().len(), 1);
}
@@ -1427,7 +1425,7 @@
fn response_backing_size_below_header_size_returns_err() {
// First fragment is full-size, but its `size` field claims less than
// the header; the floor must reject before any consumer slices a body.
- let owned = header_bytes(Command2::Reply, size_of::<ReplyHeader>() as u32 - 1);
+ let owned = header_bytes(Command::Reply, size_of::<ReplyHeader>() as u32 - 1);
let frozen: Frozen<MESSAGE_ALIGN> = owned.into();
let fragments: smallvec::SmallVec<[Frozen<MESSAGE_ALIGN>; 4]> = smallvec![frozen];
let result = Message::<ReplyHeader, ResponseBacking>::try_from(fragments);
diff --git a/core/server_common/src/in_flight.rs b/core/server_common/src/in_flight.rs
deleted file mode 100644
index 9466429..0000000
--- a/core/server_common/src/in_flight.rs
+++ /dev/null
@@ -1,100 +0,0 @@
-// 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.
-
-use iggy_common::IggyMessagesBatch;
-
-/// Holds batches that are being written to disk.
-///
-/// During async I/O, messages are transferred from the journal to disk.
-/// This buffer holds frozen (immutable, Arc-backed) copies so consumers
-/// can still read them during the write operation.
-#[derive(Debug, Default)]
-pub struct IggyMessagesBatchSetInFlight {
- batches: Vec<IggyMessagesBatch>,
- first_offset: u64,
- last_offset: u64,
-}
-
-impl IggyMessagesBatchSetInFlight {
- pub fn is_empty(&self) -> bool {
- self.batches.is_empty()
- }
-
- pub fn first_offset(&self) -> u64 {
- self.first_offset
- }
-
- pub fn last_offset(&self) -> u64 {
- self.last_offset
- }
-
- pub fn set(&mut self, batches: Vec<IggyMessagesBatch>) {
- if batches.is_empty() {
- self.clear();
- return;
- }
- self.first_offset = batches.first().and_then(|b| b.first_offset()).unwrap_or(0);
- self.last_offset = batches.last().and_then(|b| b.last_offset()).unwrap_or(0);
- self.batches = batches;
- }
-
- pub fn clear(&mut self) {
- self.batches.clear();
- self.first_offset = 0;
- self.last_offset = 0;
- }
-
- /// Get batches that overlap with the requested offset range.
- /// Returns only the batches that contain messages within [start_offset, start_offset + count).
- pub fn get_by_offset(&self, start_offset: u64, count: u32) -> &[IggyMessagesBatch] {
- if self.is_empty() || start_offset > self.last_offset || count == 0 {
- return &[];
- }
-
- let end_offset = start_offset + count as u64 - 1;
- if end_offset < self.first_offset {
- return &[];
- }
-
- let start_idx = self
- .batches
- .iter()
- .position(|b| {
- b.last_offset()
- .map(|last| last >= start_offset)
- .unwrap_or(false)
- })
- .unwrap_or(0);
-
- let end_idx = self
- .batches
- .iter()
- .rposition(|b| {
- b.first_offset()
- .map(|first| first <= end_offset)
- .unwrap_or(false)
- })
- .map(|i| i + 1)
- .unwrap_or(self.batches.len());
-
- &self.batches[start_idx..end_idx]
- }
-
- pub fn batches(&self) -> &[IggyMessagesBatch] {
- &self.batches
- }
-}
diff --git a/core/server_common/src/indexes_mut.rs b/core/server_common/src/indexes_mut.rs
deleted file mode 100644
index 394b234..0000000
--- a/core/server_common/src/indexes_mut.rs
+++ /dev/null
@@ -1,388 +0,0 @@
-// 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.
-
-use crate::PooledBuffer;
-use iggy_common::{INDEX_SIZE, IggyIndexView, IggyIndexes};
-use std::fmt;
-use std::ops::{Deref, Index as StdIndex};
-
-/// A container for binary-encoded index data.
-/// Optimized for efficient storage and I/O operations.
-#[derive(Default)]
-pub struct IggyIndexesMut {
- buffer: PooledBuffer,
- saved_count: u32,
- base_position: u32,
-}
-
-impl IggyIndexesMut {
- /// Creates a new empty container
- pub fn empty() -> Self {
- Self {
- buffer: PooledBuffer::empty(),
- saved_count: 0,
- base_position: 0,
- }
- }
-
- /// Creates indexes from bytes
- pub fn from_bytes(indexes: PooledBuffer, base_position: u32) -> Self {
- Self {
- buffer: indexes,
- saved_count: 0,
- base_position,
- }
- }
-
- /// Decompose the container into its components
- pub fn decompose(mut self) -> (u32, PooledBuffer) {
- let base_position = self.base_position;
- let buffer = std::mem::take(&mut self.buffer);
- (base_position, buffer)
- }
-
- /// Freezes the indexes buffer, converting to an immutable `IggyIndexes`.
- ///
- /// The returned `IggyIndexes` uses Arc-backed `Bytes`, allowing cheap clones.
- pub fn freeze(&mut self) -> IggyIndexes {
- let base_position = self.base_position;
- let buffer = self.buffer.freeze();
- self.saved_count = 0;
- self.base_position = 0;
- IggyIndexes::new(buffer, base_position)
- }
-
- /// Gets the size of all indexes messages
- pub fn messages_size(&self) -> u32 {
- self.last_position() - self.base_position
- }
-
- /// Gets the base position of the indexes
- pub fn base_position(&self) -> u32 {
- self.base_position
- }
-
- /// Sets the base position of the indexes
- pub fn set_base_position(&mut self, base_position: u32) {
- self.base_position = base_position;
- }
-
- /// Helper method to get the last index position
- pub fn last_position(&self) -> u32 {
- self.get(self.count() - 1)
- .map(|idx| idx.position())
- .unwrap_or(0)
- }
-
- /// Creates a new container with the specified capacity
- pub fn with_capacity(capacity: usize, base_position: u32) -> Self {
- Self {
- buffer: PooledBuffer::with_capacity(capacity * INDEX_SIZE),
- saved_count: 0,
- base_position,
- }
- }
-
- /// Gets the capacity of the buffer
- pub fn capacity(&self) -> usize {
- self.buffer.capacity()
- }
-
- /// Inserts a new index at the end of buffer
- pub fn insert(&mut self, offset: u32, position: u32, timestamp: u64) {
- self.buffer.put_u32_le(offset);
- self.buffer.put_u32_le(position);
- self.buffer.put_u64_le(timestamp);
- }
-
- /// Appends another slice of indexes to this one.
- pub fn append_slice(&mut self, other: &[u8]) {
- self.buffer.put_slice(other);
- }
-
- /// Gets the number of indexes in the container
- pub fn count(&self) -> u32 {
- self.buffer.len() as u32 / INDEX_SIZE as u32
- }
-
- /// Checks if the container is empty
- pub fn is_empty(&self) -> bool {
- self.count() == 0
- }
-
- /// Gets the size of the buffer in bytes
- pub fn size(&self) -> u32 {
- self.buffer.len() as u32
- }
-
- /// Gets a view of the Index at the specified index
- pub fn get(&self, index: u32) -> Option<IggyIndexView<'_>> {
- if index >= self.count() {
- return None;
- }
-
- let start = index as usize * INDEX_SIZE;
- let end = start + INDEX_SIZE;
-
- if end <= self.buffer.len() {
- Some(IggyIndexView::new(&self.buffer[start..end]))
- } else {
- None
- }
- }
-
- // Set the offset at the given index position
- pub fn set_offset_at(&mut self, index: u32, offset: u32) {
- let pos = index as usize * INDEX_SIZE;
- self.buffer[pos..pos + 4].copy_from_slice(&offset.to_le_bytes());
- }
-
- // Set the position at the given index
- pub fn set_position_at(&mut self, index: u32, position: u32) {
- let pos = (index as usize * INDEX_SIZE) + 4;
- self.buffer[pos..pos + 4].copy_from_slice(&position.to_le_bytes());
- }
-
- // Set the timestamp at the given index
- pub fn set_timestamp_at(&mut self, index: u32, timestamp: u64) {
- let pos = (index as usize * INDEX_SIZE) + 8;
- self.buffer[pos..pos + 8].copy_from_slice(×tamp.to_le_bytes());
- }
-
- /// Gets a last index
- pub fn last(&self) -> Option<IggyIndexView<'_>> {
- if self.count() == 0 {
- return None;
- }
-
- // Bound to exactly one entry rather than to the buffer end: a file
- // whose length is not a whole multiple of INDEX_SIZE (e.g. a 24-byte
- // server sparse index read back through this 16-byte reader on a
- // mixed-format recovery) would otherwise hand an oversized slice to the
- // view and trip its length assertion.
- let start = (self.count() - 1) as usize * INDEX_SIZE;
- Some(IggyIndexView::new(&self.buffer[start..start + INDEX_SIZE]))
- }
-
- /// Finds an index by timestamp using binary search
- /// If an exact match isn't found, returns the index with the nearest timestamp
- /// that is greater than or equal to the requested timestamp
- pub fn find_by_timestamp(&self, timestamp: u64) -> Option<IggyIndexView<'_>> {
- if self.count() == 0 {
- return None;
- }
-
- let first_idx = self.get(0)?;
- if timestamp <= first_idx.timestamp() {
- return Some(first_idx);
- }
-
- let last_saved_idx = self.get(self.count() - 1)?;
- if timestamp > last_saved_idx.timestamp() {
- return None;
- }
-
- let mut left = 0;
- let mut right = self.count() as isize - 1;
- let mut result: Option<IggyIndexView<'_>> = None;
-
- while left <= right {
- let mid = left + (right - left) / 2;
- let view = self.get(mid as u32).unwrap();
- let current_timestamp = view.timestamp();
-
- match current_timestamp.cmp(×tamp) {
- std::cmp::Ordering::Equal => {
- result = Some(view);
- right = mid - 1;
- }
- std::cmp::Ordering::Less => {
- left = mid + 1;
- }
- std::cmp::Ordering::Greater => {
- result = Some(view);
- right = mid - 1;
- }
- }
- }
-
- result
- }
-
- /// Clears the container, removing all indexes but preserving already allocated buffer capacity
- pub fn clear(&mut self) {
- self.saved_count = 0;
- self.buffer.clear();
- }
-
- /// Gets the unsaved part of the index buffer
- pub fn unsaved_slice(&self) -> PooledBuffer {
- let start_pos = self.saved_count as usize * INDEX_SIZE;
- // TODO: Dunno how to handle this better, maybe we should have a `split` method,
- // That splits the underlying Indexes buffer into two parts
- // saved on disk and not saved yet.
- PooledBuffer::from(&self.buffer[start_pos..])
- }
-
- /// Mark all indexes as saved to disk
- pub fn mark_saved(&mut self) {
- self.saved_count = self.count();
- }
-
- /// Slices the container to return a view of a specific range of indexes
- pub fn slice_by_offset(
- &self,
- relative_start_offset: u32,
- count: u32,
- ) -> Option<IggyIndexesMut> {
- let available_count = self.count().saturating_sub(relative_start_offset);
- let actual_count = std::cmp::min(count, available_count);
-
- if actual_count == 0 || relative_start_offset >= self.count() {
- return None;
- }
-
- let end_pos = relative_start_offset + actual_count;
-
- let start_byte = relative_start_offset as usize * INDEX_SIZE;
- let end_byte = end_pos as usize * INDEX_SIZE;
- let slice = PooledBuffer::from(&self.buffer[start_byte..end_byte]);
-
- if relative_start_offset == 0 {
- Some(IggyIndexesMut::from_bytes(slice, self.base_position))
- } else {
- let position_offset: u32 = self.get(relative_start_offset - 1).unwrap().position();
- Some(IggyIndexesMut::from_bytes(slice, position_offset))
- }
- }
-
- /// Loads indexes from cache based on timestamp
- pub fn slice_by_timestamp(&self, timestamp: u64, count: u32) -> Option<IggyIndexesMut> {
- if self.count() == 0 {
- return None;
- }
-
- let start_index_pos = self.binary_search_position_for_timestamp_sync(timestamp)?;
-
- let available_count = self.count().saturating_sub(start_index_pos);
- let actual_count = std::cmp::min(count, available_count);
-
- if actual_count == 0 {
- return None;
- }
-
- let end_pos = start_index_pos + actual_count;
-
- let start_byte = start_index_pos as usize * INDEX_SIZE;
- let end_byte = end_pos as usize * INDEX_SIZE;
- let slice = PooledBuffer::from(&self.buffer[start_byte..end_byte]);
-
- let base_position = if start_index_pos > 0 {
- self.get(start_index_pos - 1).unwrap().position()
- } else {
- 0
- };
-
- Some(IggyIndexesMut::from_bytes(slice, base_position))
- }
-
- /// Find the position of the index with timestamp closest to (but not exceeding) the target
- fn binary_search_position_for_timestamp_sync(&self, target_timestamp: u64) -> Option<u32> {
- if self.count() == 0 {
- return None;
- }
-
- let last_index = self.get(self.count() - 1)?;
- if target_timestamp > last_index.timestamp() {
- return Some(self.count() - 1);
- }
-
- let first_index = self.get(0)?;
- if target_timestamp <= first_index.timestamp() {
- return Some(0);
- }
-
- let mut low = 0;
- let mut high = self.count() - 1;
-
- while low <= high {
- let mid = low + (high - low) / 2;
- let mid_index = self.get(mid)?;
- let mid_timestamp = mid_index.timestamp();
-
- match mid_timestamp.cmp(&target_timestamp) {
- std::cmp::Ordering::Equal => return Some(mid),
- std::cmp::Ordering::Less => low = mid + 1,
- std::cmp::Ordering::Greater => {
- if mid == 0 {
- break;
- }
- high = mid - 1;
- }
- }
- }
-
- Some(low)
- }
-}
-
-impl StdIndex<usize> for IggyIndexesMut {
- type Output = [u8];
-
- fn index(&self, index: usize) -> &Self::Output {
- let start = index * INDEX_SIZE;
- let end = start + INDEX_SIZE;
- &self.buffer[start..end]
- }
-}
-
-impl Deref for IggyIndexesMut {
- type Target = [u8];
-
- fn deref(&self) -> &Self::Target {
- &self.buffer
- }
-}
-
-impl fmt::Debug for IggyIndexesMut {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let count = self.count();
-
- if count == 0 {
- return write!(f, "IggyIndexesMut {{ count: 0, indexes: [] }}");
- }
-
- writeln!(f, "IggyIndexesMut {{")?;
- writeln!(f, " count: {count},")?;
- writeln!(f, " indexes: [")?;
-
- for i in 0..count {
- if let Some(index) = self.get(i) {
- writeln!(
- f,
- " {{ offset: {}, position: {}, timestamp: {} }},",
- index.offset(),
- index.position(),
- index.timestamp()
- )?;
- }
- }
-
- writeln!(f, " ]")?;
- write!(f, "}}")
- }
-}
diff --git a/core/server_common/src/lib.rs b/core/server_common/src/lib.rs
index 0e94ce3..2aca6f2 100644
--- a/core/server_common/src/lib.rs
+++ b/core/server_common/src/lib.rs
@@ -23,8 +23,6 @@
pub mod diagnostics;
pub mod executor;
pub mod fs_utils;
-mod in_flight;
-mod indexes_mut;
// TODO(hubcio): iobuf was relocated verbatim from `core/binary_protocol/src/consensus/iobuf.rs`
// during the sans-io split. Its implementation is intentionally untouched; the
// lints below predate the move and are tracked as tech debt for a 0.9.x cleanup pass.
@@ -41,10 +39,8 @@
pub mod iobuf;
pub mod log;
mod memory_pool;
-mod messages_batch_mut;
-mod messages_batch_set;
mod segment_storage;
-pub mod send_messages2;
+pub mod send_messages;
pub mod sharding;
pub use bootstrap::create_directories;
@@ -55,11 +51,7 @@
MutableBacking, RequestBacking, RequestBackingKind, ResponseBacking, ResponseBackingKind,
};
pub use executor::create_shard_executor;
-pub use in_flight::IggyMessagesBatchSetInFlight;
-pub use indexes_mut::IggyIndexesMut;
pub use memory_pool::{MEMORY_POOL, MemoryPool, MemoryPoolConfigOther, memory_pool};
-pub use messages_batch_mut::IggyMessagesBatchMut;
-pub use messages_batch_set::IggyMessagesBatchSet;
pub use segment_storage::{
IndexReader, IndexWriter, MessagesReader, MessagesWriter, SegmentStorage,
};
diff --git a/core/server_common/src/messages_batch_mut.rs b/core/server_common/src/messages_batch_mut.rs
deleted file mode 100644
index bf7bbd1..0000000
--- a/core/server_common/src/messages_batch_mut.rs
+++ /dev/null
@@ -1,616 +0,0 @@
-// 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.
-
-use crate::PooledBuffer;
-use crate::indexes_mut::IggyIndexesMut;
-use iggy_common::{
- IGGY_MESSAGE_HEADER_SIZE, INDEX_SIZE, IggyByteSize, IggyError, IggyIndexView, IggyMessage,
- IggyMessageBoundaries, IggyMessageView, IggyMessageViewIterator, IggyMessageViewMutIterator,
- IggyMessagesBatch, MAX_PAYLOAD_SIZE, MAX_USER_HEADERS_SIZE, Sizeable, Validatable,
-};
-use std::ops::Index;
-use tracing::error;
-
-/// A container for mutable messages that are being prepared for persistence.
-///
-/// `IggyMessagesBatchMut` holds both the raw message data in a `BytesMut` buffer
-/// and the corresponding index data that allows for efficient message lookup.
-#[derive(Debug, Default)]
-pub struct IggyMessagesBatchMut {
- /// The index data for all messages in the buffer
- indexes: IggyIndexesMut,
-
- /// The buffer containing the serialized message data
- messages: PooledBuffer,
-}
-
-impl Sizeable for IggyMessagesBatchMut {
- fn get_size_bytes(&self) -> IggyByteSize {
- IggyByteSize::from(self.messages.len() as u64)
- }
-}
-
-impl IggyMessagesBatchMut {
- /// Creates a new empty messages container
- pub fn empty() -> Self {
- Self {
- indexes: IggyIndexesMut::empty(),
- messages: PooledBuffer::empty(),
- }
- }
-
- /// Creates a new messages container from existing index and message buffers.
- ///
- /// # Arguments
- ///
- /// * `indexes` - Preprocessed index data
- /// * `messages` - Serialized message data
- pub fn from_indexes_and_messages(indexes: IggyIndexesMut, messages: PooledBuffer) -> Self {
- Self { indexes, messages }
- }
-
- /// Creates a new messages container from a slice of IggyMessage objects.
- ///
- /// # Note
- /// This function should be used only for testing purposes,
- /// because it creates deep copies of the messages.
- ///
- /// # Arguments
- ///
- /// * `messages` - Slice of message objects to store
- /// * `messages_size` - Total size of all messages in bytes
- pub fn from_messages(messages: &[IggyMessage], messages_size: u32) -> Self {
- let mut messages_buffer = PooledBuffer::with_capacity(messages_size as usize);
- let mut indexes_buffer = IggyIndexesMut::with_capacity(messages.len(), 0);
- let mut position = 0;
-
- for message in messages {
- let bytes = message.to_bytes();
- messages_buffer.put_slice(&bytes);
- position += message.get_size_bytes().as_bytes_u32();
- indexes_buffer.insert(0, position, 0);
- }
-
- Self::from_indexes_and_messages(indexes_buffer, messages_buffer)
- }
-
- /// Creates a lending iterator that yields mutable views of messages.
- pub fn iter_mut(&mut self) -> IggyMessageViewMutIterator<'_> {
- IggyMessageViewMutIterator::new(&mut self.messages)
- }
-
- /// Creates an iterator that yields immutable views of messages.
- pub fn iter(&self) -> IggyMessageViewIterator<'_> {
- IggyMessageViewIterator::new_with_boundaries(
- &self.messages,
- &self.indexes,
- self.indexes.base_position(),
- self.count(),
- )
- }
-
- /// Returns the number of messages in the batch.
- pub fn count(&self) -> u32 {
- self.indexes.len() as u32 / INDEX_SIZE as u32
- }
-
- /// Returns the total size of all messages in bytes.
- pub fn size(&self) -> u32 {
- self.messages.len() as u32
- }
-
- /// Returns the raw message bytes as a slice.
- pub fn as_bytes(&self) -> &[u8] {
- &self.messages
- }
-
- /// Returns the first offset in the batch
- pub fn first_offset(&self) -> Option<u64> {
- if self.is_empty() {
- return None;
- }
- Some(IggyMessageView::new(&self.messages).ok()?.header().offset())
- }
-
- /// Returns the first timestamp in the batch
- pub fn first_timestamp(&self) -> Option<u64> {
- if self.is_empty() {
- return None;
- }
- Some(
- IggyMessageView::new(&self.messages)
- .ok()?
- .header()
- .timestamp(),
- )
- }
-
- /// Returns the last timestamp in the batch
- pub fn last_timestamp(&self) -> Option<u64> {
- if self.is_empty() {
- return None;
- }
-
- let last_index = self.count() as usize - 1;
- let (start, end) = self.get_message_boundaries(last_index)?;
- Some(
- IggyMessageView::new(&self.messages[start..end])
- .ok()?
- .header()
- .timestamp(),
- )
- }
-
- /// Returns the last offset in the batch
- pub fn last_offset(&self) -> Option<u64> {
- if self.is_empty() {
- return None;
- }
- let last_index = self.count() as usize - 1;
- let (start, end) = self.get_message_boundaries(last_index)?;
- Some(
- IggyMessageView::new(&self.messages[start..end])
- .ok()?
- .header()
- .offset(),
- )
- }
-
- /// Checks if the batch is empty.
- pub fn is_empty(&self) -> bool {
- self.count() == 0
- }
-
- /// Return total size of all messages in bytes.
- pub fn len(&self) -> usize {
- self.messages.len()
- }
-
- /// Decomposes the batch into its constituent parts.
- pub fn decompose(mut self) -> (IggyIndexesMut, PooledBuffer) {
- let indexes = std::mem::replace(&mut self.indexes, IggyIndexesMut::empty());
- let messages = std::mem::take(&mut self.messages);
-
- (indexes, messages)
- }
-
- /// Freezes the batch, converting to an immutable `IggyMessagesBatch`.
- ///
- /// The returned batch uses Arc-backed `Bytes`, allowing cheap clones.
- /// After calling this, the mutable batch becomes empty.
- pub fn freeze(&mut self) -> IggyMessagesBatch {
- let count = self.count();
- let indexes = self.indexes.freeze();
- let messages = self.messages.freeze();
- IggyMessagesBatch::new(indexes, messages, count)
- }
-
- pub fn take_messages(&mut self) -> PooledBuffer {
- std::mem::take(&mut self.messages)
- }
-
- /// Take the indexes from the batch
- pub fn take_indexes(&mut self) -> IggyIndexesMut {
- std::mem::take(&mut self.indexes)
- }
-
- /// Borrows the indexes from the batch
- pub fn indexes(&self) -> &IggyIndexesMut {
- &self.indexes
- }
-
- /// Get message position from the indexes at the given index
- pub fn position_at(&self, index: u32) -> Option<u32> {
- self.indexes.get(index).map(|index| index.position())
- }
-
- fn boundaries(&self) -> Option<IggyMessageBoundaries<'_>> {
- IggyMessageBoundaries::new(
- &self.indexes,
- self.messages.len(),
- self.indexes.base_position(),
- self.count(),
- )
- }
-
- /// Calculates the start position of a message at the given index in the buffer
- fn message_start_position(&self, index: usize) -> Option<usize> {
- self.get_message_boundaries(index).map(|(start, _)| start)
- }
-
- /// Calculates the end position of a message at the given index in the buffer
- fn message_end_position(&self, index: usize) -> Option<usize> {
- self.get_message_boundaries(index).map(|(_, end)| end)
- }
-
- /// Returns a contiguous slice (as a new `IggyMessagesBatch`) of up to `count` messages
- /// whose message headers have an offset greater than or equal to the provided `start_offset`.
- pub fn slice_by_offset(&self, start_offset: u64, count: u32) -> Option<Self> {
- if self.is_empty() || count == 0 {
- return None;
- }
-
- let first_offset = self.first_offset()?;
-
- if start_offset < first_offset {
- return None;
- }
-
- let last_offset = self.last_offset()?;
- if start_offset > last_offset {
- return None;
- }
-
- let offset_diff = start_offset - first_offset;
- let first_message_index = offset_diff as usize;
-
- if first_message_index >= self.count() as usize {
- return None;
- }
-
- self.slice_by_index(first_message_index as u32, count)
- }
-
- /// Helper method to slice the batch starting from a specific index
- fn slice_by_index(&self, start_index: u32, count: u32) -> Option<Self> {
- if start_index >= self.count() {
- return None;
- }
-
- let last_message_index =
- std::cmp::min((start_index + count) as usize, self.count() as usize);
-
- let sub_indexes = self.indexes.slice_by_offset(
- start_index,
- (last_message_index - start_index as usize) as u32,
- )?;
-
- let first_message_position = self.message_start_position(start_index as usize)?;
- let last_message_position = self.message_end_position(last_message_index - 1)?;
-
- // TODO(hubcio): messages from accumulator unfortunately are deep-copied
- let mut sub_buffer =
- PooledBuffer::with_capacity(last_message_position - first_message_position);
- sub_buffer.put_slice(&self.messages[first_message_position..last_message_position]);
-
- Some(IggyMessagesBatchMut {
- indexes: sub_indexes,
- messages: sub_buffer,
- })
- }
-
- /// Returns a contiguous slice (as a new `IggyMessagesBatch`) of up to `count` messages
- /// whose message headers have a timestamp greater than or equal to the provided `timestamp`.
- ///
- /// If no messages meet the criteria, returns `None`.
- pub fn slice_by_timestamp(&self, timestamp: u64, count: u32) -> Option<Self> {
- if self.is_empty() || count == 0 {
- return None;
- }
-
- // Use binary search to find the first message with timestamp >= the target
- let first_message_index = self.binary_search_timestamp(timestamp)?;
-
- self.slice_by_index(first_message_index, count)
- }
-
- /// Find the position of the index with timestamp closest to (but not exceeding) the target
- fn binary_search_timestamp(&self, target_timestamp: u64) -> Option<u32> {
- if self.count() == 0 {
- return None;
- }
-
- let last_timestamp = self.get(self.count() as usize - 1)?.header().timestamp();
- if target_timestamp > last_timestamp {
- return Some(self.count() - 1);
- }
-
- let first_timestamp = self.get(0)?.header().timestamp();
- if target_timestamp <= first_timestamp {
- return Some(0);
- }
-
- let mut low = 0;
- let mut high = self.count() - 1;
-
- while low <= high {
- let mid = low + (high - low) / 2;
- let mid_index = self.get(mid as usize)?;
- let mid_timestamp = mid_index.header().timestamp();
-
- match mid_timestamp.cmp(&target_timestamp) {
- std::cmp::Ordering::Equal => return Some(mid),
- std::cmp::Ordering::Less => low = mid + 1,
- std::cmp::Ordering::Greater => {
- if mid == 0 {
- break;
- }
- high = mid - 1;
- }
- }
- }
-
- Some(low)
- }
-
- /// Validates that all messages have correct checksums and offsets.
- /// This function should be called after messages have been read from disk.
- ///
- /// # Arguments
- ///
- /// * `absolute_start_offset` - The absolute offset of the first message in the batch.
- ///
- /// # Returns
- ///
- /// * `Ok(())` - If all messages have correct checksums and offsets.
- /// * `Err(IggyError)` - If any message has an invalid checksum or offset.
- pub fn validate_checksums_and_offsets(
- &self,
- absolute_start_offset: u64,
- ) -> Result<(), IggyError> {
- for (current_offset, message) in (absolute_start_offset..).zip(self.iter()) {
- let calculated_checksum = message.calculate_checksum();
- let actual_checksum = message.header().checksum();
- let offset = message.header().offset();
- if offset != current_offset {
- return Err(IggyError::InvalidOffset(offset));
- }
- if calculated_checksum != actual_checksum {
- return Err(IggyError::InvalidMessageChecksum(
- actual_checksum,
- calculated_checksum,
- offset,
- ));
- }
- }
- Ok(())
- }
-
- /// Gets the byte range for a message at the given index
- fn get_message_boundaries(&self, index: usize) -> Option<(usize, usize)> {
- let (start, end) = self.boundaries()?.boundaries(index)?;
-
- if start > self.messages.len()
- || end > self.messages.len()
- || start > end
- || end - start < IGGY_MESSAGE_HEADER_SIZE
- {
- return None;
- }
-
- Some((start, end))
- }
-
- /// Get the message at the specified index.
- /// Returns None if the index is out of bounds or the message cannot be found.
- pub fn get(&self, index: usize) -> Option<IggyMessageView<'_>> {
- let (start, end) = self.get_message_boundaries(index)?;
- IggyMessageView::new(&self.messages[start..end]).ok()
- }
-
- /// Validates that all messages in batch have correct checksums.
- pub fn validate_checksums(&self) -> Result<(), IggyError> {
- for message in self.iter() {
- let calculated_checksum = message.calculate_checksum();
- let actual_checksum = message.header().checksum();
- let offset = message.header().offset();
- if calculated_checksum != actual_checksum {
- return Err(IggyError::InvalidMessageChecksum(
- actual_checksum,
- calculated_checksum,
- offset,
- ));
- }
- }
- Ok(())
- }
-
- /// Validates the structure of the indexes (sizes, counts, etc.)
- fn validate_indexes_structure(&self) -> Result<(), IggyError> {
- let indexes_count = self.indexes.count();
- let indexes_size = self.indexes.size();
-
- if !indexes_size.is_multiple_of(INDEX_SIZE as u32) {
- error!(
- "Indexes size {} is not a multiple of index size {}",
- indexes_size, INDEX_SIZE
- );
- return Err(IggyError::InvalidIndexesByteSize(indexes_size));
- }
-
- if indexes_count != self.count() {
- error!(
- "Indexes count {} does not match messages count {}",
- indexes_count,
- self.count()
- );
- return Err(IggyError::InvalidIndexesCount(indexes_count, self.count()));
- }
-
- Ok(())
- }
-
- fn validate_message_contents(&self) -> Result<(), IggyError> {
- let mut messages_count = 0;
- let mut messages_size = 0;
- let mut prev_offset = 0;
- let mut prev_position = 0;
-
- for i in 0..self.count() {
- let index = self.validate_index_at(i)?;
- let message = self.validate_message_at(i)?;
-
- if message.header().offset() < prev_offset {
- error!(
- "Offset of previous message: {} is smaller than current message {} at offset {}",
- prev_offset,
- message.header().offset(),
- i
- );
- return Err(IggyError::InvalidOffset(message.header().offset()));
- }
-
- if index.position() < prev_position {
- error!(
- "Position of previous message: {} is smaller than current message {} at offset {}",
- prev_position,
- index.position(),
- i
- );
- return Err(IggyError::CannotReadIndexPosition);
- }
-
- prev_offset = message.header().offset();
- prev_position = index.position();
- messages_size += message.size();
- messages_count += 1;
- }
-
- let indexes_count = self.indexes.count();
- if indexes_count != messages_count {
- error!(
- "Indexes count {} does not match messages count {}",
- indexes_count, messages_count
- );
- return Err(IggyError::InvalidMessagesCount);
- }
-
- if messages_size != self.messages.len() {
- error!(
- "Messages size {} does not match messages buffer size {}",
- messages_size,
- self.messages.len() as u64
- );
- return Err(IggyError::InvalidMessagesSize(
- messages_size as u32,
- self.messages.len() as u32,
- ));
- }
-
- Ok(())
- }
-
- /// Validates a specific index entry
- fn validate_index_at(&self, i: u32) -> Result<IggyIndexView<'_>, IggyError> {
- let index_view = match self.indexes.get(i) {
- Some(view) => view,
- None => {
- error!("Index {} is missing", i);
- return Err(IggyError::MissingIndex(i));
- }
- };
-
- if index_view.offset() != 0 {
- error!(
- "Non-zero offset {} at index: {}, messages coming from network always have offset 0",
- index_view.offset(),
- i
- );
- return Err(IggyError::NonZeroOffset(index_view.offset() as u64, i));
- }
-
- if index_view.timestamp() != 0 {
- error!(
- "Non-zero timestamp {} at index: {}, messages coming from network always have timestamp 0",
- index_view.timestamp(),
- i
- );
- return Err(IggyError::NonZeroTimestamp(index_view.timestamp(), i));
- }
-
- Ok(index_view)
- }
-
- /// Validates a specific message
- fn validate_message_at(&self, i: u32) -> Result<IggyMessageView<'_>, IggyError> {
- let message = match self.get(i as usize) {
- Some(msg) => msg,
- None => {
- error!(
- "Message at index {} is missing, or message size is less than minimum message size {} B (header)",
- i, IGGY_MESSAGE_HEADER_SIZE
- );
- return Err(IggyError::MissingIndex(i));
- }
- };
-
- if message.payload().len() as u32 > MAX_PAYLOAD_SIZE {
- error!(
- "Message payload size {} B exceeds maximum payload size {} B",
- message.payload().len(),
- MAX_PAYLOAD_SIZE
- );
- return Err(IggyError::TooBigMessagePayload);
- }
-
- if message.size() < IGGY_MESSAGE_HEADER_SIZE {
- error!(
- "Message size {} B is less than minimum message size {} B (header)",
- message.size(),
- IGGY_MESSAGE_HEADER_SIZE
- );
- return Err(IggyError::TooSmallMessage(
- message.size() as u32,
- IGGY_MESSAGE_HEADER_SIZE as u32,
- ));
- }
-
- if let Some(user_headers) = message.user_headers()
- && user_headers.len() as u32 > MAX_USER_HEADERS_SIZE
- {
- error!(
- "Message user headers size {} B exceeds maximum size {} B",
- user_headers.len(),
- MAX_USER_HEADERS_SIZE
- );
- return Err(IggyError::TooBigUserHeaders);
- }
-
- Ok(message)
- }
-}
-
-impl Validatable<IggyError> for IggyMessagesBatchMut {
- fn validate(&self) -> Result<(), IggyError> {
- if self.is_empty() {
- return Err(IggyError::InvalidMessagesCount);
- }
-
- self.validate_indexes_structure()?;
- self.validate_message_contents()?;
- Ok(())
- }
-}
-
-impl Index<usize> for IggyMessagesBatchMut {
- type Output = [u8];
-
- fn index(&self, index: usize) -> &Self::Output {
- assert!(
- index < self.count() as usize,
- "Index out of bounds: the len is {} but the index is {}",
- self.count(),
- index
- );
-
- let (start, end) = self
- .get_message_boundaries(index)
- .expect("Invalid message boundaries");
-
- &self.messages[start..end]
- }
-}
diff --git a/core/server_common/src/messages_batch_set.rs b/core/server_common/src/messages_batch_set.rs
deleted file mode 100644
index a10bda3..0000000
--- a/core/server_common/src/messages_batch_set.rs
+++ /dev/null
@@ -1,378 +0,0 @@
-// 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.
-
-use crate::{IggyIndexesMut, IggyMessagesBatchMut, PooledBuffer};
-use bytes::Bytes;
-use iggy_common::{
- IggyByteSize, IggyMessage, IggyMessageView, IggyMessagesBatch, IggyPollMetadata,
- PolledMessages, Sizeable,
-};
-use std::ops::Index;
-use tracing::trace;
-
-/// A container for multiple IggyMessagesBatch objects
-#[derive(Debug, Default)]
-pub struct IggyMessagesBatchSet {
- /// The collection of message containers
- batches: Vec<IggyMessagesBatchMut>,
- /// Total number of messages across all containers
- count: u32,
- /// Total size in bytes across all containers
- size: u32,
-}
-
-impl IggyMessagesBatchSet {
- /// Create a new empty batch
- pub fn empty() -> Self {
- Self {
- batches: Vec::new(),
- count: 0,
- size: 0,
- }
- }
-
- /// Create a new empty batch set with a specified initial capacity of message containers
- pub fn with_capacity(capacity: usize) -> Self {
- Self {
- batches: Vec::with_capacity(capacity),
- count: 0,
- size: 0,
- }
- }
-
- /// Create a batch set from an existing vector of IggyMessages
- pub fn from_vec(messages: Vec<IggyMessagesBatchMut>) -> Self {
- let mut batch = Self::with_capacity(messages.len());
- for msg in messages {
- batch.add_batch(msg);
- }
- batch
- }
-
- /// Add another batch of messages to the batch set
- pub fn add_batch(&mut self, batch: IggyMessagesBatchMut) {
- self.count += batch.count();
- self.size += batch.size();
- self.batches.push(batch);
- }
-
- /// Add another batch set of messages to the batch set
- pub fn add_batch_set(&mut self, mut other_batch_set: IggyMessagesBatchSet) {
- self.count += other_batch_set.count();
- self.size += other_batch_set.size();
- let other_batches = std::mem::take(&mut other_batch_set.batches);
- self.batches.extend(other_batches);
- }
-
- /// Add immutable batches by copying them into mutable form.
- ///
- /// This is used when reading from the in-flight buffer (which holds
- /// frozen/immutable batches) and needs to convert them for the read path.
- pub fn add_immutable_batches(&mut self, batches: &[IggyMessagesBatch]) {
- for batch in batches {
- let mutable_batch = Self::immutable_to_mutable(batch);
- self.add_batch(mutable_batch);
- }
- }
-
- /// Convert an immutable IggyMessagesBatch to a mutable IggyMessagesBatchMut.
- ///
- /// This requires copying the Bytes into a PooledBuffer.
- fn immutable_to_mutable(batch: &IggyMessagesBatch) -> IggyMessagesBatchMut {
- let base_position = batch.indexes().base_position();
- let indexes_buffer = PooledBuffer::from(batch.indexes_slice());
- let indexes = IggyIndexesMut::from_bytes(indexes_buffer, base_position);
- let messages = PooledBuffer::from(batch.buffer());
-
- IggyMessagesBatchMut::from_indexes_and_messages(indexes, messages)
- }
-
- /// Extract indexes from all batches in the set
- pub fn append_indexes_to(&self, target: &mut IggyIndexesMut) {
- for batch in self.iter() {
- let indexes = batch.indexes();
- target.append_slice(indexes);
- }
- }
-
- /// Get the total number of messages in the batch
- pub fn count(&self) -> u32 {
- self.count
- }
-
- /// Get the total size of all messages in bytes
- pub fn size(&self) -> u32 {
- self.size
- }
-
- /// Get the number of message containers in the batch
- pub fn containers_count(&self) -> usize {
- self.batches.len()
- }
-
- /// Check if the batch is empty
- pub fn is_empty(&self) -> bool {
- self.batches.is_empty() || self.count == 0
- }
-
- /// Get timestamp of first message in first batch
- pub fn first_timestamp(&self) -> Option<u64> {
- if self.is_empty() {
- return None;
- }
- self.batches.first().map(|batch| batch.first_timestamp())?
- }
-
- /// Get offset of first message in first batch
- pub fn first_offset(&self) -> Option<u64> {
- if self.is_empty() {
- return None;
- }
- self.batches.first().map(|batch| batch.first_offset())?
- }
-
- /// Get timestamp of last message in last batch
- pub fn last_timestamp(&self) -> Option<u64> {
- if self.is_empty() {
- return None;
- }
- self.batches.last().map(|batch| batch.last_timestamp())?
- }
-
- /// Get offset of last message in last batch
- pub fn last_offset(&self) -> Option<u64> {
- self.batches.last().map(|batch| batch.last_offset())?
- }
-
- /// Get a reference to the underlying vector of message containers
- pub fn inner(&self) -> &Vec<IggyMessagesBatchMut> {
- &self.batches
- }
-
- /// Consume the batch, returning the underlying vector of message containers
- pub fn into_inner(mut self) -> Vec<IggyMessagesBatchMut> {
- std::mem::take(&mut self.batches)
- }
-
- /// Iterate over all message containers in the batch
- pub fn iter(&self) -> impl Iterator<Item = &IggyMessagesBatchMut> {
- self.batches.iter()
- }
-
- /// Iterate over all mutable message containers in the batch
- pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut IggyMessagesBatchMut> {
- self.batches.iter_mut()
- }
-
- /// Convert this batch and poll metadata into a vector of fully-formed IggyMessage objects
- ///
- /// This method transforms the internal message views into complete IggyMessage objects
- /// that can be returned to clients. It should only be used by server http implementation.
- ///
- /// # Arguments
- ///
- /// * `poll_metadata` - Metadata about the partition and current offset
- ///
- /// # Returns
- ///
- /// A vector of IggyMessage objects with proper metadata
- pub fn into_polled_messages(&self, poll_metadata: IggyPollMetadata) -> PolledMessages {
- if self.is_empty() {
- return PolledMessages::empty();
- }
-
- let mut messages = Vec::with_capacity(self.count() as usize);
-
- for batch in self.iter() {
- for message in batch.iter() {
- let header = message.header().to_header();
- let payload = Bytes::copy_from_slice(message.payload());
- let user_headers = message.user_headers().map(Bytes::copy_from_slice);
- let message = IggyMessage {
- header,
- payload,
- user_headers,
- };
- messages.push(message);
- }
- }
-
- trace!(
- "Converted batch of {} messages from partition {} with current offset {}",
- messages.len(),
- poll_metadata.partition_id,
- poll_metadata.current_offset
- );
-
- PolledMessages {
- partition_id: poll_metadata.partition_id,
- current_offset: poll_metadata.current_offset,
- count: messages.len() as u32,
- messages,
- }
- }
-
- /// Returns a new IggyMessagesBatch containing only messages with offsets greater than or equal to the specified offset,
- /// up to the specified count.
- ///
- /// If no messages match the criteria, returns an empty batch.
- pub fn get_by_offset(&self, start_offset: u64, count: u32) -> Self {
- if self.is_empty() || count == 0 {
- return Self::empty();
- }
-
- let mut result = Self::with_capacity(self.containers_count());
- let mut remaining_count = count;
- let mut current_offset = start_offset;
-
- for container in self.iter() {
- if remaining_count == 0 {
- break;
- }
-
- let Some(batch_first) = container.first_offset() else {
- continue;
- };
- if batch_first + container.count() as u64 <= current_offset {
- continue;
- }
-
- // When current_offset is below this batch's range (cross-batch
- // reads), start from the batch's first offset instead.
- let effective_start = current_offset.max(batch_first);
-
- if let Some(sliced) = container.slice_by_offset(effective_start, remaining_count)
- && sliced.count() > 0
- {
- if let Some(last) = sliced.last_offset() {
- current_offset = last + 1;
- }
- remaining_count -= sliced.count();
- result.add_batch(sliced);
- }
- }
-
- result
- }
-
- /// Returns a new IggyMessagesBatch containing only messages with timestamps greater than or equal
- /// to the specified timestamp, up to the specified count.
- ///
- /// If no messages match the criteria, returns an empty batch.
- pub fn get_by_timestamp(&self, timestamp: u64, count: u32) -> Self {
- if self.is_empty() || count == 0 {
- return Self::empty();
- }
-
- let mut result = Self::with_capacity(self.containers_count());
- let mut remaining_count = count;
-
- for container in self.iter() {
- if remaining_count == 0 {
- break;
- }
-
- let first_timestamp = container.first_timestamp();
- if first_timestamp.is_none() || first_timestamp.unwrap() < timestamp {
- continue;
- }
-
- if let Some(sliced) = container.slice_by_timestamp(timestamp, remaining_count)
- && sliced.count() > 0
- {
- remaining_count -= sliced.count();
- result.add_batch(sliced);
- }
- }
-
- result
- }
-
- /// Get the message at the specified index.
- /// Returns None if the index is out of bounds.
- pub fn get(&self, index: usize) -> Option<IggyMessageView<'_>> {
- if index >= self.count as usize {
- return None;
- }
-
- let mut seen_messages = 0;
-
- for batch in &self.batches {
- let batch_count = batch.count() as usize;
-
- if index < seen_messages + batch_count {
- let local_index = index - seen_messages;
- return batch.get(local_index);
- }
-
- seen_messages += batch_count;
- }
-
- None
- }
-}
-
-impl Index<usize> for IggyMessagesBatchSet {
- type Output = [u8];
-
- /// Get the message bytes at the specified index across all batches
- ///
- /// # Panics
- ///
- /// Panics if the index is out of bounds (>= total number of messages)
- fn index(&self, index: usize) -> &Self::Output {
- assert!(
- index < self.count as usize,
- "Index out of bounds: the len is {} but the index is {}",
- self.count,
- index
- );
-
- let mut seen_messages = 0;
-
- for batch in &self.batches {
- let batch_count = batch.count() as usize;
-
- if index < seen_messages + batch_count {
- let local_index = index - seen_messages;
- return &batch[local_index];
- }
-
- seen_messages += batch_count;
- }
-
- unreachable!("Failed to find message at index {}", index);
- }
-}
-
-impl Sizeable for IggyMessagesBatchSet {
- fn get_size_bytes(&self) -> IggyByteSize {
- IggyByteSize::from(self.size as u64)
- }
-}
-
-impl From<Vec<IggyMessagesBatchMut>> for IggyMessagesBatchSet {
- fn from(messages: Vec<IggyMessagesBatchMut>) -> Self {
- Self::from_vec(messages)
- }
-}
-
-impl From<IggyMessagesBatchMut> for IggyMessagesBatchSet {
- fn from(messages: IggyMessagesBatchMut) -> Self {
- Self::from_vec(vec![messages])
- }
-}
diff --git a/core/server_common/src/segment_storage/index_reader.rs b/core/server_common/src/segment_storage/index_reader.rs
index 7f45c84..b4fb651 100644
--- a/core/server_common/src/segment_storage/index_reader.rs
+++ b/core/server_common/src/segment_storage/index_reader.rs
@@ -15,375 +15,37 @@
// specific language governing permissions and limitations
// under the License.
-use crate::{IggyIndexesMut, PooledBuffer};
-use compio::{
- buf::{IntoInner, IoBuf},
- fs::{File, OpenOptions},
- io::AsyncReadAtExt,
-};
+use compio::fs::OpenOptions;
use err_trail::ErrContext;
-use iggy_common::{INDEX_SIZE, IggyError, IggyIndex, IggyIndexView};
-use std::{
- io::ErrorKind,
- rc::Rc,
- sync::atomic::{AtomicU64, Ordering},
-};
-use tracing::{error, trace};
+use iggy_common::IggyError;
+use tracing::trace;
-/// A dedicated struct for reading from the index file.
+/// Path handle for a segment's index file, validated openable at segment
+/// build. Reads go through the partition's own index reader; this exists so
+/// storage plumbing (bootstrap, state transfer) can resolve the index path.
#[derive(Debug)]
pub struct IndexReader {
file_path: String,
- file: File,
- index_size_bytes: Rc<AtomicU64>,
}
-// Safety: We are guaranteeing that IndexReader will never be used from multiple threads
-unsafe impl Send for IndexReader {}
-
impl IndexReader {
- /// Opens the index file in read-only mode.
- pub async fn new(file_path: &str, index_size_bytes: Rc<AtomicU64>) -> Result<Self, IggyError> {
- let file = OpenOptions::new()
+ /// Opens the index file read-only to prove it exists, then drops the
+ /// descriptor: nothing reads through this type.
+ pub async fn new(file_path: &str) -> Result<Self, IggyError> {
+ OpenOptions::new()
.read(true)
.open(file_path)
.await
.error(|e: &std::io::Error| format!("Failed to open index file: {file_path}. {e}"))
.map_err(|_| IggyError::CannotReadFile)?;
- trace!(
- "Opened index file for reading: {file_path}, size: {}",
- index_size_bytes.load(Ordering::Acquire)
- );
+ trace!("Validated index file for reading: {file_path}");
Ok(Self {
file_path: file_path.to_string(),
- file,
- index_size_bytes,
})
}
pub fn path(&self) -> String {
self.file_path.clone()
}
-
- /// Loads all indexes from the index file into the optimized binary format.
- /// Note that this function does not use the pool, as the messages are not cached.
- /// This is expected - this method is called at startup and we want to preserve
- /// memory pool usage.
- pub async fn load_all_indexes_from_disk(&self) -> Result<IggyIndexesMut, IggyError> {
- let file_size = self.file_size();
- if file_size == 0 {
- return Ok(IggyIndexesMut::empty());
- }
-
- let buf = match self.read_at(0, file_size, false).await {
- Ok(buf) => buf,
- Err(e) if e.kind() == ErrorKind::UnexpectedEof => {
- return Ok(IggyIndexesMut::empty());
- }
- Err(e) => {
- error!(
- "Error reading batch header at offset 0 in file {}: {e}",
- self.file_path
- );
- return Err(IggyError::CannotReadFile);
- }
- };
- let index_count = file_size / INDEX_SIZE as u32;
- let mut indexes = IggyIndexesMut::from_bytes(buf, 0);
- if indexes.count() != index_count {
- error!(
- "Loaded {} indexes from disk, expected {}, file {} is probably corrupted!",
- indexes.count(),
- index_count,
- self.file_path
- );
- }
- indexes.mark_saved();
- Ok(indexes)
- }
-
- /// Loads a specific range of indexes from disk based on offset.
- ///
- /// Returns a slice of indexes starting at the relative_start_offset with the specified count,
- /// or None if the requested range is not available.
- pub async fn load_from_disk_by_offset(
- &self,
- relative_start_offset: u32,
- count: u32,
- ) -> Result<Option<IggyIndexesMut>, IggyError> {
- let file_size = self.file_size();
- let total_indexes = file_size / INDEX_SIZE as u32;
-
- if file_size == 0 || total_indexes == 0 {
- trace!(
- "Index file {} is empty, cannot load indexes",
- self.file_path
- );
- return Ok(None);
- }
-
- if relative_start_offset >= total_indexes {
- trace!(
- "Start offset {} is out of bounds. Total indexes: {}",
- relative_start_offset, total_indexes
- );
- return Ok(None);
- }
-
- let available_count = total_indexes.saturating_sub(relative_start_offset);
- let actual_count = std::cmp::min(count, available_count);
-
- if actual_count == 0 {
- trace!(
- "No available indexes to load. Start offset: {}, requested count: {}",
- relative_start_offset, count
- );
- return Ok(None);
- }
-
- let start_byte = relative_start_offset as usize * INDEX_SIZE;
- let end_byte = start_byte + (actual_count as usize * INDEX_SIZE);
-
- let indexes_bytes = match self
- .read_at(start_byte as u32, (end_byte - start_byte) as u32, true)
- .await
- {
- Ok(buf) => buf,
- Err(e) if e.kind() == ErrorKind::UnexpectedEof => {
- error!("Unexpected EOF while reading indexes");
- return Ok(None);
- }
- Err(e) => {
- error!(
- "Error reading {actual_count} indexes at position {relative_start_offset} in file {} of size {file_size}: {e}",
- self.file_path
- );
- return Err(IggyError::CannotReadFile);
- }
- };
-
- let base_position = if relative_start_offset > 0 {
- match self.load_nth_index(relative_start_offset - 1).await? {
- Some(prev_index) => prev_index.position,
- None => {
- trace!(
- "Failed to load previous index at position {}",
- relative_start_offset - 1
- );
- 0
- }
- }
- } else {
- 0
- };
-
- let indexes = IggyIndexesMut::from_bytes(indexes_bytes, base_position);
-
- trace!(
- "Loaded {actual_count} indexes from disk starting at offset {relative_start_offset}, base position: {base_position}, last position: {}",
- indexes.last_position()
- );
-
- Ok(Some(indexes))
- }
-
- /// Loads a specific range of indexes from disk based on timestamp.
- ///
- /// Returns a slice of indexes starting from the index with timestamp closest to
- /// (but not exceeding) the requested timestamp, with the specified count.
- pub async fn load_from_disk_by_timestamp(
- &self,
- timestamp: u64,
- count: u32,
- ) -> Result<Option<IggyIndexesMut>, IggyError> {
- let file_size = self.file_size();
- let total_indexes = file_size / INDEX_SIZE as u32;
-
- if file_size == 0 || total_indexes == 0 {
- trace!("Index file is empty");
- return Ok(None);
- }
-
- let start_index_pos = match self
- .binary_search_position_for_timestamp_async(timestamp)
- .await?
- {
- Some(pos) => pos,
- None => return Ok(None),
- };
-
- let available_count = total_indexes.saturating_sub(start_index_pos);
- let actual_count = std::cmp::min(count, available_count);
-
- if actual_count == 0 {
- trace!(
- "No available indexes to load. Start index pos: {}, requested count: {}",
- start_index_pos, count
- );
- return Ok(None);
- }
-
- let start_byte = start_index_pos as usize * INDEX_SIZE;
- let end_byte = start_byte + (actual_count as usize * INDEX_SIZE);
-
- let indexes_bytes = match self
- .read_at(start_byte as u32, (end_byte - start_byte) as u32, true)
- .await
- {
- Ok(buf) => buf,
- Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Ok(None),
- Err(e) => {
- error!(
- "Error reading {actual_count} indexes at position {start_index_pos} in file {}: {e}",
- self.file_path
- );
- return Err(IggyError::CannotReadFile);
- }
- };
-
- let base_position = if start_index_pos > 0 {
- match self.load_nth_index(start_index_pos - 1).await? {
- Some(prev_index) => prev_index.position,
- None => {
- trace!(
- "Failed to load previous index at position {}",
- start_index_pos - 1
- );
- 0
- }
- }
- } else {
- 0
- };
-
- trace!(
- "Loaded {} indexes from disk starting at timestamp {}, base position: {}",
- actual_count, timestamp, base_position
- );
-
- Ok(Some(IggyIndexesMut::from_bytes(
- indexes_bytes,
- base_position,
- )))
- }
-
- /// Finds the position of the index with timestamp closest to (but not exceeding) the target
- async fn binary_search_position_for_timestamp_async(
- &self,
- target_timestamp: u64,
- ) -> Result<Option<u32>, IggyError> {
- let file_size = self.file_size();
- if file_size == 0 {
- return Ok(None);
- }
-
- let total_indexes = file_size / INDEX_SIZE as u32;
- if total_indexes == 0 {
- return Ok(None);
- }
-
- let last_idx = match self.load_nth_index(total_indexes - 1).await? {
- Some(idx) => idx,
- None => return Ok(None),
- };
-
- if target_timestamp > last_idx.timestamp {
- return Ok(Some(total_indexes - 1));
- }
-
- let first_idx = match self.load_nth_index(0).await? {
- Some(idx) => idx,
- None => return Ok(None),
- };
-
- if target_timestamp <= first_idx.timestamp {
- return Ok(Some(0));
- }
-
- let mut low = 0;
- let mut high = total_indexes - 1;
-
- while low <= high {
- let mid = low + (high - low) / 2;
- let mid_index = match self.load_nth_index(mid).await? {
- Some(idx) => idx,
- None => break,
- };
-
- match mid_index.timestamp.cmp(&target_timestamp) {
- std::cmp::Ordering::Equal => return Ok(Some(mid)),
- std::cmp::Ordering::Less => low = mid + 1,
- std::cmp::Ordering::Greater => {
- if mid == 0 {
- break;
- }
- high = mid - 1;
- }
- }
- }
-
- Ok(Some(low))
- }
-
- /// Returns the size of the index file in bytes.
- fn file_size(&self) -> u32 {
- self.index_size_bytes.load(Ordering::Acquire) as u32
- }
-
- /// Reads a specified number of bytes from the index file at a given offset.
- async fn read_at(
- &self,
- offset: u32,
- len: u32,
- _use_pool: bool,
- ) -> Result<PooledBuffer, std::io::Error> {
- let buf = PooledBuffer::with_capacity(len as usize);
-
- let (result, buf) = self
- .file
- .read_exact_at(buf.slice(..len as usize), offset as u64)
- .await
- .into();
- let buf = buf.into_inner();
- result?;
- Ok(buf)
- }
-
- /// Gets the nth index from the index file.
- ///
- /// The index position is 0-based (first index is at position 0).
- /// Returns None if the specified position is out of bounds.
- async fn load_nth_index(&self, position: u32) -> Result<Option<IggyIndex>, IggyError> {
- let file_size = self.file_size();
- let total_indexes = file_size / INDEX_SIZE as u32;
-
- if position >= total_indexes {
- trace!(
- "Index position {} is out of bounds. Total indexes: {}",
- position, total_indexes
- );
- return Ok(None);
- }
-
- let offset = position * INDEX_SIZE as u32;
-
- let buf = match self.read_at(offset, INDEX_SIZE as u32, true).await {
- Ok(buf) => buf,
- Err(e) if e.kind() == ErrorKind::UnexpectedEof => {
- return Ok(None);
- }
- Err(e) => {
- error!(
- "Error reading index at position {} (offset {}) in file {}: {e}",
- position, offset, self.file_path
- );
- return Err(IggyError::CannotReadFile);
- }
- };
-
- let index = IggyIndexView::new(&buf).to_index();
-
- Ok(Some(index))
- }
}
diff --git a/core/server_common/src/segment_storage/index_writer.rs b/core/server_common/src/segment_storage/index_writer.rs
index 0ea1ce8..f0603fb 100644
--- a/core/server_common/src/segment_storage/index_writer.rs
+++ b/core/server_common/src/segment_storage/index_writer.rs
@@ -15,12 +15,9 @@
// specific language governing permissions and limitations
// under the License.
-use crate::PooledBuffer;
use compio::fs::File;
use compio::fs::OpenOptions;
-use compio::io::AsyncWriteAtExt;
use err_trail::ErrContext;
-use iggy_common::INDEX_SIZE;
use iggy_common::IggyError;
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
@@ -32,7 +29,6 @@
file_path: String,
file: File,
index_size_bytes: Rc<AtomicU64>,
- fsync: bool,
}
// Safety: We are guaranteeing that IndexWriter will never be used from multiple threads
@@ -43,7 +39,6 @@
pub async fn new(
file_path: &str,
index_size_bytes: Rc<AtomicU64>,
- fsync: bool,
file_exists: bool,
) -> Result<Self, IggyError> {
let mut opts = OpenOptions::new();
@@ -82,48 +77,9 @@
file_path: file_path.to_string(),
file,
index_size_bytes,
- fsync,
})
}
- /// Appends multiple index buffer to the index file in a single operation.
- pub async fn save_indexes(&self, indexes: PooledBuffer) -> Result<(), IggyError> {
- if indexes.is_empty() {
- return Ok(());
- }
-
- let count = indexes.len() / INDEX_SIZE;
- let len = indexes.len();
-
- let position = self.index_size_bytes.load(Ordering::Relaxed);
- let file = &self.file;
- (&*file)
- .write_all_at(indexes, position)
- .await
- .0
- .error(|e: &std::io::Error| {
- format!(
- "Failed to write {} indexes to file: {}. {e}",
- count, self.file_path
- )
- })
- .map_err(|_| IggyError::CannotSaveIndexToSegment)?;
-
- self.index_size_bytes
- .fetch_add(len as u64, Ordering::Release);
-
- if self.fsync {
- let _ = self.fsync().await;
- }
- trace!(
- "Saved {count} indexes of size {} to file: {}",
- INDEX_SIZE * count,
- self.file_path
- );
-
- Ok(())
- }
-
pub fn size_counter(&self) -> Rc<AtomicU64> {
self.index_size_bytes.clone()
}
diff --git a/core/server_common/src/segment_storage/messages_reader.rs b/core/server_common/src/segment_storage/messages_reader.rs
index ecfe7e2..6c07e45 100644
--- a/core/server_common/src/segment_storage/messages_reader.rs
+++ b/core/server_common/src/segment_storage/messages_reader.rs
@@ -15,136 +15,39 @@
// specific language governing permissions and limitations
// under the License.
-use crate::{IggyIndexesMut, IggyMessagesBatchMut, PooledBuffer};
-use compio::buf::{IntoInner, IoBuf};
-use compio::fs::{File, OpenOptions};
-use compio::io::AsyncReadAtExt;
+use compio::fs::OpenOptions;
use err_trail::ErrContext;
use iggy_common::IggyError;
-use std::rc::Rc;
-use std::{
- io::ErrorKind,
- sync::atomic::{AtomicU64, Ordering},
-};
-use tracing::{error, trace};
+use tracing::trace;
-/// A dedicated struct for reading from the messages file.
+/// Path handle for a segment's messages file, validated openable at segment
+/// build. Reads go through the partition's own sealed-segment handles; this
+/// exists so storage plumbing (bootstrap, state transfer) can resolve the
+/// segment path.
#[derive(Debug)]
pub struct MessagesReader {
file_path: String,
- file: File,
- messages_size_bytes: Rc<AtomicU64>,
}
-// Safety: We are guaranteeing that MessagesReader will never be used from multiple threads
-unsafe impl Send for MessagesReader {}
-
impl MessagesReader {
- /// Opens the messages file in read mode.
- pub async fn new(
- file_path: &str,
- messages_size_bytes: Rc<AtomicU64>,
- ) -> Result<Self, IggyError> {
- let file = OpenOptions::new()
+ /// Opens the messages file read-only to prove it exists, then drops the
+ /// descriptor: nothing reads through this type.
+ pub async fn new(file_path: &str) -> Result<Self, IggyError> {
+ OpenOptions::new()
.read(true)
.open(file_path)
.await
.error(|e: &std::io::Error| format!("Failed to open messages file: {file_path}. {e}"))
.map_err(|_| IggyError::CannotReadFile)?;
- // posix_fadvise() is Linux-only in the nix crate
- #[cfg(target_os = "linux")]
- {
- let _ = nix::fcntl::posix_fadvise(
- &file,
- 0,
- 0, // 0 means the entire file
- nix::fcntl::PosixFadviseAdvice::POSIX_FADV_SEQUENTIAL,
- )
- .info(|e: &nix::errno::Errno| {
- format!(
- "Failed to set sequential access pattern on messages file: {file_path}. {e}"
- )
- });
- }
-
- let size_bytes = messages_size_bytes.load(Ordering::Relaxed);
- trace!(
- "Opened messages file for reading: {file_path}, size: {}",
- size_bytes
- );
+ trace!("Validated messages file for reading: {file_path}");
Ok(Self {
file_path: file_path.to_string(),
- file,
- messages_size_bytes,
})
}
pub fn path(&self) -> String {
self.file_path.clone()
}
-
- /// Loads and returns a batch of messages from the messages file.
- pub async fn load_messages_from_disk(
- &self,
- indexes: IggyIndexesMut,
- ) -> Result<IggyMessagesBatchMut, IggyError> {
- let file_size = self.file_size();
- if file_size == 0 {
- return Ok(IggyMessagesBatchMut::empty());
- }
-
- let start_pos = indexes.base_position();
- let count_bytes = indexes.messages_size();
- let messages_count = indexes.count();
-
- if start_pos + count_bytes > file_size {
- return Ok(IggyMessagesBatchMut::empty());
- }
-
- let messages_bytes = match self.read_at(start_pos, count_bytes, true).await {
- Ok(buf) => buf,
- Err(e) if e.kind() == ErrorKind::UnexpectedEof => {
- return Ok(IggyMessagesBatchMut::empty());
- }
- Err(e) => {
- error!(
- "Error reading {messages_count} messages at position {start_pos} in file {} of size {}: {e}",
- self.file_path, file_size
- );
- return Err(IggyError::CannotReadMessage);
- }
- };
-
- Ok(IggyMessagesBatchMut::from_indexes_and_messages(
- indexes,
- messages_bytes,
- ))
- }
-
- /// Returns the size of the messages file in bytes.
- pub fn file_size(&self) -> u32 {
- self.messages_size_bytes.load(Ordering::Acquire) as u32
- }
-
- /// Reads `len` bytes from the messages file at the specified `offset`.
- async fn read_at(
- &self,
- offset: u32,
- len: u32,
- _use_pool: bool,
- ) -> Result<PooledBuffer, std::io::Error> {
- let buf = PooledBuffer::with_capacity(len as usize);
-
- let (result, buf) = self
- .file
- .read_exact_at(buf.slice(..len as usize), offset as u64)
- .await
- .into();
-
- let buf = buf.into_inner();
- result?;
- Ok(buf)
- }
}
diff --git a/core/server_common/src/segment_storage/messages_writer.rs b/core/server_common/src/segment_storage/messages_writer.rs
index 214c719..1d0094b 100644
--- a/core/server_common/src/segment_storage/messages_writer.rs
+++ b/core/server_common/src/segment_storage/messages_writer.rs
@@ -15,10 +15,9 @@
// specific language governing permissions and limitations
// under the License.
-use super::write_batch_frozen;
use compio::fs::{File, OpenOptions};
use err_trail::ErrContext;
-use iggy_common::{IggyByteSize, IggyError, IggyMessagesBatch};
+use iggy_common::IggyError;
use std::{
rc::Rc,
sync::atomic::{AtomicU64, Ordering},
@@ -31,7 +30,6 @@
file_path: String,
file: File,
messages_size_bytes: Rc<AtomicU64>,
- fsync: bool,
}
// Safety: We are guaranteeing that MessagesWriter will never be used from multiple threads
@@ -46,7 +44,6 @@
pub async fn new(
file_path: &str,
messages_size_bytes: Rc<AtomicU64>,
- fsync: bool,
file_exists: bool,
) -> Result<Self, IggyError> {
let mut opts = OpenOptions::new();
@@ -90,38 +87,9 @@
file_path: file_path.to_string(),
file,
messages_size_bytes,
- fsync,
})
}
- /// Append frozen (immutable) batches to the messages file.
- /// The caller retains the batches (for use in in-flight buffer) while disk I/O proceeds.
- pub async fn save_frozen_batches(
- &self,
- batches: &[IggyMessagesBatch],
- ) -> Result<IggyByteSize, IggyError> {
- let messages_size: u64 = batches.iter().map(|b| b.size() as u64).sum();
-
- let position = self.messages_size_bytes.load(Ordering::Relaxed);
- let file = &self.file;
- write_batch_frozen(file, position, batches)
- .await
- .error(|e: &IggyError| {
- format!(
- "Failed to write frozen batch to messages file: {}. {e}",
- self.file_path
- )
- })?;
-
- if self.fsync {
- let _ = self.fsync().await;
- }
-
- self.messages_size_bytes
- .fetch_add(messages_size, Ordering::Release);
-
- Ok(IggyByteSize::from(messages_size))
- }
pub fn path(&self) -> String {
self.file_path.clone()
}
diff --git a/core/server_common/src/segment_storage/mod.rs b/core/server_common/src/segment_storage/mod.rs
index 953b359..e88d90f 100644
--- a/core/server_common/src/segment_storage/mod.rs
+++ b/core/server_common/src/segment_storage/mod.rs
@@ -20,64 +20,14 @@
mod messages_reader;
mod messages_writer;
-use bytes::Bytes;
-use compio::{fs::File, io::AsyncWriteAtExt};
-use iggy_common::{IggyError, IggyMessagesBatch};
+use iggy_common::IggyError;
use std::rc::Rc;
-use tracing::error;
pub use index_reader::IndexReader;
pub use index_writer::IndexWriter;
pub use messages_reader::MessagesReader;
pub use messages_writer::MessagesWriter;
-/// Maximum number of IO vectors for a single writev() call.
-/// Linux typically has IOV_MAX=1024, but we use a conservative value to ensure
-/// cross-platform compatibility and leave room for any internal overhead.
-const MAX_IOV_COUNT: usize = 1024;
-
-/// Vectored write frozen (immutable) batches to file.
-pub async fn write_batch_frozen(
- file: &File,
- position: u64,
- batches: &[IggyMessagesBatch],
-) -> Result<usize, IggyError> {
- let (total_written, buffers) = batches.iter().fold(
- (0usize, Vec::with_capacity(batches.len())),
- |(size, mut bufs), batch| {
- bufs.push(batch.messages_bytes());
- (size + batch.size() as usize, bufs)
- },
- );
-
- write_vectored_chunked_bytes(file, position, buffers).await?;
- Ok(total_written)
-}
-
-/// Writes Bytes buffers to file using vectored I/O, chunking to respect IOV_MAX limits.
-async fn write_vectored_chunked_bytes(
- file: &File,
- mut position: u64,
- buffers: Vec<Bytes>,
-) -> Result<(), IggyError> {
- for chunk in buffers.chunks(MAX_IOV_COUNT) {
- let chunk_size: usize = chunk.iter().map(|b| b.len()).sum();
- let chunk_vec: Vec<Bytes> = chunk.to_vec();
-
- let (result, _) = (&*file)
- .write_vectored_all_at(chunk_vec, position)
- .await
- .into();
- result.map_err(|e| {
- error!("Failed to write frozen batch to messages file: {e}");
- IggyError::CannotWriteToFile
- })?;
-
- position += chunk_size as u64;
- }
- Ok(())
-}
-
unsafe impl Send for SegmentStorage {}
#[derive(Debug, Clone, Default)]
@@ -94,27 +44,21 @@
index_path: &str,
messages_size: u64,
indexes_size: u64,
- log_fsync: bool,
- index_fsync: bool,
file_exists: bool,
) -> Result<Self, IggyError> {
let size = Rc::new(std::sync::atomic::AtomicU64::new(messages_size));
let indexes_size = Rc::new(std::sync::atomic::AtomicU64::new(indexes_size));
- let messages_writer = Rc::new(
- MessagesWriter::new(messages_path, size.clone(), log_fsync, file_exists).await?,
- );
+ let messages_writer = Rc::new(MessagesWriter::new(messages_path, size, file_exists).await?);
- let index_writer = Rc::new(
- IndexWriter::new(index_path, indexes_size.clone(), index_fsync, file_exists).await?,
- );
+ let index_writer = Rc::new(IndexWriter::new(index_path, indexes_size, file_exists).await?);
if file_exists {
messages_writer.fsync().await?;
index_writer.fsync().await?;
}
- let messages_reader = Rc::new(MessagesReader::new(messages_path, size).await?);
- let index_reader = Rc::new(IndexReader::new(index_path, indexes_size).await?);
+ let messages_reader = Rc::new(MessagesReader::new(messages_path).await?);
+ let index_reader = Rc::new(IndexReader::new(index_path).await?);
Ok(Self {
messages_writer: Some(messages_writer),
messages_reader: Some(messages_reader),
diff --git a/core/server_common/src/send_messages.rs b/core/server_common/src/send_messages.rs
new file mode 100644
index 0000000..c0a51eb
--- /dev/null
+++ b/core/server_common/src/send_messages.rs
@@ -0,0 +1,1307 @@
+// 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.
+
+//! Server-side handling of the canonical message batch (layout in
+//! [`iggy_binary_protocol::batch`]).
+//!
+//! Producers put the batch on the wire themselves; admission
+//! ([`convert_request_message`]) verifies the producer's checksums, strips the
+//! routing metadata, and stamps the partition. From there the same bytes are
+//! journaled, replicated, persisted, and served back to polls.
+
+use crate::consensus_message::{MESSAGE_ALIGN, Message};
+use crate::iobuf::Owned;
+use crate::sharding::IggyNamespace;
+use bytes::{Bytes, BytesMut};
+use iggy_binary_protocol::batch;
+use iggy_binary_protocol::requests::messages::SendMessagesHeader as SendMessagesMetadata;
+use iggy_binary_protocol::{PrepareHeader, RoutedRequestHeader, WireDecode, WireError};
+use iggy_common::{EncryptorKind, IggyError, random_id};
+use twox_hash::XxHash3_64;
+
+pub use iggy_binary_protocol::batch::{
+ BATCH_CHECKSUM_OFFSET, BATCH_HEADER_SIZE, BATCH_MESSAGE_HEADER_SIZE, BatchHeader,
+ BatchIntegrity, BatchIterator, BatchIteratorWithOffsets, BatchMessageHeader, BatchMessageView,
+ BatchMessageViewWithOffsets, BatchRef, MAX_TIMESTAMP_DELTA_MICROS, calculate_batch_checksum,
+};
+
+/// Size of the batch header at the front of a `SendMessages` prepare body.
+pub const COMMAND_HEADER_SIZE: usize = BATCH_HEADER_SIZE;
+
+/// Offset of the blob inside a prepare frame:
+/// `[256B PrepareHeader][256B batch header][blob]`.
+pub const PREPARE_SPLIT_POINT: usize = 512;
+
+/// Map a wire-level batch error onto the typed `IggyError` the server paths
+/// key on. The two integrity variants keep their payloads; every structural
+/// error is an invalid command.
+fn batch_error(error: &WireError) -> IggyError {
+ match *error {
+ WireError::InvalidBatchChecksum {
+ stored,
+ computed,
+ base_offset,
+ } => IggyError::InvalidBatchChecksum(stored, computed, base_offset),
+ WireError::InvalidMessageChecksum {
+ stored,
+ computed,
+ offset,
+ } => IggyError::InvalidMessageChecksum(stored, computed, offset),
+ _ => IggyError::InvalidCommand,
+ }
+}
+
+/// Decode one batch record, verifying the batch checksum and every
+/// per-message checksum.
+///
+/// See [`batch::decode_batch_slice`]; this wrapper types the error for the
+/// server paths.
+///
+/// # Errors
+/// [`IggyError::InvalidCommand`] for a short or inconsistent record, and
+/// [`IggyError::InvalidBatchChecksum`] / [`IggyError::InvalidMessageChecksum`]
+/// on an integrity mismatch.
+pub fn decode_batch_slice(body: &[u8]) -> Result<BatchRef<'_>, IggyError> {
+ batch::decode_batch_slice(body).map_err(|error| batch_error(&error))
+}
+
+/// [`decode_batch_slice`] with the integrity level chosen by the caller.
+///
+/// The one caller that passes anything but [`BatchIntegrity::Verify`] is the
+/// disk poll under its operator knob; layout checks are not optional either
+/// way.
+///
+/// # Errors
+/// See [`decode_batch_slice`].
+pub fn decode_batch_slice_with(
+ body: &[u8],
+ integrity: BatchIntegrity,
+) -> Result<BatchRef<'_>, IggyError> {
+ batch::decode_batch_slice_with(body, integrity).map_err(|error| batch_error(&error))
+}
+
+#[derive(Debug, Clone)]
+pub struct SendMessagesOwned {
+ pub header: BatchHeader,
+ pub blob: Bytes,
+}
+
+impl SendMessagesOwned {
+ pub fn from_messages(
+ namespace: IggyNamespace,
+ messages: &IggyMessages,
+ ) -> Result<Self, IggyError> {
+ let message_count = messages.count();
+ let mut origin_timestamp = u64::MAX;
+ for message in messages {
+ origin_timestamp = origin_timestamp.min(message.header.origin_timestamp);
+ }
+
+ if origin_timestamp == u64::MAX {
+ origin_timestamp = 0;
+ }
+
+ let mut blob = BytesMut::new();
+ for (index, message) in messages.iter().enumerate() {
+ let id = if message.header.id == 0 {
+ random_id::get_uuid()
+ } else {
+ message.header.id
+ };
+ let offset_delta = u32::try_from(index).map_err(|_| IggyError::InvalidCommand)?;
+ let timestamp_delta = message
+ .header
+ .origin_timestamp
+ .checked_sub(origin_timestamp)
+ .ok_or(IggyError::InvalidCommand)?;
+ if timestamp_delta > MAX_TIMESTAMP_DELTA_MICROS {
+ return Err(IggyError::InvalidMessageTimestampDelta(timestamp_delta));
+ }
+ let timestamp_delta =
+ u32::try_from(timestamp_delta).map_err(|_| IggyError::InvalidCommand)?;
+ let user_headers = message.user_headers.as_deref().unwrap_or_default();
+ let user_headers_length =
+ u32::try_from(user_headers.len()).map_err(|_| IggyError::InvalidCommand)?;
+ let payload_length =
+ u32::try_from(message.payload.len()).map_err(|_| IggyError::InvalidCommand)?;
+
+ let mut header = [0u8; BATCH_MESSAGE_HEADER_SIZE];
+ header[8..24].copy_from_slice(&id.to_le_bytes());
+ header[24..28].copy_from_slice(&offset_delta.to_le_bytes());
+ header[28..32].copy_from_slice(×tamp_delta.to_le_bytes());
+ header[32..36].copy_from_slice(&user_headers_length.to_le_bytes());
+ header[36..40].copy_from_slice(&payload_length.to_le_bytes());
+
+ let msg_start = blob.len();
+ blob.extend_from_slice(&header);
+ blob.extend_from_slice(&message.payload);
+ blob.extend_from_slice(user_headers);
+ let checksum = XxHash3_64::oneshot(&blob[msg_start + 8..]);
+ blob[msg_start..msg_start + 8].copy_from_slice(&checksum.to_le_bytes());
+ }
+
+ let blob = blob.freeze();
+ let mut header = BatchHeader::new(
+ namespace.partition_id() as u64,
+ origin_timestamp,
+ u64::try_from(COMMAND_HEADER_SIZE + blob.len())
+ .map_err(|_| IggyError::InvalidCommand)?,
+ message_count,
+ );
+ header.batch_checksum = calculate_batch_checksum(&header, &blob);
+
+ Ok(Self { header, blob })
+ }
+
+ pub fn encode_request(
+ self,
+ mut request_header: RoutedRequestHeader,
+ ) -> Result<Message<RoutedRequestHeader>, IggyError> {
+ let total_size = std::mem::size_of::<RoutedRequestHeader>() + self.header.total_size();
+ // The rebuilt body differs in size from the wire body the header
+ // described; a stale `size` truncates the blob for every downstream
+ // slice (stamping, journal reads).
+ request_header.size = u32::try_from(total_size).map_err(|_| IggyError::InvalidCommand)?;
+ let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total_size);
+ let bytes = buffer.as_mut_slice();
+ bytes[0..std::mem::size_of::<RoutedRequestHeader>()]
+ .copy_from_slice(bytemuck::bytes_of(&request_header));
+ self.header.encode_into(
+ &mut bytes[std::mem::size_of::<RoutedRequestHeader>()
+ ..std::mem::size_of::<RoutedRequestHeader>() + COMMAND_HEADER_SIZE],
+ );
+ bytes[PREPARE_SPLIT_POINT..PREPARE_SPLIT_POINT + self.blob.len()]
+ .copy_from_slice(&self.blob);
+
+ Message::try_from(buffer).map_err(|_| IggyError::InvalidCommand)
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub struct IggyMessageHeader {
+ pub checksum: u64,
+ pub id: u128,
+ pub offset: u64,
+ pub timestamp: u64,
+ pub origin_timestamp: u64,
+ pub user_headers_length: u32,
+ pub payload_length: u32,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct IggyMessage {
+ pub header: IggyMessageHeader,
+ pub payload: Bytes,
+ pub user_headers: Option<Bytes>,
+}
+
+#[derive(Debug, Clone, Default, PartialEq, Eq)]
+pub struct IggyMessages {
+ messages: Vec<IggyMessage>,
+}
+
+impl IggyMessages {
+ #[must_use]
+ pub fn empty() -> Self {
+ Self::default()
+ }
+
+ #[must_use]
+ pub fn with_capacity(capacity: usize) -> Self {
+ Self {
+ messages: Vec::with_capacity(capacity),
+ }
+ }
+
+ pub fn push(&mut self, message: IggyMessage) {
+ self.messages.push(message);
+ }
+
+ #[must_use]
+ pub fn count(&self) -> u32 {
+ u32::try_from(self.messages.len()).unwrap_or(u32::MAX)
+ }
+
+ #[must_use]
+ pub const fn is_empty(&self) -> bool {
+ self.messages.is_empty()
+ }
+
+ #[must_use]
+ pub fn first_offset(&self) -> Option<u64> {
+ self.messages.first().map(|message| message.header.offset)
+ }
+
+ #[must_use]
+ pub fn last_offset(&self) -> Option<u64> {
+ self.messages.last().map(|message| message.header.offset)
+ }
+
+ #[must_use]
+ pub fn limit(self, count: u32) -> Self {
+ let mut messages = self.messages;
+ messages.truncate(usize::try_from(count).unwrap_or(usize::MAX));
+ Self { messages }
+ }
+
+ pub fn iter(&self) -> std::slice::Iter<'_, IggyMessage> {
+ self.messages.iter()
+ }
+}
+
+impl IntoIterator for IggyMessages {
+ type Item = IggyMessage;
+ type IntoIter = std::vec::IntoIter<IggyMessage>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.messages.into_iter()
+ }
+}
+
+impl<'a> IntoIterator for &'a IggyMessages {
+ type Item = &'a IggyMessage;
+ type IntoIter = std::slice::Iter<'a, IggyMessage>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.messages.iter()
+ }
+}
+
+/// Encode `header` into its own frozen 256-byte buffer (e.g. the rewritten
+/// header fragment of a server-sliced poll batch).
+#[must_use]
+pub fn frozen_batch_header(header: &BatchHeader) -> crate::iobuf::Frozen<MESSAGE_ALIGN> {
+ let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(COMMAND_HEADER_SIZE);
+ header.encode_into(buffer.as_mut_slice());
+ buffer.into()
+}
+
+/// Re-encode a canonical `SendMessages` request with every message's payload
+/// and user headers encrypted, per-message checksums and lengths recomputed,
+/// and the batch header (length + checksum) restamped.
+///
+/// Runs ONCE, on the primary at ingestion (after [`convert_request_message`]
+/// admitted the wire form), so the ciphertext is what replicates: every
+/// replica journals and persists identical bytes, and the poll path decrypts
+/// uniformly regardless of which replica or tier served the fragment.
+///
+/// # Errors
+///
+/// [`IggyError::InvalidCommand`] on an undecodable batch; encryption errors
+/// propagate from the encryptor.
+pub fn encrypt_batch_request(
+ message: Message<RoutedRequestHeader>,
+ encryptor: &EncryptorKind,
+) -> Result<Message<RoutedRequestHeader>, IggyError> {
+ let request_header = *message.header();
+ let total_size = request_header.size as usize;
+ let body = &message.as_slice()[std::mem::size_of::<RoutedRequestHeader>()..total_size];
+ let batch = decode_batch_slice(body)?;
+
+ let mut blob = BytesMut::with_capacity(batch.blob().len() * 2);
+ for view in batch.iter() {
+ let encrypted_payload = encryptor.encrypt(view.payload)?;
+ let encrypted_user_headers = if view.user_headers.is_empty() {
+ None
+ } else {
+ Some(encryptor.encrypt(view.user_headers)?)
+ };
+ let user_headers: &[u8] = encrypted_user_headers.as_deref().unwrap_or_default();
+ let payload_length =
+ u32::try_from(encrypted_payload.len()).map_err(|_| IggyError::InvalidCommand)?;
+ let user_headers_length =
+ u32::try_from(user_headers.len()).map_err(|_| IggyError::InvalidCommand)?;
+
+ let mut header = [0u8; BATCH_MESSAGE_HEADER_SIZE];
+ header[8..24].copy_from_slice(&view.header.id.to_le_bytes());
+ header[24..28].copy_from_slice(&view.header.offset_delta.to_le_bytes());
+ header[28..32].copy_from_slice(&view.header.timestamp_delta.to_le_bytes());
+ header[32..36].copy_from_slice(&user_headers_length.to_le_bytes());
+ header[36..40].copy_from_slice(&payload_length.to_le_bytes());
+ let msg_start = blob.len();
+ blob.extend_from_slice(&header);
+ blob.extend_from_slice(&encrypted_payload);
+ blob.extend_from_slice(user_headers);
+ let checksum = XxHash3_64::oneshot(&blob[msg_start + 8..]);
+ blob[msg_start..msg_start + 8].copy_from_slice(&checksum.to_le_bytes());
+ }
+
+ let blob = blob.freeze();
+ let mut header = batch.header;
+ header.batch_length =
+ u64::try_from(COMMAND_HEADER_SIZE + blob.len()).map_err(|_| IggyError::InvalidCommand)?;
+ header.batch_checksum = calculate_batch_checksum(&header, &blob);
+
+ SendMessagesOwned { header, blob }.encode_request(request_header)
+}
+
+/// Rebuild one stored batch record with every message's payload and user
+/// headers decrypted: the poll reply's single decrypt point, mirroring
+/// [`encrypt_batch_request`]. Lengths, per-message checksums, and the batch
+/// header (length + checksum) are restamped over the plaintext.
+///
+/// Framing is validated layout-only: the read path already applied its
+/// integrity knob to the stored bytes, and a server-sliced fragment's header
+/// was rewritten checksum-consistent at slice time.
+///
+/// # Errors
+/// [`IggyError::InvalidCommand`] on a malformed record;
+/// [`IggyError::CannotDecryptData`] when a section fails to decrypt.
+pub fn decrypt_batch_record(
+ record: &[u8],
+ encryptor: &EncryptorKind,
+) -> Result<Vec<u8>, IggyError> {
+ let batch = decode_batch_slice_with(record, BatchIntegrity::LayoutOnly)?;
+ if record.len() != batch.header.total_size() {
+ return Err(IggyError::InvalidCommand);
+ }
+
+ let mut blob = BytesMut::with_capacity(batch.blob().len());
+ for view in batch.iter() {
+ let payload = encryptor
+ .decrypt(view.payload)
+ .map_err(|_| IggyError::CannotDecryptData)?;
+ let decrypted_user_headers = if view.user_headers.is_empty() {
+ None
+ } else {
+ Some(
+ encryptor
+ .decrypt(view.user_headers)
+ .map_err(|_| IggyError::CannotDecryptData)?,
+ )
+ };
+ let user_headers: &[u8] = decrypted_user_headers.as_deref().unwrap_or_default();
+ let payload_length = u32::try_from(payload.len()).map_err(|_| IggyError::InvalidCommand)?;
+ let user_headers_length =
+ u32::try_from(user_headers.len()).map_err(|_| IggyError::InvalidCommand)?;
+
+ let mut header = [0u8; BATCH_MESSAGE_HEADER_SIZE];
+ header[8..24].copy_from_slice(&view.header.id.to_le_bytes());
+ header[24..28].copy_from_slice(&view.header.offset_delta.to_le_bytes());
+ header[28..32].copy_from_slice(&view.header.timestamp_delta.to_le_bytes());
+ header[32..36].copy_from_slice(&user_headers_length.to_le_bytes());
+ header[36..40].copy_from_slice(&payload_length.to_le_bytes());
+ let msg_start = blob.len();
+ blob.extend_from_slice(&header);
+ blob.extend_from_slice(&payload);
+ blob.extend_from_slice(user_headers);
+ let checksum = XxHash3_64::oneshot(&blob[msg_start + 8..]);
+ blob[msg_start..msg_start + 8].copy_from_slice(&checksum.to_le_bytes());
+ }
+
+ let mut header = batch.header;
+ header.batch_length =
+ u64::try_from(COMMAND_HEADER_SIZE + blob.len()).map_err(|_| IggyError::InvalidCommand)?;
+ header.batch_checksum = calculate_batch_checksum(&header, &blob);
+
+ let mut out = vec![0u8; COMMAND_HEADER_SIZE + blob.len()];
+ header.encode_into(&mut out[..COMMAND_HEADER_SIZE]);
+ out[COMMAND_HEADER_SIZE..].copy_from_slice(&blob);
+ Ok(out)
+}
+
+/// Whether admission stamps a batch checksum onto its output.
+///
+/// The recompute is an `XxHash3` batch-checksum pass, needed only when a reader
+/// validates the admitted batch before [`stamp_prepare_for_persistence`]
+/// recomputes it: the encrypt ingest path re-decodes the admitted batch
+/// (`encrypt_batch_request`'s validating decode, then the second `convert` its
+/// output re-enters as the canonical-batch fast path). The partition ingest
+/// path has no such reader, so it skips the pass and the checksum stays zero
+/// until stamp.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ChecksumMode {
+ /// Compute the batch checksum for the admitted batch.
+ Compute,
+ /// Leave the batch checksum zero; `stamp_prepare_for_persistence` fills it.
+ Skip,
+}
+
+/// Admit a `SendMessages` request into the pipeline form
+/// `[RoutedRequestHeader][256B batch header][blob]`.
+///
+/// Two input shapes reach this:
+/// - The wire form `[metadata][batch]` from a producer. The metadata section
+/// is validated and stripped, the batch is checksum-verified exactly as the
+/// producer hashed it (`partition_id` still zero), and the partition is
+/// stamped afterwards.
+/// - A body that already IS one canonical batch: the encrypt ingest path
+/// re-enters here with its own output. Detected first via the
+/// checksum-verified decode - a wire-form body cannot pass it, since its
+/// leading metadata bytes cannot form a batch whose length and checksum
+/// both match. The only legitimate producer of this shape is the earlier
+/// convert, which stamped the resolved partition, so a `partition_id` that
+/// does not match the namespace is rejected rather than persisted verbatim.
+///
+/// Either way the batch must fill the body exactly: `size` and `batch_length`
+/// are independent client-supplied fields, and a suffix past `batch_length`
+/// is covered by no checksum, still rides the buffer to disk, and desyncs the
+/// segment walk that advances by `batch_length`.
+///
+/// # Errors
+/// [`IggyError::InvalidCommand`] on a malformed body, an empty batch, or a
+/// metadata/batch count mismatch; typed checksum errors from the validating
+/// decode.
+pub fn convert_request_message(
+ namespace: IggyNamespace,
+ message: Message<RoutedRequestHeader>,
+ checksum: ChecksumMode,
+) -> Result<Message<RoutedRequestHeader>, IggyError> {
+ let request_header = *message.header();
+ let total_size = request_header.size as usize;
+ let body = &message.as_slice()[std::mem::size_of::<RoutedRequestHeader>()..total_size];
+
+ if let Ok(batch) = decode_batch_slice(body) {
+ if batch.message_count() == 0
+ || body.len() != batch.header.total_size()
+ || batch.header.partition_id != namespace.partition_id() as u64
+ {
+ return Err(IggyError::InvalidCommand);
+ }
+ return Ok(message);
+ }
+
+ admit_wire_request(namespace, body, request_header, checksum)
+}
+
+/// Validate a producer's `[metadata][batch]` body and rebuild it as the
+/// pipeline form with the partition stamped.
+fn admit_wire_request(
+ namespace: IggyNamespace,
+ body: &[u8],
+ mut request_header: RoutedRequestHeader,
+ checksum: ChecksumMode,
+) -> Result<Message<RoutedRequestHeader>, IggyError> {
+ if body.len() < 4 {
+ return Err(IggyError::InvalidCommand);
+ }
+ let metadata_length = u32::from_le_bytes(
+ body[..4]
+ .try_into()
+ .map_err(|_| IggyError::InvalidNumberEncoding)?,
+ ) as usize;
+ let batch_start = 4usize
+ .checked_add(metadata_length)
+ .ok_or(IggyError::InvalidCommand)?;
+ if body.len() < batch_start {
+ return Err(IggyError::InvalidCommand);
+ }
+
+ let (metadata, consumed) = SendMessagesMetadata::decode(&body[4..batch_start])
+ .map_err(|_| IggyError::InvalidCommand)?;
+ if consumed != metadata_length {
+ return Err(IggyError::InvalidCommand);
+ }
+
+ let batch_bytes = &body[batch_start..];
+ let batch = decode_batch_slice(batch_bytes)?;
+ if batch.message_count() == 0
+ || batch.message_count() != metadata.messages_count
+ || batch_bytes.len() != batch.header.total_size()
+ {
+ return Err(IggyError::InvalidCommand);
+ }
+
+ let header_size = std::mem::size_of::<RoutedRequestHeader>();
+ let total_size = header_size + batch.header.total_size();
+ request_header.size = u32::try_from(total_size).map_err(|_| IggyError::InvalidCommand)?;
+ let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total_size);
+ let bytes = buffer.as_mut_slice();
+ bytes[0..header_size].copy_from_slice(bytemuck::bytes_of(&request_header));
+ bytes[header_size..total_size].copy_from_slice(batch_bytes);
+
+ // The producer hashed `partition_id = 0`; stamp the resolved partition
+ // and restamp (or clear, for the stamp-fills-it path) the batch checksum.
+ let mut stamped = batch.header;
+ stamped.partition_id = namespace.partition_id() as u64;
+ stamped.batch_checksum = match checksum {
+ ChecksumMode::Compute => {
+ calculate_batch_checksum(&stamped, &bytes[PREPARE_SPLIT_POINT..total_size])
+ }
+ ChecksumMode::Skip => 0,
+ };
+ stamped.encode_into(&mut bytes[header_size..header_size + COMMAND_HEADER_SIZE]);
+
+ Message::try_from(buffer).map_err(|_| IggyError::InvalidCommand)
+}
+
+/// Decode a `Prepare` message from a slice of bytes, validating the batch
+/// checksum and every per-message checksum.
+///
+/// `bytes` must be 16-byte aligned (`PrepareHeader` has `u128` fields). Source
+/// from `Frozen<MESSAGE_ALIGN>` / `Owned<MESSAGE_ALIGN>` / `Message<H>`.
+/// Misalignment: `debug_assert!` in debug; `InvalidCommand` in release.
+///
+/// # Errors
+///
+/// `IggyError::InvalidCommand` on a short buffer, bad bit pattern, `size`
+/// outside `[header_size, bytes.len()]`, a `size` that does not describe the
+/// batch exactly, or frames that do not tile the batch;
+/// `InvalidBatchChecksum` / `InvalidMessageChecksum` on an integrity mismatch.
+pub fn decode_prepare_slice(bytes: &[u8]) -> Result<BatchRef<'_>, IggyError> {
+ decode_prepare_slice_inner(bytes, true)
+}
+
+/// Like [`decode_prepare_slice`] but skips the per-message checksum
+/// verification and batch-checksum recompute, extracting only the header meta.
+/// Every cheap structural check (length, 16-byte alignment, `size` bounds, and
+/// `size` describing the batch exactly) is still enforced.
+///
+/// INVARIANT: `bytes` MUST be node-local self-stamped -
+/// [`stamp_prepare_for_persistence`] recomputed the batch checksum over the
+/// exact blob on the local node - or already integrity-checked at network
+/// ingress. There is no consensus-layer blob validation: the `PrepareHeader`
+/// integrity fields are inert zeros. Replicated and repaired prepares are
+/// validated via [`decode_prepare_slice`] before the bytes reach any trusted
+/// decode. Calling this on unvalidated network bytes would let a corrupted blob
+/// pass undetected. The full-body per-message checksum pass dominates
+/// produce-path CPU, so trusted call sites that only read header meta skip it.
+///
+/// # Errors
+///
+/// Same structural errors as [`decode_prepare_slice`], minus
+/// `InvalidBatchChecksum` and `InvalidMessageChecksum`.
+pub fn decode_prepare_slice_trusted(bytes: &[u8]) -> Result<BatchRef<'_>, IggyError> {
+ decode_prepare_slice_inner(bytes, false)
+}
+
+fn decode_prepare_slice_inner(
+ bytes: &[u8],
+ validate_checksum: bool,
+) -> Result<BatchRef<'_>, IggyError> {
+ let header_size = std::mem::size_of::<PrepareHeader>();
+ if bytes.len() < header_size {
+ return Err(IggyError::InvalidCommand);
+ }
+
+ // Bytemuck enforces alignment in release (maps to InvalidCommand below);
+ // debug_assert surfaces the contract violation early in dev.
+ debug_assert_eq!(
+ bytes
+ .as_ptr()
+ .align_offset(std::mem::align_of::<PrepareHeader>()),
+ 0,
+ "decode_prepare_slice: bytes must be at least 16-byte aligned",
+ );
+
+ let prepare = bytemuck::checked::try_from_bytes::<PrepareHeader>(&bytes[..header_size])
+ .map_err(|_| IggyError::InvalidCommand)?;
+ let total_size = prepare.size as usize;
+ // Wire-controllable `size`: reject < header_size to avoid slice OOB below.
+ if total_size < header_size || bytes.len() < total_size {
+ return Err(IggyError::InvalidCommand);
+ }
+
+ let body = &bytes[header_size..total_size];
+ if body.len() < COMMAND_HEADER_SIZE {
+ return Err(IggyError::InvalidCommand);
+ }
+
+ let header =
+ BatchHeader::decode(&body[..COMMAND_HEADER_SIZE]).map_err(|error| batch_error(&error))?;
+ let blob_len = header.blob_len().map_err(|error| batch_error(&error))?;
+ // Exact, not a lower bound: a prepare frame IS one batch, so bytes past
+ // `batch_length` belong to nobody - no checksum covers them, yet the flush
+ // writes them, desyncing the segment walk. Readers walking a multi-batch
+ // chunk use `decode_batch_slice`, which bounds the blob by design.
+ if body.len() != header.total_size() {
+ return Err(IggyError::InvalidCommand);
+ }
+
+ let blob = &body[COMMAND_HEADER_SIZE..COMMAND_HEADER_SIZE + blob_len];
+ let batch = BatchRef::new(header, blob);
+ if validate_checksum {
+ let expected_checksum = batch::verify_and_recompute_batch_checksum(&batch)
+ .map_err(|error| batch_error(&error))?;
+ if header.batch_checksum != expected_checksum {
+ return Err(IggyError::InvalidBatchChecksum(
+ header.batch_checksum,
+ expected_checksum,
+ header.base_offset,
+ ));
+ }
+ }
+
+ Ok(batch)
+}
+
+pub fn stamp_prepare_for_persistence(
+ mut message: Message<PrepareHeader>,
+ base_offset: u64,
+ base_timestamp: u64,
+) -> Result<(Message<PrepareHeader>, BatchHeader, u32), IggyError> {
+ let total_size = message.header().size as usize;
+ let bytes = message.as_mut_slice();
+ if bytes.len() < PREPARE_SPLIT_POINT || total_size < PREPARE_SPLIT_POINT {
+ return Err(IggyError::InvalidCommand);
+ }
+
+ let header_offset = std::mem::size_of::<PrepareHeader>();
+ let mut command =
+ BatchHeader::decode(&bytes[header_offset..header_offset + COMMAND_HEADER_SIZE])
+ .map_err(|error| batch_error(&error))?;
+ command.base_offset = base_offset;
+ command.base_timestamp = base_timestamp;
+ let blob = &bytes[PREPARE_SPLIT_POINT..total_size];
+ command.batch_checksum = calculate_batch_checksum(&command, blob);
+ command.encode_into(&mut bytes[header_offset..header_offset + COMMAND_HEADER_SIZE]);
+ Ok((message, command, command.message_count))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use bytes::BufMut;
+ use iggy_binary_protocol::requests::messages::{RawMessage, SendMessagesEncoder};
+ use iggy_binary_protocol::{Command, Operation, WireEncode, WireIdentifier, WirePartitioning};
+ use iggy_common::Aes256GcmEncryptor;
+ use std::hash::Hasher;
+
+ fn aligned_prepare_bytes(size: u32) -> Owned<MESSAGE_ALIGN> {
+ let mut owned = Owned::<MESSAGE_ALIGN>::zeroed(std::mem::size_of::<PrepareHeader>());
+ let header: &mut PrepareHeader =
+ bytemuck::checked::try_from_bytes_mut(owned.as_mut_slice())
+ .expect("zeroed bytes form a valid PrepareHeader");
+ header.command = Command::Prepare;
+ header.size = size;
+ owned
+ }
+
+ /// Assemble an already-stamped batch into a `Prepare`:
+ /// `[PrepareHeader][256B batch header][blob]`, copying `owned`'s header and
+ /// blob verbatim. Shared by every real-batch fixture.
+ fn prepare_from_owned(owned: &SendMessagesOwned) -> Owned<MESSAGE_ALIGN> {
+ let header_size = std::mem::size_of::<PrepareHeader>();
+ let total = header_size + owned.header.total_size();
+ let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total);
+ {
+ let prepare: &mut PrepareHeader =
+ bytemuck::checked::try_from_bytes_mut(&mut buffer.as_mut_slice()[..header_size])
+ .expect("zeroed bytes form a valid PrepareHeader");
+ prepare.command = Command::Prepare;
+ prepare.size = u32::try_from(total).expect("prepare size fits u32");
+ }
+ let bytes = buffer.as_mut_slice();
+ owned
+ .header
+ .encode_into(&mut bytes[header_size..header_size + COMMAND_HEADER_SIZE]);
+ bytes[PREPARE_SPLIT_POINT..PREPARE_SPLIT_POINT + owned.blob.len()]
+ .copy_from_slice(&owned.blob);
+ buffer
+ }
+
+ /// A checksum-consistent STAMPED `Prepare` carrying real per-message records,
+ /// stamped at a non-zero `base_offset` / `base_timestamp` with a
+ /// `batch_checksum` over the final header fields + per-message checksum fields.
+ fn valid_prepare_bytes() -> Owned<MESSAGE_ALIGN> {
+ let namespace = IggyNamespace::new(1, 1, 7);
+ let mut owned = SendMessagesOwned::from_messages(namespace, &sample_messages())
+ .expect("build send batch");
+ owned.header.base_offset = 10;
+ owned.header.base_timestamp = 20;
+ owned.header.batch_checksum = owned.header.checksum_for_blob(&owned.blob);
+ prepare_from_owned(&owned)
+ }
+
+ #[test]
+ fn decode_prepare_slice_trusted_matches_validating_for_valid_batch() {
+ // The trusted variant must surface byte-identical header meta to the
+ // validating decode for a checksum-consistent batch; only the
+ // per-message and batch-checksum passes are skipped.
+ let owned = valid_prepare_bytes();
+
+ let validated = decode_prepare_slice(owned.as_slice()).expect("valid batch decodes");
+ let trusted =
+ decode_prepare_slice_trusted(owned.as_slice()).expect("valid batch decodes trusted");
+
+ assert_eq!(validated.header.base_offset, trusted.header.base_offset);
+ assert_eq!(
+ validated.header.base_timestamp,
+ trusted.header.base_timestamp
+ );
+ assert_eq!(
+ validated.header.origin_timestamp,
+ trusted.header.origin_timestamp
+ );
+ assert_eq!(validated.header.batch_length, trusted.header.batch_length);
+ assert_eq!(validated.message_count(), trusted.message_count());
+ assert_eq!(validated.header.total_size(), trusted.header.total_size());
+ assert_eq!(validated.blob(), trusted.blob());
+ }
+
+ #[test]
+ fn decode_prepare_slice_trusted_skips_batch_checksum() {
+ // A stored batch_checksum mutated after stamping fails the validating
+ // decode but passes the trusted one: exactly why the trusted variant is
+ // confined to locally-produced bytes (see its doc invariant).
+ let mut owned = valid_prepare_bytes();
+ let corrupt_index = std::mem::size_of::<PrepareHeader>() + BATCH_CHECKSUM_OFFSET;
+ owned.as_mut_slice()[corrupt_index] ^= 0xFF;
+
+ assert!(
+ matches!(
+ decode_prepare_slice(owned.as_slice()),
+ Err(IggyError::InvalidBatchChecksum(..))
+ ),
+ "validating decode must reject a mutated batch checksum",
+ );
+ assert!(
+ decode_prepare_slice_trusted(owned.as_slice()).is_ok(),
+ "trusted decode skips the batch-checksum recomputation",
+ );
+ }
+
+ #[test]
+ fn decode_prepare_slice_size_below_header_size_does_not_panic() {
+ // Regression: without the `total_size < header_size` guard,
+ // `&bytes[256..size]` panics for any size < 256.
+ for adversarial_size in [0u32, 255] {
+ let owned = aligned_prepare_bytes(adversarial_size);
+ let result = decode_prepare_slice(owned.as_slice());
+ assert!(
+ matches!(result, Err(IggyError::InvalidCommand)),
+ "size={adversarial_size} must be rejected, got {result:?}",
+ );
+ }
+ }
+
+ #[cfg(debug_assertions)]
+ #[test]
+ #[should_panic(expected = "must be at least 16-byte aligned")]
+ fn decode_prepare_slice_debug_asserts_on_misaligned_input() {
+ // `Vec<u8>` requests align=1; glibc returns a base that is a
+ // multiple of 16, so `&buf[1..]` has offset 1 mod 16, reliably
+ // misaligned.
+ let buf: Vec<u8> = vec![0u8; std::mem::size_of::<PrepareHeader>() + 1];
+ let misaligned = &buf[1..];
+ assert_ne!(
+ misaligned.as_ptr().align_offset(16),
+ 0,
+ "test setup: allocator returned non-16k base",
+ );
+ let _ = decode_prepare_slice(misaligned);
+ }
+
+ fn sample_messages() -> IggyMessages {
+ let mut messages = IggyMessages::with_capacity(2);
+ messages.push(IggyMessage {
+ header: IggyMessageHeader {
+ id: 7,
+ origin_timestamp: 1_000,
+ ..Default::default()
+ },
+ payload: Bytes::from_static(b"first-payload"),
+ user_headers: None,
+ });
+ messages.push(IggyMessage {
+ header: IggyMessageHeader {
+ id: 8,
+ origin_timestamp: 1_050,
+ ..Default::default()
+ },
+ payload: Bytes::from_static(b"second-payload"),
+ user_headers: Some(Bytes::from_static(b"user-header-bytes")),
+ });
+ messages
+ }
+
+ /// `[PrepareHeader][256B batch header][blob]` carrying real per-message
+ /// records and checksums from the production encoder, with the initial zero
+ /// base offset and timestamp.
+ fn prepare_with_messages(messages: &IggyMessages) -> Owned<MESSAGE_ALIGN> {
+ let namespace = IggyNamespace::new(1, 1, 7);
+ let owned =
+ SendMessagesOwned::from_messages(namespace, messages).expect("build send batch");
+ prepare_from_owned(&owned)
+ }
+
+ #[test]
+ fn checksum_oneshot_matches_streaming_reference() {
+ // Formula pin: the per-message checksum is XxHash3-64 (default seed)
+ // over `header[8..48] || payload || user_headers` as one byte stream.
+ // The encoders hash the concatenation in a single oneshot pass; this
+ // streaming reference feeds the same parts separately. Both must agree
+ // for every shape, or checksums at rest stop verifying.
+ fn streaming_reference(header_tail: &[u8], payload: &[u8], user_headers: &[u8]) -> u64 {
+ let mut hasher = XxHash3_64::new();
+ hasher.write(header_tail);
+ hasher.write(payload);
+ hasher.write(user_headers);
+ hasher.finish()
+ }
+
+ let header_tail: Vec<u8> = (0u8..40).collect();
+ let kilobyte: Vec<u8> = (0..1024u32).map(|index| (index % 251) as u8).collect();
+ let cases: &[(&[u8], &[u8])] = &[
+ (&[], &[]),
+ (b"payload-bytes", &[]),
+ (b"payload-bytes", b"user-header-bytes"),
+ (&kilobyte, &[]),
+ (&kilobyte, &kilobyte[..7]),
+ (&kilobyte[..1023], &kilobyte[..7]),
+ ];
+ for (payload, user_headers) in cases {
+ let mut concatenated =
+ Vec::with_capacity(header_tail.len() + payload.len() + user_headers.len());
+ concatenated.extend_from_slice(&header_tail);
+ concatenated.extend_from_slice(payload);
+ concatenated.extend_from_slice(user_headers);
+ assert_eq!(
+ XxHash3_64::oneshot(&concatenated),
+ streaming_reference(&header_tail, payload, user_headers),
+ "oneshot must match the streaming reference for payload {} B, user headers {} B",
+ payload.len(),
+ user_headers.len(),
+ );
+ }
+ }
+
+ #[test]
+ fn batch_checksum_pins_header_fields_then_message_checksum_fields() {
+ // Formula pin for the batch checksum: XxHash3-64 (default seed) streaming
+ // over the six batch header meta fields (LE, in field order) then each
+ // message's stored 8-byte checksum field in message order - never the
+ // bodies. This reference walks the blob by the KNOWN input message sizes,
+ // independent of the production frame decoder, and must equal what the
+ // encoder stamped, or a stamp will not verify against a read-back
+ // recompute.
+ let namespace = IggyNamespace::new(1, 1, 7);
+ let messages = sample_messages();
+ let mut owned =
+ SendMessagesOwned::from_messages(namespace, &messages).expect("build batch");
+ owned.header.base_offset = 100;
+ owned.header.base_timestamp = 200;
+ owned.header.batch_checksum = owned.header.checksum_for_blob(&owned.blob);
+
+ let mut hasher = XxHash3_64::new();
+ hasher.write(&owned.header.partition_id.to_le_bytes());
+ hasher.write(&owned.header.base_offset.to_le_bytes());
+ hasher.write(&owned.header.base_timestamp.to_le_bytes());
+ hasher.write(&owned.header.origin_timestamp.to_le_bytes());
+ hasher.write(&owned.header.batch_length.to_le_bytes());
+ hasher.write(&owned.header.message_count.to_le_bytes());
+ let mut frame_start = 0usize;
+ for message in messages.iter() {
+ hasher.write(&owned.blob[frame_start..frame_start + 8]);
+ let user_headers = message.user_headers.as_deref().unwrap_or_default();
+ frame_start += BATCH_MESSAGE_HEADER_SIZE + message.payload.len() + user_headers.len();
+ }
+ let reference = hasher.finish();
+
+ assert_eq!(
+ frame_start,
+ owned.blob.len(),
+ "reference walk must consume the whole blob",
+ );
+ assert_eq!(
+ owned.header.batch_checksum, reference,
+ "batch checksum must equal hash(6 header fields || per-message checksum fields)",
+ );
+ }
+
+ #[test]
+ fn decode_batch_slice_rejects_body_corruption_with_intact_checksum_field() {
+ // Equal-integrity: the batch value binds bodies only through the
+ // per-message checksum fields, so a flipped body byte that leaves the
+ // 8-byte checksum field intact keeps the batch value matching. The
+ // validating decode must still reject it via the per-message verify -
+ // the sole at-rest read-back check (the poll disk walk) decodes
+ // through here.
+ let namespace = IggyNamespace::new(1, 1, 7);
+ let owned =
+ SendMessagesOwned::from_messages(namespace, &sample_messages()).expect("build batch");
+ let mut body = vec![0u8; COMMAND_HEADER_SIZE + owned.blob.len()];
+ owned.header.encode_into(&mut body[..COMMAND_HEADER_SIZE]);
+ body[COMMAND_HEADER_SIZE..].copy_from_slice(&owned.blob);
+
+ decode_batch_slice(&body).expect("the clean batch decodes");
+
+ // First payload byte sits right after the command header and the first
+ // message's 48B frame header, leaving that frame's checksum field intact.
+ let payload_index = COMMAND_HEADER_SIZE + BATCH_MESSAGE_HEADER_SIZE;
+ body[payload_index] ^= 0xFF;
+ assert!(
+ matches!(
+ decode_batch_slice(&body),
+ Err(IggyError::InvalidMessageChecksum(..))
+ ),
+ "body corruption with an intact checksum field must fail the per-message verify",
+ );
+ }
+
+ #[test]
+ fn decode_prepare_slice_rejects_body_corruption_with_intact_checksum_field() {
+ // The same equal-integrity guarantee at the resident/repair validating
+ // decode, plus proof that the batch value alone is blind to it.
+ let mut owned = prepare_with_messages(&sample_messages());
+ decode_prepare_slice(owned.as_slice()).expect("the clean prepare decodes");
+
+ let payload_index = PREPARE_SPLIT_POINT + BATCH_MESSAGE_HEADER_SIZE;
+ owned.as_mut_slice()[payload_index] ^= 0xFF;
+ assert!(
+ matches!(
+ decode_prepare_slice(owned.as_slice()),
+ Err(IggyError::InvalidMessageChecksum(..))
+ ),
+ "body corruption with an intact checksum field must fail the per-message verify",
+ );
+ assert!(
+ decode_prepare_slice_trusted(owned.as_slice()).is_ok(),
+ "the intact checksum field leaves the batch value matching, so trusted still passes",
+ );
+ }
+
+ /// Wire-form `SendMessages` body built by the production client encoder:
+ /// `[metadata][256B batch header][blob]` with producer-computed checksums
+ /// and `partition_id = 0`.
+ fn wire_send_messages_body(messages: &IggyMessages) -> Vec<u8> {
+ let raw: Vec<RawMessage<'_>> = messages
+ .iter()
+ .map(|message| RawMessage {
+ id: message.header.id,
+ origin_timestamp: message.header.origin_timestamp,
+ headers: message.user_headers.as_deref(),
+ payload: &message.payload,
+ })
+ .collect();
+ let stream_id = WireIdentifier::numeric(1);
+ let topic_id = WireIdentifier::numeric(1);
+ let partitioning = WirePartitioning::Balanced;
+ let mut buf = BytesMut::with_capacity(SendMessagesEncoder::encoded_size(
+ &stream_id,
+ &topic_id,
+ &partitioning,
+ &raw,
+ ));
+ SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &raw)
+ .expect("wire body encodes");
+ buf.to_vec()
+ }
+
+ fn wire_request_message(body: &[u8]) -> Message<RoutedRequestHeader> {
+ let header_size = std::mem::size_of::<RoutedRequestHeader>();
+ let total = header_size + body.len();
+ let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total);
+ {
+ let header: &mut RoutedRequestHeader =
+ bytemuck::checked::try_from_bytes_mut(&mut buffer.as_mut_slice()[..header_size])
+ .expect("zeroed bytes form a valid RoutedRequestHeader");
+ header.command = Command::Request;
+ header.operation = Operation::SendMessages;
+ header.client = 1;
+ header.session = 1;
+ header.request = 1;
+ header.size = u32::try_from(total).expect("size fits u32");
+ }
+ buffer.as_mut_slice()[header_size..].copy_from_slice(body);
+ Message::try_from(buffer).expect("wire request message is valid")
+ }
+
+ #[test]
+ fn convert_request_message_rejects_empty_batches() {
+ let namespace = IggyNamespace::new(1, 1, 3);
+ let messages = IggyMessages::with_capacity(0);
+ let canonical = SendMessagesOwned::from_messages(namespace, &messages)
+ .expect("build empty canonical batch");
+ let mut canonical_body = vec![0; canonical.header.total_size()];
+ canonical
+ .header
+ .encode_into(&mut canonical_body[..COMMAND_HEADER_SIZE]);
+ // The client encoder refuses an empty batch, so assemble the wire form
+ // from the metadata primitives directly to prove admission rejects one
+ // independently.
+ let stream_id = WireIdentifier::numeric(1);
+ let topic_id = WireIdentifier::numeric(1);
+ let partitioning = WirePartitioning::Balanced;
+ let metadata_length =
+ stream_id.encoded_size() + topic_id.encoded_size() + partitioning.encoded_size() + 4;
+ let mut wire_buf = BytesMut::new();
+ wire_buf.put_u32_le(u32::try_from(metadata_length).expect("metadata fits u32"));
+ stream_id.encode(&mut wire_buf);
+ topic_id.encode(&mut wire_buf);
+ partitioning.encode(&mut wire_buf);
+ wire_buf.put_u32_le(0);
+ wire_buf.extend_from_slice(&canonical_body);
+ let wire_body = wire_buf.to_vec();
+
+ for mode in [ChecksumMode::Compute, ChecksumMode::Skip] {
+ let canonical_result =
+ convert_request_message(namespace, wire_request_message(&canonical_body), mode);
+ assert!(matches!(canonical_result, Err(IggyError::InvalidCommand)));
+
+ let wire_result =
+ convert_request_message(namespace, wire_request_message(&wire_body), mode);
+ assert!(matches!(wire_result, Err(IggyError::InvalidCommand)));
+ }
+ }
+
+ #[test]
+ fn convert_request_message_admits_wire_body_and_stamps_partition() {
+ // Golden: admitting the producer's wire body must yield the exact
+ // canonical batch the server-side builder (`from_messages`) produces
+ // for the same messages - command header + blob, byte for byte.
+ // Explicit non-zero ids keep it deterministic.
+ let namespace = IggyNamespace::new(1, 1, 3);
+ let messages = sample_messages();
+
+ let owned =
+ SendMessagesOwned::from_messages(namespace, &messages).expect("build canonical batch");
+ let mut expected_body = vec![0u8; COMMAND_HEADER_SIZE + owned.blob.len()];
+ owned
+ .header
+ .encode_into(&mut expected_body[..COMMAND_HEADER_SIZE]);
+ expected_body[COMMAND_HEADER_SIZE..].copy_from_slice(&owned.blob);
+
+ let wire = wire_request_message(&wire_send_messages_body(&messages));
+ let converted = convert_request_message(namespace, wire, ChecksumMode::Compute)
+ .expect("wire body admits");
+ let header_size = std::mem::size_of::<RoutedRequestHeader>();
+ let actual_body = &converted.as_slice()[header_size..converted.header().size as usize];
+
+ assert_eq!(
+ actual_body, expected_body,
+ "admitted wire body must be byte-identical to the server-built canonical batch",
+ );
+
+ // And the admitted batch is self-consistent: it validates through the
+ // batch-checksum decode and yields the original messages.
+ let decoded = decode_batch_slice(actual_body).expect("admitted batch checksum is valid");
+ assert_eq!(decoded.header.partition_id, 3);
+ assert_eq!(decoded.message_count(), messages.count());
+ let payloads: Vec<&[u8]> = decoded.iter().map(|view| view.payload).collect();
+ assert_eq!(
+ payloads,
+ vec![&b"first-payload"[..], &b"second-payload"[..]]
+ );
+ }
+
+ #[test]
+ fn convert_request_message_rejects_tampered_wire_body() {
+ // A flipped payload byte invalidates the producer's per-message
+ // checksum; admission must refuse the batch instead of stamping it.
+ let namespace = IggyNamespace::new(1, 1, 3);
+ let mut body = wire_send_messages_body(&sample_messages());
+ let last = body.len() - 1;
+ body[last] ^= 0xFF;
+ let result =
+ convert_request_message(namespace, wire_request_message(&body), ChecksumMode::Skip);
+ assert!(matches!(result, Err(IggyError::InvalidMessageChecksum(..))));
+ }
+
+ #[test]
+ fn convert_request_message_rejects_metadata_count_mismatch() {
+ let namespace = IggyNamespace::new(1, 1, 3);
+ let mut body = wire_send_messages_body(&sample_messages());
+ // The metadata count is the 4 bytes right before the batch header:
+ // metadata = [stream][topic][partitioning][count].
+ let metadata_length =
+ u32::from_le_bytes(body[..4].try_into().expect("4-byte slice")) as usize;
+ let count_offset = 4 + metadata_length - 4;
+ body[count_offset..count_offset + 4].copy_from_slice(&9u32.to_le_bytes());
+ let result =
+ convert_request_message(namespace, wire_request_message(&body), ChecksumMode::Skip);
+ assert!(matches!(result, Err(IggyError::InvalidCommand)));
+ }
+
+ #[test]
+ fn convert_request_message_skip_leaves_batch_checksum_zero_until_stamp() {
+ // The partition ingest path passes Skip: the admitted batch must carry
+ // a zero checksum (stamp fills it) and be otherwise byte-identical to
+ // the Compute output - the flag toggles nothing but that one hash.
+ let namespace = IggyNamespace::new(1, 1, 3);
+ let messages = sample_messages();
+ let body = wire_send_messages_body(&messages);
+ let header_size = std::mem::size_of::<RoutedRequestHeader>();
+
+ let computed = convert_request_message(
+ namespace,
+ wire_request_message(&body),
+ ChecksumMode::Compute,
+ )
+ .expect("compute admission");
+ let skipped =
+ convert_request_message(namespace, wire_request_message(&body), ChecksumMode::Skip)
+ .expect("skip admission");
+
+ let computed_body = &computed.as_slice()[header_size..computed.header().size as usize];
+ let skipped_body = &skipped.as_slice()[header_size..skipped.header().size as usize];
+
+ let skipped_header = BatchHeader::decode(&skipped_body[..COMMAND_HEADER_SIZE])
+ .expect("decode skipped header");
+ assert_eq!(
+ skipped_header.batch_checksum, 0,
+ "skip leaves the batch checksum zero until stamp",
+ );
+
+ // Patch only the 8-byte batch_checksum field into the skipped body; it
+ // must then equal the computed body, proving nothing else diverges.
+ let mut patched = skipped_body.to_vec();
+ patched[BATCH_CHECKSUM_OFFSET..BATCH_CHECKSUM_OFFSET + 8]
+ .copy_from_slice(&computed_body[BATCH_CHECKSUM_OFFSET..BATCH_CHECKSUM_OFFSET + 8]);
+ assert_eq!(
+ patched.as_slice(),
+ computed_body,
+ "skip and compute differ only in the batch_checksum field",
+ );
+ }
+
+ #[test]
+ fn encrypt_ingest_path_stays_canonical_through_flag_split() {
+ // Mirror the plane encrypt ingest sequence: convert(Compute) -> the
+ // validating decode encrypt performs on its input -> encrypt -> the
+ // validating decode the second convert performs as its discriminator ->
+ // convert(Skip) (the partition convert), which sees an already-canonical
+ // batch and returns it unchanged. Every decode must succeed.
+ let namespace = IggyNamespace::new(1, 1, 3);
+ let messages = sample_messages();
+ let header_size = std::mem::size_of::<RoutedRequestHeader>();
+
+ let wire = wire_request_message(&wire_send_messages_body(&messages));
+ let canonical = convert_request_message(namespace, wire, ChecksumMode::Compute)
+ .expect("pre-encrypt admission");
+ let canonical_body = &canonical.as_slice()[header_size..canonical.header().size as usize];
+ decode_batch_slice(canonical_body).expect("encrypt input decode validates the checksum");
+
+ let encryptor =
+ EncryptorKind::Aes256Gcm(Aes256GcmEncryptor::new(&[7u8; 32]).expect("valid 32B key"));
+ let encrypted = encrypt_batch_request(canonical, &encryptor).expect("encrypt batch");
+ let encrypted_body: Vec<u8> =
+ encrypted.as_slice()[header_size..encrypted.header().size as usize].to_vec();
+ decode_batch_slice(&encrypted_body)
+ .expect("encrypt output drives the 2nd-convert discriminator");
+
+ let repassed = convert_request_message(namespace, encrypted, ChecksumMode::Skip)
+ .expect("second convert passes the canonical batch");
+ let repassed_body = &repassed.as_slice()[header_size..repassed.header().size as usize];
+ assert_eq!(
+ repassed_body,
+ encrypted_body.as_slice(),
+ "an already-canonical encrypted batch passes the partition convert untouched",
+ );
+ }
+
+ /// Junk suffixes that must be refused at both ingest boundaries: one below a
+ /// frame header (the frame walk stops on a short read) and one frame-sized
+ /// but undecodable (`reserved != 0`). Neither is covered by any checksum, so
+ /// a walk that stops at the last decodable frame cannot see them.
+ const TRAILING_JUNK_CASES: [&[u8]; 2] = [&[0xAA], &[0xFF; 64]];
+
+ /// Canonical `SendMessages` request carrying `junk` past `batch_length`, with
+ /// `RoutedRequestHeader.size` inflated to cover it. `size` and `batch_length` are
+ /// independent wire fields, so a non-conforming client can emit this.
+ fn canonical_request_with_trailing_bytes(junk: &[u8]) -> Message<RoutedRequestHeader> {
+ let namespace = IggyNamespace::new(1, 1, 3);
+ let owned =
+ SendMessagesOwned::from_messages(namespace, &sample_messages()).expect("build batch");
+ let header_size = std::mem::size_of::<RoutedRequestHeader>();
+ let total = header_size + owned.header.total_size() + junk.len();
+ let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total);
+ {
+ let header: &mut RoutedRequestHeader =
+ bytemuck::checked::try_from_bytes_mut(&mut buffer.as_mut_slice()[..header_size])
+ .expect("zeroed bytes form a valid RoutedRequestHeader");
+ header.command = Command::Request;
+ header.operation = Operation::SendMessages;
+ header.client = 1;
+ header.session = 1;
+ header.request = 1;
+ header.size = u32::try_from(total).expect("size fits u32");
+ }
+ let bytes = buffer.as_mut_slice();
+ owned
+ .header
+ .encode_into(&mut bytes[header_size..header_size + COMMAND_HEADER_SIZE]);
+ let blob_end = PREPARE_SPLIT_POINT + owned.blob.len();
+ bytes[PREPARE_SPLIT_POINT..blob_end].copy_from_slice(&owned.blob);
+ bytes[blob_end..].copy_from_slice(junk);
+ Message::try_from(buffer).expect("request message is valid")
+ }
+
+ /// A `Prepare` whose `size` covers `junk` past `batch_length`.
+ fn prepare_with_trailing_bytes(junk: &[u8]) -> Owned<MESSAGE_ALIGN> {
+ let namespace = IggyNamespace::new(1, 1, 7);
+ let owned =
+ SendMessagesOwned::from_messages(namespace, &sample_messages()).expect("build batch");
+ let header_size = std::mem::size_of::<PrepareHeader>();
+ let total = header_size + owned.header.total_size() + junk.len();
+ let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total);
+ {
+ let prepare: &mut PrepareHeader =
+ bytemuck::checked::try_from_bytes_mut(&mut buffer.as_mut_slice()[..header_size])
+ .expect("zeroed bytes form a valid PrepareHeader");
+ prepare.command = Command::Prepare;
+ prepare.size = u32::try_from(total).expect("prepare size fits u32");
+ }
+ let bytes = buffer.as_mut_slice();
+ owned
+ .header
+ .encode_into(&mut bytes[header_size..header_size + COMMAND_HEADER_SIZE]);
+ let blob_end = PREPARE_SPLIT_POINT + owned.blob.len();
+ bytes[PREPARE_SPLIT_POINT..blob_end].copy_from_slice(&owned.blob);
+ bytes[blob_end..].copy_from_slice(junk);
+ buffer
+ }
+
+ #[test]
+ fn convert_request_message_rejects_canonical_batch_with_trailing_bytes() {
+ // Client ingest boundary. Accepting the request would carry the suffix
+ // into the journal and onto disk: the flush writes the whole frame while
+ // every reader advances by `batch_length`, so the segment walk lands
+ // inside the junk and every later batch becomes unreadable.
+ let namespace = IggyNamespace::new(1, 1, 3);
+ for junk in TRAILING_JUNK_CASES {
+ for mode in [ChecksumMode::Compute, ChecksumMode::Skip] {
+ let message = canonical_request_with_trailing_bytes(junk);
+ let result = convert_request_message(namespace, message, mode);
+ assert!(
+ matches!(result, Err(IggyError::InvalidCommand)),
+ "{} trailing bytes ({mode:?}) must be rejected, got {result:?}",
+ junk.len(),
+ );
+ }
+ }
+ }
+
+ #[test]
+ fn convert_request_message_accepts_exact_canonical_batch() {
+ // The same builder with no suffix must still pass untouched, so the
+ // rejection above is the suffix and not the fixture.
+ let namespace = IggyNamespace::new(1, 1, 3);
+ let message = canonical_request_with_trailing_bytes(&[]);
+ let expected = message.as_slice().to_vec();
+ let converted = convert_request_message(namespace, message, ChecksumMode::Skip)
+ .expect("an exact canonical batch passes untouched");
+ assert_eq!(converted.as_slice(), expected.as_slice());
+ }
+
+ #[test]
+ fn decode_prepare_slice_rejects_trailing_bytes_past_batch_length() {
+ // Replica ingest must reject bytes beyond `batch_length` because no
+ // per-message checksum covers them.
+ for junk in TRAILING_JUNK_CASES {
+ let owned = prepare_with_trailing_bytes(junk);
+ assert!(
+ matches!(
+ decode_prepare_slice(owned.as_slice()),
+ Err(IggyError::InvalidCommand)
+ ),
+ "{} trailing bytes must fail the validating decode",
+ junk.len(),
+ );
+ }
+ }
+}
diff --git a/core/server_common/src/send_messages2.rs b/core/server_common/src/send_messages2.rs
deleted file mode 100644
index 4dacba2..0000000
--- a/core/server_common/src/send_messages2.rs
+++ /dev/null
@@ -1,1643 +0,0 @@
-// 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.
-
-use crate::consensus_message::{MESSAGE_ALIGN, Message};
-use crate::iobuf::Owned;
-use crate::sharding::IggyNamespace;
-use bytes::{Bytes, BytesMut};
-use iggy_binary_protocol::{PrepareHeader, RoutedRequestHeader};
-use iggy_common::{EncryptorKind, INDEX_SIZE, IggyError, random_id};
-use std::hash::Hasher;
-use twox_hash::XxHash3_64;
-
-pub const COMMAND_HEADER_SIZE: usize = 256;
-pub const PREPARE_SPLIT_POINT: usize = 512;
-const MESSAGE_HEADER_SIZE: usize = 48;
-const LEGACY_MESSAGE_HEADER_SIZE: usize = 64;
-const BATCH_CHECKSUM_OFFSET: usize = 40;
-const MESSAGE_COUNT_OFFSET: usize = 48;
-const MAX_TIMESTAMP_DELTA_MICROS: u64 = u32::MAX as u64;
-
-#[derive(Debug, Clone, Copy, Default)]
-pub struct SendMessages2Header {
- pub partition_id: u64,
- pub base_offset: u64,
- pub base_timestamp: u64,
- pub origin_timestamp: u64,
- pub batch_length: u64,
- pub batch_checksum: u64,
- pub message_count: u32,
-}
-
-impl SendMessages2Header {
- pub const fn new(
- partition_id: u64,
- origin_timestamp: u64,
- batch_length: u64,
- message_count: u32,
- ) -> Self {
- Self {
- partition_id,
- base_offset: 0,
- base_timestamp: 0,
- origin_timestamp,
- batch_length,
- batch_checksum: 0,
- message_count,
- }
- }
-
- pub fn decode(bytes: &[u8]) -> Result<Self, IggyError> {
- if bytes.len() < COMMAND_HEADER_SIZE {
- return Err(IggyError::InvalidCommand);
- }
-
- let batch_length = read_u64(bytes, 32)?;
- if batch_length < COMMAND_HEADER_SIZE as u64 {
- return Err(IggyError::InvalidCommand);
- }
-
- Ok(Self {
- partition_id: read_u64(bytes, 0)?,
- base_offset: read_u64(bytes, 8)?,
- base_timestamp: read_u64(bytes, 16)?,
- origin_timestamp: read_u64(bytes, 24)?,
- batch_length,
- batch_checksum: read_u64(bytes, BATCH_CHECKSUM_OFFSET)?,
- message_count: read_u32(bytes, MESSAGE_COUNT_OFFSET)?,
- })
- }
-
- pub fn encode_into(&self, bytes: &mut [u8]) {
- assert!(bytes.len() >= COMMAND_HEADER_SIZE);
- bytes[..COMMAND_HEADER_SIZE].fill(0);
- bytes[0..8].copy_from_slice(&self.partition_id.to_le_bytes());
- bytes[8..16].copy_from_slice(&self.base_offset.to_le_bytes());
- bytes[16..24].copy_from_slice(&self.base_timestamp.to_le_bytes());
- bytes[24..32].copy_from_slice(&self.origin_timestamp.to_le_bytes());
- bytes[32..40].copy_from_slice(&self.batch_length.to_le_bytes());
- bytes[BATCH_CHECKSUM_OFFSET..BATCH_CHECKSUM_OFFSET + 8]
- .copy_from_slice(&self.batch_checksum.to_le_bytes());
- bytes[MESSAGE_COUNT_OFFSET..MESSAGE_COUNT_OFFSET + 4]
- .copy_from_slice(&self.message_count.to_le_bytes());
- }
-
- pub fn total_size(&self) -> usize {
- usize::try_from(self.batch_length).expect("batch length exceeds usize::MAX")
- }
-
- pub fn blob_len(&self) -> Result<usize, IggyError> {
- usize::try_from(
- self.batch_length
- .checked_sub(COMMAND_HEADER_SIZE as u64)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidCommand)
- }
-
- pub fn into_frozen(self) -> FrozenBatchHeader {
- let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(COMMAND_HEADER_SIZE);
- self.encode_into(buffer.as_mut_slice());
- buffer.into()
- }
-
- #[must_use]
- pub fn checksum_for_blob(&self, blob: &[u8]) -> u64 {
- calculate_batch_checksum(self, blob)
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct SendMessages2Owned {
- pub header: SendMessages2Header,
- pub blob: Bytes,
-}
-
-impl SendMessages2Owned {
- pub fn from_messages(
- namespace: IggyNamespace,
- messages: &IggyMessages2,
- ) -> Result<Self, IggyError> {
- let message_count = messages.count();
- let mut origin_timestamp = u64::MAX;
- for message in messages {
- origin_timestamp = origin_timestamp.min(message.header.origin_timestamp);
- }
-
- if origin_timestamp == u64::MAX {
- origin_timestamp = 0;
- }
-
- let mut blob = BytesMut::new();
- for (index, message) in messages.iter().enumerate() {
- let id = if message.header.id == 0 {
- random_id::get_uuid()
- } else {
- message.header.id
- };
- let offset_delta = u32::try_from(index).map_err(|_| IggyError::InvalidCommand)?;
- let timestamp_delta = message
- .header
- .origin_timestamp
- .checked_sub(origin_timestamp)
- .ok_or(IggyError::InvalidCommand)?;
- if timestamp_delta > MAX_TIMESTAMP_DELTA_MICROS {
- return Err(IggyError::InvalidMessageTimestampDelta(timestamp_delta));
- }
- let timestamp_delta =
- u32::try_from(timestamp_delta).map_err(|_| IggyError::InvalidCommand)?;
- let user_headers = message.user_headers.as_deref().unwrap_or_default();
- let user_headers_length =
- u32::try_from(user_headers.len()).map_err(|_| IggyError::InvalidCommand)?;
- let payload_length =
- u32::try_from(message.payload.len()).map_err(|_| IggyError::InvalidCommand)?;
-
- let mut header = [0u8; MESSAGE_HEADER_SIZE];
- header[8..24].copy_from_slice(&id.to_le_bytes());
- header[24..28].copy_from_slice(&offset_delta.to_le_bytes());
- header[28..32].copy_from_slice(×tamp_delta.to_le_bytes());
- header[32..36].copy_from_slice(&user_headers_length.to_le_bytes());
- header[36..40].copy_from_slice(&payload_length.to_le_bytes());
-
- let msg_start = blob.len();
- blob.extend_from_slice(&header);
- blob.extend_from_slice(&message.payload);
- blob.extend_from_slice(user_headers);
- let checksum = XxHash3_64::oneshot(&blob[msg_start + 8..]);
- blob[msg_start..msg_start + 8].copy_from_slice(&checksum.to_le_bytes());
- }
-
- let blob = blob.freeze();
- let mut header = SendMessages2Header::new(
- namespace.partition_id() as u64,
- origin_timestamp,
- u64::try_from(COMMAND_HEADER_SIZE + blob.len())
- .map_err(|_| IggyError::InvalidCommand)?,
- message_count,
- );
- header.batch_checksum = calculate_batch_checksum(&header, &blob);
-
- Ok(Self { header, blob })
- }
-
- pub fn encode_request(
- self,
- mut request_header: RoutedRequestHeader,
- ) -> Result<Message<RoutedRequestHeader>, IggyError> {
- let total_size = std::mem::size_of::<RoutedRequestHeader>() + self.header.total_size();
- // The converted body differs in size from the legacy wire body the
- // header described; a stale `size` truncates the rebuilt blob for
- // every downstream slice (stamping, journal reads).
- request_header.size = u32::try_from(total_size).map_err(|_| IggyError::InvalidCommand)?;
- let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total_size);
- let bytes = buffer.as_mut_slice();
- bytes[0..std::mem::size_of::<RoutedRequestHeader>()]
- .copy_from_slice(bytemuck::bytes_of(&request_header));
- self.header.encode_into(
- &mut bytes[std::mem::size_of::<RoutedRequestHeader>()
- ..std::mem::size_of::<RoutedRequestHeader>() + COMMAND_HEADER_SIZE],
- );
- bytes[PREPARE_SPLIT_POINT..PREPARE_SPLIT_POINT + self.blob.len()]
- .copy_from_slice(&self.blob);
-
- Message::try_from(buffer).map_err(|_| IggyError::InvalidCommand)
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub struct IggyMessage2Header {
- pub checksum: u64,
- pub id: u128,
- pub offset: u64,
- pub timestamp: u64,
- pub origin_timestamp: u64,
- pub user_headers_length: u32,
- pub payload_length: u32,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct IggyMessage2 {
- pub header: IggyMessage2Header,
- pub payload: Bytes,
- pub user_headers: Option<Bytes>,
-}
-
-#[derive(Debug, Clone, Default, PartialEq, Eq)]
-pub struct IggyMessages2 {
- messages: Vec<IggyMessage2>,
-}
-
-impl IggyMessages2 {
- #[must_use]
- pub fn empty() -> Self {
- Self::default()
- }
-
- #[must_use]
- pub fn with_capacity(capacity: usize) -> Self {
- Self {
- messages: Vec::with_capacity(capacity),
- }
- }
-
- pub fn push(&mut self, message: IggyMessage2) {
- self.messages.push(message);
- }
-
- #[must_use]
- pub fn count(&self) -> u32 {
- u32::try_from(self.messages.len()).unwrap_or(u32::MAX)
- }
-
- #[must_use]
- pub const fn is_empty(&self) -> bool {
- self.messages.is_empty()
- }
-
- #[must_use]
- pub fn first_offset(&self) -> Option<u64> {
- self.messages.first().map(|message| message.header.offset)
- }
-
- #[must_use]
- pub fn last_offset(&self) -> Option<u64> {
- self.messages.last().map(|message| message.header.offset)
- }
-
- #[must_use]
- pub fn limit(self, count: u32) -> Self {
- let mut messages = self.messages;
- messages.truncate(usize::try_from(count).unwrap_or(usize::MAX));
- Self { messages }
- }
-
- pub fn iter(&self) -> std::slice::Iter<'_, IggyMessage2> {
- self.messages.iter()
- }
-}
-
-impl IntoIterator for IggyMessages2 {
- type Item = IggyMessage2;
- type IntoIter = std::vec::IntoIter<IggyMessage2>;
-
- fn into_iter(self) -> Self::IntoIter {
- self.messages.into_iter()
- }
-}
-
-impl<'a> IntoIterator for &'a IggyMessages2 {
- type Item = &'a IggyMessage2;
- type IntoIter = std::slice::Iter<'a, IggyMessage2>;
-
- fn into_iter(self) -> Self::IntoIter {
- self.messages.iter()
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct SendMessages2Ref<'a> {
- pub header: SendMessages2Header,
- blob: &'a [u8],
-}
-
-#[allow(dead_code)]
-impl<'a> SendMessages2Ref<'a> {
- pub const fn iter(&self) -> SendMessages2Iterator<'a> {
- SendMessages2Iterator {
- blob: self.blob,
- position: 0,
- }
- }
-
- pub const fn iter_with_offsets(&self) -> SendMessages2IteratorWithOffsets<'a> {
- SendMessages2IteratorWithOffsets {
- blob: self.blob,
- position: 0,
- }
- }
-
- pub const fn blob(&self) -> &'a [u8] {
- self.blob
- }
-
- pub const fn message_count(&self) -> u32 {
- self.header.message_count
- }
-}
-
-#[allow(dead_code)]
-#[derive(Debug, Clone, Copy)]
-pub struct SendMessages2MessageHeader {
- pub checksum: u64,
- pub id: u128,
- pub offset_delta: u32,
- /// Microsecond delta from `SendMessages2Header::origin_timestamp`.
- ///
- /// This is stored in `u32`, which limits a single batch to roughly
- /// 71.6 minutes of origin timestamp span.
- pub timestamp_delta: u32,
- pub user_headers_length: u32,
- pub payload_length: u32,
-}
-
-impl SendMessages2MessageHeader {
- fn decode(bytes: &[u8]) -> Result<Self, IggyError> {
- if bytes.len() < MESSAGE_HEADER_SIZE {
- return Err(IggyError::InvalidCommand);
- }
-
- let reserved = read_u64(bytes, 40)?;
- if reserved != 0 {
- return Err(IggyError::InvalidCommand);
- }
-
- Ok(Self {
- checksum: read_u64(bytes, 0)?,
- id: read_u128(bytes, 8)?,
- offset_delta: read_u32(bytes, 24)?,
- timestamp_delta: read_u32(bytes, 28)?,
- user_headers_length: read_u32(bytes, 32)?,
- payload_length: read_u32(bytes, 36)?,
- })
- }
-
- const fn total_size(&self) -> usize {
- MESSAGE_HEADER_SIZE + self.user_headers_length as usize + self.payload_length as usize
- }
-}
-
-#[allow(dead_code)]
-#[derive(Debug, Clone, Copy)]
-pub struct SendMessages2MessageView<'a> {
- pub header: SendMessages2MessageHeader,
- pub user_headers: &'a [u8],
- pub payload: &'a [u8],
-}
-
-#[allow(dead_code)]
-pub struct SendMessages2Iterator<'a> {
- blob: &'a [u8],
- position: usize,
-}
-
-impl<'a> Iterator for SendMessages2Iterator<'a> {
- type Item = SendMessages2MessageView<'a>;
-
- fn next(&mut self) -> Option<Self::Item> {
- if self.position >= self.blob.len() {
- return None;
- }
-
- let header = SendMessages2MessageHeader::decode(&self.blob[self.position..]).ok()?;
- let start = self.position + MESSAGE_HEADER_SIZE;
- let payload_end = start + header.payload_length as usize;
- let headers_end = payload_end + header.user_headers_length as usize;
- let payload = self.blob.get(start..payload_end)?;
- let user_headers = self.blob.get(payload_end..headers_end)?;
- self.position += header.total_size();
- Some(SendMessages2MessageView {
- header,
- user_headers,
- payload,
- })
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct SendMessages2MessageViewWithOffsets<'a> {
- pub message: SendMessages2MessageView<'a>,
- pub start: usize,
- pub end: usize,
-}
-
-pub struct SendMessages2IteratorWithOffsets<'a> {
- blob: &'a [u8],
- position: usize,
-}
-
-impl<'a> Iterator for SendMessages2IteratorWithOffsets<'a> {
- type Item = SendMessages2MessageViewWithOffsets<'a>;
-
- fn next(&mut self) -> Option<Self::Item> {
- if self.position >= self.blob.len() {
- return None;
- }
-
- let start = self.position;
- let header = SendMessages2MessageHeader::decode(&self.blob[self.position..]).ok()?;
- let message_start = self.position + MESSAGE_HEADER_SIZE;
- let payload_end = message_start + header.payload_length as usize;
- let headers_end = payload_end + header.user_headers_length as usize;
- let payload = self.blob.get(message_start..payload_end)?;
- let user_headers = self.blob.get(payload_end..headers_end)?;
- self.position += header.total_size();
- Some(SendMessages2MessageViewWithOffsets {
- message: SendMessages2MessageView {
- header,
- user_headers,
- payload,
- },
- start,
- end: self.position,
- })
- }
-}
-
-pub(crate) type FrozenBatchHeader = crate::iobuf::Frozen<MESSAGE_ALIGN>;
-
-/// Re-encode a canonical `SendMessages2` request with every message's payload
-/// and user headers encrypted, per-message checksums and lengths recomputed,
-/// and the batch header (length + checksum) restamped.
-///
-/// Runs ONCE, on the primary at ingestion (after [`convert_request_message`]
-/// canonicalized the wire form), so the ciphertext is what replicates: every
-/// replica journals and persists identical bytes, and the poll path decrypts
-/// uniformly regardless of which replica or tier served the fragment.
-///
-/// # Errors
-///
-/// [`IggyError::InvalidCommand`] on an undecodable batch; encryption errors
-/// propagate from the encryptor.
-pub fn encrypt_batch_request(
- message: Message<RoutedRequestHeader>,
- encryptor: &EncryptorKind,
-) -> Result<Message<RoutedRequestHeader>, IggyError> {
- let request_header = *message.header();
- let total_size = request_header.size as usize;
- let body = &message.as_slice()[std::mem::size_of::<RoutedRequestHeader>()..total_size];
- let batch = decode_batch_slice(body)?;
-
- let mut blob = BytesMut::with_capacity(batch.blob().len() * 2);
- for view in batch.iter() {
- let encrypted_payload = encryptor.encrypt(view.payload)?;
- let encrypted_user_headers = if view.user_headers.is_empty() {
- None
- } else {
- Some(encryptor.encrypt(view.user_headers)?)
- };
- let user_headers: &[u8] = encrypted_user_headers.as_deref().unwrap_or_default();
- let payload_length =
- u32::try_from(encrypted_payload.len()).map_err(|_| IggyError::InvalidCommand)?;
- let user_headers_length =
- u32::try_from(user_headers.len()).map_err(|_| IggyError::InvalidCommand)?;
-
- let mut header = [0u8; MESSAGE_HEADER_SIZE];
- header[8..24].copy_from_slice(&view.header.id.to_le_bytes());
- header[24..28].copy_from_slice(&view.header.offset_delta.to_le_bytes());
- header[28..32].copy_from_slice(&view.header.timestamp_delta.to_le_bytes());
- header[32..36].copy_from_slice(&user_headers_length.to_le_bytes());
- header[36..40].copy_from_slice(&payload_length.to_le_bytes());
- let msg_start = blob.len();
- blob.extend_from_slice(&header);
- blob.extend_from_slice(&encrypted_payload);
- blob.extend_from_slice(user_headers);
- let checksum = XxHash3_64::oneshot(&blob[msg_start + 8..]);
- blob[msg_start..msg_start + 8].copy_from_slice(&checksum.to_le_bytes());
- }
-
- let blob = blob.freeze();
- let mut header = batch.header;
- header.batch_length =
- u64::try_from(COMMAND_HEADER_SIZE + blob.len()).map_err(|_| IggyError::InvalidCommand)?;
- header.batch_checksum = calculate_batch_checksum(&header, &blob);
-
- SendMessages2Owned { header, blob }.encode_request(request_header)
-}
-
-/// Whether the legacy transcode stamps a batch checksum onto its output.
-///
-/// The recompute is an `XxHash3` batch-checksum pass, needed only when a reader
-/// validates the transcoded batch before [`stamp_prepare_for_persistence`]
-/// recomputes it: the encrypt ingest path re-decodes the canonicalized batch
-/// (`encrypt_batch_request`'s validating decode, then the second `convert` its
-/// output re-enters as the canonical-vs-legacy discriminator). The partition
-/// ingest path has no such reader, so it skips the pass and the checksum stays
-/// zero until stamp.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ChecksumMode {
- /// Compute the batch checksum for the transcoded batch.
- Compute,
- /// Leave the batch checksum zero; `stamp_prepare_for_persistence` fills it.
- Skip,
-}
-
-pub fn convert_request_message(
- namespace: IggyNamespace,
- message: Message<RoutedRequestHeader>,
- checksum: ChecksumMode,
-) -> Result<Message<RoutedRequestHeader>, IggyError> {
- let request_header = *message.header();
- let total_size = request_header.size as usize;
- let body = &message.as_slice()[std::mem::size_of::<RoutedRequestHeader>()..total_size];
- // A canonical body enters the pipeline verbatim, so it must end exactly at
- // `batch_length`: `size` and `batch_length` are independent client-supplied
- // fields and `decode_batch_slice` only lower-bounds the frame. A suffix past
- // `batch_length` is covered by no checksum, still rides the buffer to disk,
- // and desyncs the segment walk that advances by `batch_length`.
- match decode_batch_slice(body) {
- Ok(batch) if batch.message_count() == 0 => Err(IggyError::InvalidCommand),
- Ok(batch) if body.len() == batch.header.total_size() => Ok(message),
- Ok(_) => Err(IggyError::InvalidCommand),
- Err(_) => transcode_legacy_request(namespace, body, request_header, checksum),
- }
-}
-
-/// Transcode a legacy `SendMessages` request body directly into the canonical
-/// `[RoutedRequestHeader][256B SendMessages2Header][blob]` form, writing each message
-/// record straight into the final aligned buffer.
-///
-/// Fused replacement for the `from_legacy_request(..).encode_request(..)`
-/// two-step: a size walk over the legacy input sizes the single output
-/// allocation, then a write walk lays down each canonical record in place. This
-/// drops the intermediate blob allocation and the full-blob copy the two-step
-/// paid. Output bytes are identical to that path.
-///
-/// `checksum` selects whether the output carries a batch checksum (see
-/// [`ChecksumMode`]); [`ChecksumMode::Skip`] leaves it zero for the partition
-/// ingest path, where stamp recomputes it.
-fn transcode_legacy_request(
- namespace: IggyNamespace,
- body: &[u8],
- mut request_header: RoutedRequestHeader,
- checksum: ChecksumMode,
-) -> Result<Message<RoutedRequestHeader>, IggyError> {
- let (message_count, messages) = legacy_messages_slice(body)?;
- if message_count == 0 {
- return Err(IggyError::InvalidCommand);
- }
- let mut parsed = Vec::with_capacity(message_count as usize);
- let mut origin_timestamp = u64::MAX;
- let mut cursor = 0usize;
- let mut blob_len = 0usize;
-
- while cursor < messages.len() && parsed.len() < message_count as usize {
- let legacy = LegacyMessageRef::decode(&messages[cursor..])?;
- origin_timestamp = origin_timestamp.min(legacy.origin_timestamp);
- cursor += legacy.total_size;
- blob_len = blob_len
- .checked_add(MESSAGE_HEADER_SIZE + legacy.payload.len() + legacy.user_headers.len())
- .ok_or(IggyError::InvalidCommand)?;
- parsed.push(legacy);
- }
-
- if parsed.len() != message_count as usize || cursor != messages.len() {
- return Err(IggyError::InvalidCommand);
- }
-
- if origin_timestamp == u64::MAX {
- origin_timestamp = 0;
- }
-
- let header_size = std::mem::size_of::<RoutedRequestHeader>();
- let batch_length = COMMAND_HEADER_SIZE
- .checked_add(blob_len)
- .ok_or(IggyError::InvalidCommand)?;
- let total_size = header_size
- .checked_add(batch_length)
- .ok_or(IggyError::InvalidCommand)?;
- request_header.size = u32::try_from(total_size).map_err(|_| IggyError::InvalidCommand)?;
-
- let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total_size);
- let bytes = buffer.as_mut_slice();
- bytes[0..header_size].copy_from_slice(bytemuck::bytes_of(&request_header));
-
- let mut write = PREPARE_SPLIT_POINT;
- for (index, legacy) in parsed.iter().enumerate() {
- let id = if legacy.id == 0 {
- random_id::get_uuid()
- } else {
- legacy.id
- };
- let offset_delta = u32::try_from(index).map_err(|_| IggyError::InvalidCommand)?;
- let timestamp_delta = legacy
- .origin_timestamp
- .checked_sub(origin_timestamp)
- .ok_or(IggyError::InvalidCommand)?;
- if timestamp_delta > MAX_TIMESTAMP_DELTA_MICROS {
- return Err(IggyError::InvalidMessageTimestampDelta(timestamp_delta));
- }
- let timestamp_delta =
- u32::try_from(timestamp_delta).map_err(|_| IggyError::InvalidCommand)?;
- let user_headers_length =
- u32::try_from(legacy.user_headers.len()).map_err(|_| IggyError::InvalidCommand)?;
- let payload_length =
- u32::try_from(legacy.payload.len()).map_err(|_| IggyError::InvalidCommand)?;
-
- let mut header = [0u8; MESSAGE_HEADER_SIZE];
- header[8..24].copy_from_slice(&id.to_le_bytes());
- header[24..28].copy_from_slice(&offset_delta.to_le_bytes());
- header[28..32].copy_from_slice(×tamp_delta.to_le_bytes());
- header[32..36].copy_from_slice(&user_headers_length.to_le_bytes());
- header[36..40].copy_from_slice(&payload_length.to_le_bytes());
- let msg_start = write;
- bytes[write..write + MESSAGE_HEADER_SIZE].copy_from_slice(&header);
- write += MESSAGE_HEADER_SIZE;
- bytes[write..write + legacy.payload.len()].copy_from_slice(legacy.payload);
- write += legacy.payload.len();
- bytes[write..write + legacy.user_headers.len()].copy_from_slice(legacy.user_headers);
- write += legacy.user_headers.len();
- // The cover is [msg_start + 8 .. write], including the 8 reserved zero
- // header bytes. This relies on the stack header being zero-initialized,
- // not on the output buffer being pre-zeroed.
- let checksum = XxHash3_64::oneshot(&bytes[msg_start + 8..write]);
- bytes[msg_start..msg_start + 8].copy_from_slice(&checksum.to_le_bytes());
- }
-
- let mut command = SendMessages2Header::new(
- namespace.partition_id() as u64,
- origin_timestamp,
- batch_length as u64,
- message_count,
- );
- if checksum == ChecksumMode::Compute {
- command.batch_checksum = calculate_batch_checksum(
- &command,
- &bytes[PREPARE_SPLIT_POINT..PREPARE_SPLIT_POINT + blob_len],
- );
- }
- command.encode_into(&mut bytes[header_size..header_size + COMMAND_HEADER_SIZE]);
-
- Message::try_from(buffer).map_err(|_| IggyError::InvalidCommand)
-}
-
-/// Decode one batch slice (`[256B command header][blob]`), validating the batch
-/// checksum and every per-message checksum. The persisted segment-file record
-/// and the request slice share this layout, so both decode through here.
-///
-/// `body` may extend past the batch: the poll disk walk hands in the rest of the
-/// chunk and steps by `batch_length`. Callers whose buffer is meant to BE the
-/// batch must reject the surplus themselves - see [`convert_request_message`].
-pub fn decode_batch_slice(body: &[u8]) -> Result<SendMessages2Ref<'_>, IggyError> {
- decode_batch_slice_with(body, BatchIntegrity::Verify)
-}
-
-/// How much of a batch record [`decode_batch_slice_with`] proves before returning it.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum BatchIntegrity {
- /// Re-hash the batch and reject it unless it matches its own `batch_checksum`.
- Verify,
- /// Check the framing only, and hand back whatever it describes. The caller is
- /// accepting bytes that may not be the ones written.
- LayoutOnly,
-}
-
-/// [`decode_batch_slice`] with the integrity level chosen by the caller.
-///
-/// An enum, not a bool: the one caller that passes anything but
-/// [`BatchIntegrity::Verify`] is the disk poll under its operator knob, and
-/// `..., false)` there reads like a detail rather than opting a read out of
-/// corruption detection.
-///
-/// Layout checks are not optional either way: a short or self-inconsistent record is
-/// rejected regardless, because the caller would otherwise index past it.
-///
-/// # Errors
-/// [`IggyError::InvalidCommand`] for a short or inconsistent record, and
-/// [`IggyError::InvalidBatchChecksum`] under [`BatchIntegrity::Verify`] when the batch
-/// does not match. Callers that must tell corruption from a partial tail need both.
-pub fn decode_batch_slice_with(
- body: &[u8],
- integrity: BatchIntegrity,
-) -> Result<SendMessages2Ref<'_>, IggyError> {
- if body.len() < COMMAND_HEADER_SIZE {
- return Err(IggyError::InvalidCommand);
- }
-
- let header = SendMessages2Header::decode(&body[..COMMAND_HEADER_SIZE])?;
- let blob_len = header.blob_len()?;
- if body.len() < header.total_size() {
- return Err(IggyError::InvalidCommand);
- }
-
- let blob = &body[COMMAND_HEADER_SIZE..COMMAND_HEADER_SIZE + blob_len];
- let batch = SendMessages2Ref { header, blob };
- match integrity {
- BatchIntegrity::Verify => {
- let expected_checksum = verify_and_recompute_batch_checksum(&batch)?;
- if header.batch_checksum != expected_checksum {
- return Err(IggyError::InvalidBatchChecksum(
- header.batch_checksum,
- expected_checksum,
- header.base_offset,
- ));
- }
- }
- BatchIntegrity::LayoutOnly => validate_batch_layout(&batch)?,
- }
-
- Ok(batch)
-}
-
-/// Decode a `Prepare` message from a slice of bytes, validating the batch
-/// checksum and every per-message checksum.
-///
-/// `bytes` must be 16-byte aligned (`PrepareHeader` has `u128` fields). Source
-/// from `Frozen<MESSAGE_ALIGN>` / `Owned<MESSAGE_ALIGN>` / `Message<H>`.
-/// Misalignment: `debug_assert!` in debug; `InvalidCommand` in release.
-///
-/// # Errors
-///
-/// `IggyError::InvalidCommand` on a short buffer, bad bit pattern, `size`
-/// outside `[header_size, bytes.len()]`, a `size` that does not describe the
-/// batch exactly, or frames that do not tile the batch;
-/// `InvalidBatchChecksum` / `InvalidMessageChecksum` on an integrity mismatch.
-pub fn decode_prepare_slice(bytes: &[u8]) -> Result<SendMessages2Ref<'_>, IggyError> {
- decode_prepare_slice_inner(bytes, true)
-}
-
-/// Like [`decode_prepare_slice`] but skips the per-message checksum
-/// verification and batch-checksum recompute, extracting only the header meta.
-/// Every cheap structural check (length, 16-byte alignment, `size` bounds, and
-/// `size` describing the batch exactly) is still enforced.
-///
-/// INVARIANT: `bytes` MUST be node-local self-stamped -
-/// [`stamp_prepare_for_persistence`] recomputed the batch checksum over the
-/// exact blob on the local node - or already integrity-checked at network
-/// ingress. There is no consensus-layer blob validation: the `PrepareHeader`
-/// integrity fields are inert zeros. Replicated and repaired prepares are
-/// validated via [`decode_prepare_slice`] before the bytes reach any trusted
-/// decode. Calling this on unvalidated network bytes would let a corrupted blob
-/// pass undetected. The full-body per-message checksum pass dominates
-/// produce-path CPU, so trusted call sites that only read header meta skip it.
-///
-/// # Errors
-///
-/// Same structural errors as [`decode_prepare_slice`], minus
-/// `InvalidBatchChecksum` and `InvalidMessageChecksum`.
-pub fn decode_prepare_slice_trusted(bytes: &[u8]) -> Result<SendMessages2Ref<'_>, IggyError> {
- decode_prepare_slice_inner(bytes, false)
-}
-
-fn decode_prepare_slice_inner(
- bytes: &[u8],
- validate_checksum: bool,
-) -> Result<SendMessages2Ref<'_>, IggyError> {
- let header_size = std::mem::size_of::<PrepareHeader>();
- if bytes.len() < header_size {
- return Err(IggyError::InvalidCommand);
- }
-
- // Bytemuck enforces alignment in release (maps to InvalidCommand below);
- // debug_assert surfaces the contract violation early in dev.
- debug_assert_eq!(
- bytes
- .as_ptr()
- .align_offset(std::mem::align_of::<PrepareHeader>()),
- 0,
- "decode_prepare_slice: bytes must be at least 16-byte aligned",
- );
-
- let prepare = bytemuck::checked::try_from_bytes::<PrepareHeader>(&bytes[..header_size])
- .map_err(|_| IggyError::InvalidCommand)?;
- let total_size = prepare.size as usize;
- // Wire-controllable `size`: reject < header_size to avoid slice OOB below.
- if total_size < header_size || bytes.len() < total_size {
- return Err(IggyError::InvalidCommand);
- }
-
- let body = &bytes[header_size..total_size];
- if body.len() < COMMAND_HEADER_SIZE {
- return Err(IggyError::InvalidCommand);
- }
-
- let header = SendMessages2Header::decode(&body[..COMMAND_HEADER_SIZE])?;
- let blob_len = header.blob_len()?;
- // Exact, not a lower bound: a prepare frame IS one batch, so bytes past
- // `batch_length` belong to nobody - no checksum covers them, yet the flush
- // writes them, desyncing the segment walk. Readers walking a multi-batch
- // chunk use `decode_batch_slice`, which bounds the blob by design.
- if body.len() != header.total_size() {
- return Err(IggyError::InvalidCommand);
- }
-
- let blob = &body[COMMAND_HEADER_SIZE..COMMAND_HEADER_SIZE + blob_len];
- let batch = SendMessages2Ref { header, blob };
- if validate_checksum {
- let expected_checksum = verify_and_recompute_batch_checksum(&batch)?;
- if header.batch_checksum != expected_checksum {
- return Err(IggyError::InvalidBatchChecksum(
- header.batch_checksum,
- expected_checksum,
- header.base_offset,
- ));
- }
- }
-
- Ok(batch)
-}
-
-pub fn stamp_prepare_for_persistence(
- mut message: Message<PrepareHeader>,
- base_offset: u64,
- base_timestamp: u64,
-) -> Result<(Message<PrepareHeader>, SendMessages2Header, u32), IggyError> {
- let total_size = message.header().size as usize;
- let bytes = message.as_mut_slice();
- if bytes.len() < PREPARE_SPLIT_POINT || total_size < PREPARE_SPLIT_POINT {
- return Err(IggyError::InvalidCommand);
- }
-
- let header_offset = std::mem::size_of::<PrepareHeader>();
- let mut command =
- SendMessages2Header::decode(&bytes[header_offset..header_offset + COMMAND_HEADER_SIZE])?;
- command.base_offset = base_offset;
- command.base_timestamp = base_timestamp;
- let blob = &bytes[PREPARE_SPLIT_POINT..total_size];
- command.batch_checksum = calculate_batch_checksum(&command, blob);
- command.encode_into(&mut bytes[header_offset..header_offset + COMMAND_HEADER_SIZE]);
- Ok((message, command, command.message_count))
-}
-
-fn legacy_messages_slice(body: &[u8]) -> Result<(u32, &[u8]), IggyError> {
- if body.len() < 4 {
- return Err(IggyError::InvalidCommand);
- }
-
- let metadata_length = read_u32(body, 0)? as usize;
- let metadata_end = 4usize
- .checked_add(metadata_length)
- .ok_or(IggyError::InvalidCommand)?;
- if metadata_end < 4 || body.len() < metadata_end {
- return Err(IggyError::InvalidCommand);
- }
-
- let message_count = read_u32(body, metadata_end - 4)?;
- let indexes_len = usize::try_from(message_count)
- .ok()
- .and_then(|count| count.checked_mul(INDEX_SIZE))
- .ok_or(IggyError::InvalidCommand)?;
- let messages_start = metadata_end
- .checked_add(indexes_len)
- .ok_or(IggyError::InvalidCommand)?;
- if body.len() < messages_start {
- return Err(IggyError::InvalidCommand);
- }
-
- Ok((message_count, &body[messages_start..]))
-}
-
-#[derive(Clone, Copy)]
-struct LegacyMessageRef<'a> {
- id: u128,
- origin_timestamp: u64,
- user_headers: &'a [u8],
- payload: &'a [u8],
- total_size: usize,
-}
-
-impl<'a> LegacyMessageRef<'a> {
- fn decode(bytes: &'a [u8]) -> Result<Self, IggyError> {
- if bytes.len() < LEGACY_MESSAGE_HEADER_SIZE {
- return Err(IggyError::InvalidCommand);
- }
-
- let user_headers_length = read_u32(bytes, 48)? as usize;
- let payload_length = read_u32(bytes, 52)? as usize;
- let total_size = LEGACY_MESSAGE_HEADER_SIZE
- .checked_add(payload_length)
- .and_then(|size| size.checked_add(user_headers_length))
- .ok_or(IggyError::InvalidCommand)?;
- if bytes.len() < total_size {
- return Err(IggyError::InvalidCommand);
- }
-
- let payload_start = LEGACY_MESSAGE_HEADER_SIZE;
- let payload_end = payload_start + payload_length;
- let headers_end = payload_end + user_headers_length;
-
- Ok(Self {
- id: read_u128(bytes, 8)?,
- origin_timestamp: read_u64(bytes, 40)?,
- user_headers: &bytes[payload_end..headers_end],
- payload: &bytes[payload_start..payload_end],
- total_size,
- })
- }
-}
-
-/// Batch checksum v2: streaming `XxHash3_64` over the six batch header meta
-/// fields followed by each message's stored 8-byte checksum field in message
-/// order - NOT the message bodies.
-///
-/// Bodies are bound only transitively: each per-message checksum already covers
-/// `header[8..48] || payload || user_headers`, so hashing the checksum fields
-/// binds every body byte IFF a reader also re-verifies the per-message
-/// checksums. Stamp (produce) hashes `N * 8` bytes instead of the whole blob;
-/// validating decoders pay the one body pass as the per-message verify in
-/// [`verify_and_recompute_batch_checksum`], which hashes the checksum-field
-/// bytes in the same order so its recompute matches a compute here.
-///
-/// Assumes a well-formed blob whose frames tile exactly; every compute site
-/// builds the blob and satisfies this.
-fn calculate_batch_checksum(header: &SendMessages2Header, blob: &[u8]) -> u64 {
- let mut hasher = XxHash3_64::new();
- write_batch_header_fields(&mut hasher, header);
- let batch = SendMessages2Ref {
- header: *header,
- blob,
- };
- for framed in batch.iter_with_offsets() {
- hasher.write(&blob[framed.start..framed.start + 8]);
- }
- hasher.finish()
-}
-
-fn write_batch_header_fields(hasher: &mut XxHash3_64, header: &SendMessages2Header) {
- hasher.write(&header.partition_id.to_le_bytes());
- hasher.write(&header.base_offset.to_le_bytes());
- hasher.write(&header.base_timestamp.to_le_bytes());
- hasher.write(&header.origin_timestamp.to_le_bytes());
- hasher.write(&header.batch_length.to_le_bytes());
- hasher.write(&header.message_count.to_le_bytes());
-}
-
-/// Verify every per-message checksum in `batch` and return the recomputed v2
-/// batch checksum (see [`calculate_batch_checksum`]) from a single frame walk.
-///
-/// The per-message pass is the equal-integrity half of v2: the batch value
-/// binds bodies only through the checksum fields, so a validating decode must
-/// re-verify each message here or body corruption that leaves the checksum
-/// field intact would pass. This is the one full-body pass a validating decode
-/// pays; the caller then compares the returned value against the stored
-/// `batch_checksum`.
-///
-/// # Errors
-///
-/// [`IggyError::InvalidMessageChecksum`] on the first per-message mismatch;
-/// [`IggyError::InvalidCommand`] if the frames do not tile `message_count`
-/// exactly.
-fn verify_and_recompute_batch_checksum(batch: &SendMessages2Ref<'_>) -> Result<u64, IggyError> {
- let blob = batch.blob();
- let mut hasher = XxHash3_64::new();
- write_batch_header_fields(&mut hasher, &batch.header);
- let mut verified = 0u32;
- let mut covered = 0usize;
- for framed in batch.iter_with_offsets() {
- // Cover (`header[8..48] || payload || user_headers`) hashed raw from the
- // blob, byte-exact with the encoder's, so a flipped body byte fails even
- // when the stored checksum field is left intact.
- let stored = framed.message.header.checksum;
- let expected = XxHash3_64::oneshot(&blob[framed.start + 8..framed.end]);
- if expected != stored {
- return Err(IggyError::InvalidMessageChecksum(
- stored,
- expected,
- batch.header.base_offset + u64::from(framed.message.header.offset_delta),
- ));
- }
- hasher.write(&blob[framed.start..framed.start + 8]);
- verified += 1;
- covered = framed.end;
- }
- if verified != batch.message_count() || covered != blob.len() {
- return Err(IggyError::InvalidCommand);
- }
- Ok(hasher.finish())
-}
-
-/// The layout half of [`verify_and_recompute_batch_checksum`], without the hashing.
-///
-/// A caller that opts out of checksum verification still must not be handed a batch
-/// whose framing disagrees with its header, since it indexes by `batch_length` and
-/// would step into the next record. Walking the frames costs no hashing.
-fn validate_batch_layout(batch: &SendMessages2Ref<'_>) -> Result<(), IggyError> {
- let mut framed = 0u32;
- let mut covered = 0usize;
- for message in batch.iter_with_offsets() {
- framed += 1;
- covered = message.end;
- }
- if framed != batch.message_count() || covered != batch.blob().len() {
- return Err(IggyError::InvalidCommand);
- }
- Ok(())
-}
-
-fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, IggyError> {
- bytes
- .get(offset..offset + 4)
- .and_then(|slice| slice.try_into().ok())
- .map(u32::from_le_bytes)
- .ok_or(IggyError::InvalidNumberEncoding)
-}
-
-fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, IggyError> {
- bytes
- .get(offset..offset + 8)
- .and_then(|slice| slice.try_into().ok())
- .map(u64::from_le_bytes)
- .ok_or(IggyError::InvalidNumberEncoding)
-}
-
-fn read_u128(bytes: &[u8], offset: usize) -> Result<u128, IggyError> {
- bytes
- .get(offset..offset + 16)
- .and_then(|slice| slice.try_into().ok())
- .map(u128::from_le_bytes)
- .ok_or(IggyError::InvalidNumberEncoding)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use iggy_binary_protocol::{Command2, Operation};
- use iggy_common::Aes256GcmEncryptor;
-
- fn aligned_prepare_bytes(size: u32) -> Owned<MESSAGE_ALIGN> {
- let mut owned = Owned::<MESSAGE_ALIGN>::zeroed(std::mem::size_of::<PrepareHeader>());
- let header: &mut PrepareHeader =
- bytemuck::checked::try_from_bytes_mut(owned.as_mut_slice())
- .expect("zeroed bytes form a valid PrepareHeader");
- header.command = Command2::Prepare;
- header.size = size;
- owned
- }
-
- /// Assemble an already-stamped batch into a `Prepare`:
- /// `[PrepareHeader][256B batch header][blob]`, copying `owned`'s header and
- /// blob verbatim. Shared by every real-batch fixture.
- fn prepare_from_owned(owned: &SendMessages2Owned) -> Owned<MESSAGE_ALIGN> {
- let header_size = std::mem::size_of::<PrepareHeader>();
- let total = header_size + owned.header.total_size();
- let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total);
- {
- let prepare: &mut PrepareHeader =
- bytemuck::checked::try_from_bytes_mut(&mut buffer.as_mut_slice()[..header_size])
- .expect("zeroed bytes form a valid PrepareHeader");
- prepare.command = Command2::Prepare;
- prepare.size = u32::try_from(total).expect("prepare size fits u32");
- }
- let bytes = buffer.as_mut_slice();
- owned
- .header
- .encode_into(&mut bytes[header_size..header_size + COMMAND_HEADER_SIZE]);
- bytes[PREPARE_SPLIT_POINT..PREPARE_SPLIT_POINT + owned.blob.len()]
- .copy_from_slice(&owned.blob);
- buffer
- }
-
- /// A checksum-consistent STAMPED `Prepare` carrying real per-message records,
- /// stamped at a non-zero `base_offset` / `base_timestamp` with a v2
- /// `batch_checksum` over the final header fields + per-message checksum fields.
- fn valid_prepare_bytes() -> Owned<MESSAGE_ALIGN> {
- let namespace = IggyNamespace::new(1, 1, 7);
- let mut owned = SendMessages2Owned::from_messages(namespace, &sample_messages())
- .expect("build send batch");
- owned.header.base_offset = 10;
- owned.header.base_timestamp = 20;
- owned.header.batch_checksum = owned.header.checksum_for_blob(&owned.blob);
- prepare_from_owned(&owned)
- }
-
- #[test]
- fn decode_prepare_slice_trusted_matches_validating_for_valid_batch() {
- // The trusted variant must surface byte-identical header meta to the
- // validating decode for a checksum-consistent batch; only the
- // per-message and batch-checksum passes are skipped.
- let owned = valid_prepare_bytes();
-
- let validated = decode_prepare_slice(owned.as_slice()).expect("valid batch decodes");
- let trusted =
- decode_prepare_slice_trusted(owned.as_slice()).expect("valid batch decodes trusted");
-
- assert_eq!(validated.header.base_offset, trusted.header.base_offset);
- assert_eq!(
- validated.header.base_timestamp,
- trusted.header.base_timestamp
- );
- assert_eq!(
- validated.header.origin_timestamp,
- trusted.header.origin_timestamp
- );
- assert_eq!(validated.header.batch_length, trusted.header.batch_length);
- assert_eq!(validated.message_count(), trusted.message_count());
- assert_eq!(validated.header.total_size(), trusted.header.total_size());
- assert_eq!(validated.blob(), trusted.blob());
- }
-
- #[test]
- fn decode_prepare_slice_trusted_skips_batch_checksum() {
- // A stored batch_checksum mutated after stamping fails the validating
- // decode but passes the trusted one: exactly why the trusted variant is
- // confined to locally-produced bytes (see its doc invariant).
- let mut owned = valid_prepare_bytes();
- let corrupt_index = std::mem::size_of::<PrepareHeader>() + BATCH_CHECKSUM_OFFSET;
- owned.as_mut_slice()[corrupt_index] ^= 0xFF;
-
- assert!(
- matches!(
- decode_prepare_slice(owned.as_slice()),
- Err(IggyError::InvalidBatchChecksum(..))
- ),
- "validating decode must reject a mutated batch checksum",
- );
- assert!(
- decode_prepare_slice_trusted(owned.as_slice()).is_ok(),
- "trusted decode skips the batch-checksum recomputation",
- );
- }
-
- #[test]
- fn decode_prepare_slice_size_below_header_size_does_not_panic() {
- // Regression: without the `total_size < header_size` guard,
- // `&bytes[256..size]` panics for any size < 256.
- for adversarial_size in [0u32, 255] {
- let owned = aligned_prepare_bytes(adversarial_size);
- let result = decode_prepare_slice(owned.as_slice());
- assert!(
- matches!(result, Err(IggyError::InvalidCommand)),
- "size={adversarial_size} must be rejected, got {result:?}",
- );
- }
- }
-
- #[cfg(debug_assertions)]
- #[test]
- #[should_panic(expected = "must be at least 16-byte aligned")]
- fn decode_prepare_slice_debug_asserts_on_misaligned_input() {
- // `Vec<u8>` requests align=1; glibc returns a base that is a
- // multiple of 16, so `&buf[1..]` has offset 1 mod 16, eliably
- // misaligned.
- let buf: Vec<u8> = vec![0u8; std::mem::size_of::<PrepareHeader>() + 1];
- let misaligned = &buf[1..];
- assert_ne!(
- misaligned.as_ptr().align_offset(16),
- 0,
- "test setup: allocator returned non-16k base",
- );
- let _ = decode_prepare_slice(misaligned);
- }
-
- fn sample_messages() -> IggyMessages2 {
- let mut messages = IggyMessages2::with_capacity(2);
- messages.push(IggyMessage2 {
- header: IggyMessage2Header {
- id: 7,
- origin_timestamp: 1_000,
- ..Default::default()
- },
- payload: Bytes::from_static(b"first-payload"),
- user_headers: None,
- });
- messages.push(IggyMessage2 {
- header: IggyMessage2Header {
- id: 8,
- origin_timestamp: 1_050,
- ..Default::default()
- },
- payload: Bytes::from_static(b"second-payload"),
- user_headers: Some(Bytes::from_static(b"user-header-bytes")),
- });
- messages
- }
-
- /// `[PrepareHeader][256B batch header][blob]` carrying real per-message
- /// records and checksums from the production encoder, with the initial zero
- /// base offset and timestamp.
- fn prepare_with_messages(messages: &IggyMessages2) -> Owned<MESSAGE_ALIGN> {
- let namespace = IggyNamespace::new(1, 1, 7);
- let owned =
- SendMessages2Owned::from_messages(namespace, messages).expect("build send batch");
- prepare_from_owned(&owned)
- }
-
- #[test]
- fn checksum_oneshot_matches_streaming_reference() {
- // Formula pin: the per-message checksum is XxHash3-64 (default seed)
- // over `header[8..48] || payload || user_headers` as one byte stream.
- // The encoders hash the concatenation in a single oneshot pass; this
- // streaming reference feeds the same parts separately. Both must agree
- // for every shape, or checksums at rest stop verifying.
- fn streaming_reference(header_tail: &[u8], payload: &[u8], user_headers: &[u8]) -> u64 {
- let mut hasher = XxHash3_64::new();
- hasher.write(header_tail);
- hasher.write(payload);
- hasher.write(user_headers);
- hasher.finish()
- }
-
- let header_tail: Vec<u8> = (0u8..40).collect();
- let kilobyte: Vec<u8> = (0..1024u32).map(|index| (index % 251) as u8).collect();
- let cases: &[(&[u8], &[u8])] = &[
- (&[], &[]),
- (b"payload-bytes", &[]),
- (b"payload-bytes", b"user-header-bytes"),
- (&kilobyte, &[]),
- (&kilobyte, &kilobyte[..7]),
- (&kilobyte[..1023], &kilobyte[..7]),
- ];
- for (payload, user_headers) in cases {
- let mut concatenated =
- Vec::with_capacity(header_tail.len() + payload.len() + user_headers.len());
- concatenated.extend_from_slice(&header_tail);
- concatenated.extend_from_slice(payload);
- concatenated.extend_from_slice(user_headers);
- assert_eq!(
- XxHash3_64::oneshot(&concatenated),
- streaming_reference(&header_tail, payload, user_headers),
- "oneshot must match the streaming reference for payload {} B, user headers {} B",
- payload.len(),
- user_headers.len(),
- );
- }
- }
-
- #[test]
- fn batch_checksum_v2_pins_header_fields_then_message_checksum_fields() {
- // Formula pin for batch checksum v2: XxHash3-64 (default seed) streaming
- // over the six batch header meta fields (LE, in field order) then each
- // message's stored 8-byte checksum field in message order - never the
- // bodies. This reference walks the blob by the KNOWN input message sizes,
- // independent of the production frame decoder, and must equal what the
- // encoder stamped, or a stamp will not verify against a read-back
- // recompute.
- let namespace = IggyNamespace::new(1, 1, 7);
- let messages = sample_messages();
- let mut owned =
- SendMessages2Owned::from_messages(namespace, &messages).expect("build batch");
- owned.header.base_offset = 100;
- owned.header.base_timestamp = 200;
- owned.header.batch_checksum = owned.header.checksum_for_blob(&owned.blob);
-
- let mut hasher = XxHash3_64::new();
- hasher.write(&owned.header.partition_id.to_le_bytes());
- hasher.write(&owned.header.base_offset.to_le_bytes());
- hasher.write(&owned.header.base_timestamp.to_le_bytes());
- hasher.write(&owned.header.origin_timestamp.to_le_bytes());
- hasher.write(&owned.header.batch_length.to_le_bytes());
- hasher.write(&owned.header.message_count.to_le_bytes());
- let mut frame_start = 0usize;
- for message in messages.iter() {
- hasher.write(&owned.blob[frame_start..frame_start + 8]);
- let user_headers = message.user_headers.as_deref().unwrap_or_default();
- frame_start += MESSAGE_HEADER_SIZE + message.payload.len() + user_headers.len();
- }
- let reference = hasher.finish();
-
- assert_eq!(
- frame_start,
- owned.blob.len(),
- "reference walk must consume the whole blob",
- );
- assert_eq!(
- owned.header.batch_checksum, reference,
- "v2 batch checksum must equal hash(6 header fields || per-message checksum fields)",
- );
- }
-
- #[test]
- fn decode_batch_slice_rejects_body_corruption_with_intact_checksum_field() {
- // Equal-integrity: v2 binds bodies only through the per-message checksum
- // fields, so a flipped body byte that leaves the 8-byte checksum field
- // intact keeps the batch value matching. The validating decode must still
- // reject it via the per-message verify - the sole at-rest read-back check
- // (the poll disk walk) decodes through here.
- let namespace = IggyNamespace::new(1, 1, 7);
- let owned =
- SendMessages2Owned::from_messages(namespace, &sample_messages()).expect("build batch");
- let mut body = vec![0u8; COMMAND_HEADER_SIZE + owned.blob.len()];
- owned.header.encode_into(&mut body[..COMMAND_HEADER_SIZE]);
- body[COMMAND_HEADER_SIZE..].copy_from_slice(&owned.blob);
-
- decode_batch_slice(&body).expect("the clean batch decodes");
-
- // First payload byte sits right after the command header and the first
- // message's 48B frame header, leaving that frame's checksum field intact.
- let payload_index = COMMAND_HEADER_SIZE + MESSAGE_HEADER_SIZE;
- body[payload_index] ^= 0xFF;
- assert!(
- matches!(
- decode_batch_slice(&body),
- Err(IggyError::InvalidMessageChecksum(..))
- ),
- "body corruption with an intact checksum field must fail the per-message verify",
- );
- }
-
- #[test]
- fn decode_prepare_slice_rejects_body_corruption_with_intact_checksum_field() {
- // The same equal-integrity guarantee at the resident/repair validating
- // decode, plus proof that the batch value alone is blind to it.
- let mut owned = prepare_with_messages(&sample_messages());
- decode_prepare_slice(owned.as_slice()).expect("the clean prepare decodes");
-
- let payload_index = PREPARE_SPLIT_POINT + MESSAGE_HEADER_SIZE;
- owned.as_mut_slice()[payload_index] ^= 0xFF;
- assert!(
- matches!(
- decode_prepare_slice(owned.as_slice()),
- Err(IggyError::InvalidMessageChecksum(..))
- ),
- "body corruption with an intact checksum field must fail the per-message verify",
- );
- assert!(
- decode_prepare_slice_trusted(owned.as_slice()).is_ok(),
- "the intact checksum field leaves the batch value matching, so trusted still passes",
- );
- }
-
- /// Legacy `SendMessages` request body: `[metadata_len=4][message_count]`
- /// then `count` skipped index slots, then the 64B-header legacy records.
- fn legacy_send_messages_body(messages: &IggyMessages2) -> Vec<u8> {
- let count = messages.count();
- let mut body = Vec::new();
- body.extend_from_slice(&4u32.to_le_bytes());
- body.extend_from_slice(&count.to_le_bytes());
- body.extend_from_slice(&vec![0u8; count as usize * INDEX_SIZE]);
- for message in messages.iter() {
- let user_headers = message.user_headers.as_deref().unwrap_or_default();
- let mut header = [0u8; LEGACY_MESSAGE_HEADER_SIZE];
- header[8..24].copy_from_slice(&message.header.id.to_le_bytes());
- header[40..48].copy_from_slice(&message.header.origin_timestamp.to_le_bytes());
- header[48..52].copy_from_slice(&(user_headers.len() as u32).to_le_bytes());
- header[52..56].copy_from_slice(&(message.payload.len() as u32).to_le_bytes());
- body.extend_from_slice(&header);
- body.extend_from_slice(&message.payload);
- body.extend_from_slice(user_headers);
- }
- body
- }
-
- fn legacy_request_message(body: &[u8]) -> Message<RoutedRequestHeader> {
- let header_size = std::mem::size_of::<RoutedRequestHeader>();
- let total = header_size + body.len();
- let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total);
- {
- let header: &mut RoutedRequestHeader =
- bytemuck::checked::try_from_bytes_mut(&mut buffer.as_mut_slice()[..header_size])
- .expect("zeroed bytes form a valid RoutedRequestHeader");
- header.command = Command2::Request;
- header.operation = Operation::SendMessages;
- header.client = 1;
- header.session = 1;
- header.request = 1;
- header.size = u32::try_from(total).expect("size fits u32");
- }
- buffer.as_mut_slice()[header_size..].copy_from_slice(body);
- Message::try_from(buffer).expect("legacy request message is valid")
- }
-
- #[test]
- fn convert_request_message_rejects_empty_canonical_and_legacy_batches() {
- let namespace = IggyNamespace::new(1, 1, 3);
- let messages = IggyMessages2::with_capacity(0);
- let canonical = SendMessages2Owned::from_messages(namespace, &messages)
- .expect("build empty canonical batch");
- let mut canonical_body = vec![0; canonical.header.total_size()];
- canonical
- .header
- .encode_into(&mut canonical_body[..COMMAND_HEADER_SIZE]);
- let legacy_body = legacy_send_messages_body(&messages);
-
- for mode in [ChecksumMode::Compute, ChecksumMode::Skip] {
- let canonical_result =
- convert_request_message(namespace, legacy_request_message(&canonical_body), mode);
- assert!(matches!(canonical_result, Err(IggyError::InvalidCommand)));
-
- let legacy_result =
- convert_request_message(namespace, legacy_request_message(&legacy_body), mode);
- assert!(matches!(legacy_result, Err(IggyError::InvalidCommand)));
- }
- }
-
- #[test]
- fn convert_request_message_transcodes_legacy_to_canonical_bytes() {
- // Golden: the fused legacy transcode must emit the exact canonical batch
- // the native builder (`from_messages`) produces for the same messages -
- // command header + blob, byte for byte. Explicit non-zero ids keep it
- // deterministic (no `random_id` substitution).
- let namespace = IggyNamespace::new(1, 1, 3);
- let messages = sample_messages();
-
- let owned =
- SendMessages2Owned::from_messages(namespace, &messages).expect("build canonical batch");
- let mut expected_body = vec![0u8; COMMAND_HEADER_SIZE + owned.blob.len()];
- owned
- .header
- .encode_into(&mut expected_body[..COMMAND_HEADER_SIZE]);
- expected_body[COMMAND_HEADER_SIZE..].copy_from_slice(&owned.blob);
-
- let legacy = legacy_request_message(&legacy_send_messages_body(&messages));
- let converted = convert_request_message(namespace, legacy, ChecksumMode::Compute)
- .expect("legacy body transcodes");
- let header_size = std::mem::size_of::<RoutedRequestHeader>();
- let actual_body = &converted.as_slice()[header_size..converted.header().size as usize];
-
- assert_eq!(
- actual_body, expected_body,
- "legacy transcode must be byte-identical to the canonical native batch",
- );
-
- // And the emitted batch is self-consistent: it validates through the
- // batch-checksum decode and yields the original messages.
- let decoded = decode_batch_slice(actual_body).expect("transcoded batch checksum is valid");
- assert_eq!(decoded.message_count(), messages.count());
- let payloads: Vec<&[u8]> = decoded.iter().map(|view| view.payload).collect();
- assert_eq!(
- payloads,
- vec![&b"first-payload"[..], &b"second-payload"[..]]
- );
- }
-
- #[test]
- fn convert_request_message_skip_leaves_batch_checksum_zero_until_stamp() {
- // The partition ingest path passes Skip: the transcoded batch must carry
- // a zero checksum (stamp fills it) and be otherwise byte-identical to the
- // Compute output - the flag toggles nothing but that one hash.
- let namespace = IggyNamespace::new(1, 1, 3);
- let messages = sample_messages();
- let body = legacy_send_messages_body(&messages);
- let header_size = std::mem::size_of::<RoutedRequestHeader>();
-
- let computed = convert_request_message(
- namespace,
- legacy_request_message(&body),
- ChecksumMode::Compute,
- )
- .expect("compute transcode");
- let skipped =
- convert_request_message(namespace, legacy_request_message(&body), ChecksumMode::Skip)
- .expect("skip transcode");
-
- let computed_body = &computed.as_slice()[header_size..computed.header().size as usize];
- let skipped_body = &skipped.as_slice()[header_size..skipped.header().size as usize];
-
- let skipped_header = SendMessages2Header::decode(&skipped_body[..COMMAND_HEADER_SIZE])
- .expect("decode skipped header");
- assert_eq!(
- skipped_header.batch_checksum, 0,
- "skip leaves the batch checksum zero until stamp",
- );
-
- // Patch only the 8-byte batch_checksum field into the skipped body; it
- // must then equal the computed body, proving nothing else diverges.
- let mut patched = skipped_body.to_vec();
- patched[BATCH_CHECKSUM_OFFSET..BATCH_CHECKSUM_OFFSET + 8]
- .copy_from_slice(&computed_body[BATCH_CHECKSUM_OFFSET..BATCH_CHECKSUM_OFFSET + 8]);
- assert_eq!(
- patched.as_slice(),
- computed_body,
- "skip and compute differ only in the batch_checksum field",
- );
- }
-
- #[test]
- fn encrypt_ingest_path_stays_canonical_through_flag_split() {
- // Mirror the plane encrypt ingest sequence: convert(Compute) -> the
- // validating decode encrypt performs on its input -> encrypt -> the
- // validating decode the second convert performs as its discriminator ->
- // convert(Skip) (the partition convert), which sees an already-canonical
- // batch and returns it unchanged. Every decode must succeed.
- let namespace = IggyNamespace::new(1, 1, 3);
- let messages = sample_messages();
- let header_size = std::mem::size_of::<RoutedRequestHeader>();
-
- let legacy = legacy_request_message(&legacy_send_messages_body(&messages));
- let canonical = convert_request_message(namespace, legacy, ChecksumMode::Compute)
- .expect("pre-encrypt transcode");
- let canonical_body = &canonical.as_slice()[header_size..canonical.header().size as usize];
- decode_batch_slice(canonical_body).expect("encrypt input decode validates the checksum");
-
- let encryptor =
- EncryptorKind::Aes256Gcm(Aes256GcmEncryptor::new(&[7u8; 32]).expect("valid 32B key"));
- let encrypted = encrypt_batch_request(canonical, &encryptor).expect("encrypt batch");
- let encrypted_body: Vec<u8> =
- encrypted.as_slice()[header_size..encrypted.header().size as usize].to_vec();
- decode_batch_slice(&encrypted_body)
- .expect("encrypt output drives the 2nd-convert discriminator");
-
- let repassed = convert_request_message(namespace, encrypted, ChecksumMode::Skip)
- .expect("second convert passes the canonical batch");
- let repassed_body = &repassed.as_slice()[header_size..repassed.header().size as usize];
- assert_eq!(
- repassed_body,
- encrypted_body.as_slice(),
- "an already-canonical encrypted batch passes the partition convert untouched",
- );
- }
-
- /// Junk suffixes that must be refused at both ingest boundaries: one below a
- /// frame header (the frame walk stops on a short read) and one frame-sized
- /// but undecodable (`reserved != 0`). Neither is covered by any checksum, so
- /// a walk that stops at the last decodable frame cannot see them.
- const TRAILING_JUNK_CASES: [&[u8]; 2] = [&[0xAA], &[0xFF; 64]];
-
- /// Canonical `SendMessages` request carrying `junk` past `batch_length`, with
- /// `RoutedRequestHeader.size` inflated to cover it. `size` and `batch_length` are
- /// independent wire fields, so a non-conforming client can emit this.
- fn canonical_request_with_trailing_bytes(junk: &[u8]) -> Message<RoutedRequestHeader> {
- let namespace = IggyNamespace::new(1, 1, 3);
- let owned =
- SendMessages2Owned::from_messages(namespace, &sample_messages()).expect("build batch");
- let header_size = std::mem::size_of::<RoutedRequestHeader>();
- let total = header_size + owned.header.total_size() + junk.len();
- let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total);
- {
- let header: &mut RoutedRequestHeader =
- bytemuck::checked::try_from_bytes_mut(&mut buffer.as_mut_slice()[..header_size])
- .expect("zeroed bytes form a valid RoutedRequestHeader");
- header.command = Command2::Request;
- header.operation = Operation::SendMessages;
- header.client = 1;
- header.session = 1;
- header.request = 1;
- header.size = u32::try_from(total).expect("size fits u32");
- }
- let bytes = buffer.as_mut_slice();
- owned
- .header
- .encode_into(&mut bytes[header_size..header_size + COMMAND_HEADER_SIZE]);
- let blob_end = PREPARE_SPLIT_POINT + owned.blob.len();
- bytes[PREPARE_SPLIT_POINT..blob_end].copy_from_slice(&owned.blob);
- bytes[blob_end..].copy_from_slice(junk);
- Message::try_from(buffer).expect("request message is valid")
- }
-
- /// A `Prepare` whose `size` covers `junk` past `batch_length`.
- fn prepare_with_trailing_bytes(junk: &[u8]) -> Owned<MESSAGE_ALIGN> {
- let namespace = IggyNamespace::new(1, 1, 7);
- let owned =
- SendMessages2Owned::from_messages(namespace, &sample_messages()).expect("build batch");
- let header_size = std::mem::size_of::<PrepareHeader>();
- let total = header_size + owned.header.total_size() + junk.len();
- let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total);
- {
- let prepare: &mut PrepareHeader =
- bytemuck::checked::try_from_bytes_mut(&mut buffer.as_mut_slice()[..header_size])
- .expect("zeroed bytes form a valid PrepareHeader");
- prepare.command = Command2::Prepare;
- prepare.size = u32::try_from(total).expect("prepare size fits u32");
- }
- let bytes = buffer.as_mut_slice();
- owned
- .header
- .encode_into(&mut bytes[header_size..header_size + COMMAND_HEADER_SIZE]);
- let blob_end = PREPARE_SPLIT_POINT + owned.blob.len();
- bytes[PREPARE_SPLIT_POINT..blob_end].copy_from_slice(&owned.blob);
- bytes[blob_end..].copy_from_slice(junk);
- buffer
- }
-
- #[test]
- fn convert_request_message_rejects_canonical_batch_with_trailing_bytes() {
- // Client ingest boundary. Accepting the request would carry the suffix
- // into the journal and onto disk: the flush writes the whole frame while
- // every reader advances by `batch_length`, so the segment walk lands
- // inside the junk and every later batch becomes unreadable.
- let namespace = IggyNamespace::new(1, 1, 3);
- for junk in TRAILING_JUNK_CASES {
- for mode in [ChecksumMode::Compute, ChecksumMode::Skip] {
- let message = canonical_request_with_trailing_bytes(junk);
- let result = convert_request_message(namespace, message, mode);
- assert!(
- matches!(result, Err(IggyError::InvalidCommand)),
- "{} trailing bytes ({mode:?}) must be rejected, got {result:?}",
- junk.len(),
- );
- }
- }
- }
-
- #[test]
- fn convert_request_message_accepts_exact_canonical_batch() {
- // The same builder with no suffix must still pass untouched, so the
- // rejection above is the suffix and not the fixture.
- let namespace = IggyNamespace::new(1, 1, 3);
- let message = canonical_request_with_trailing_bytes(&[]);
- let expected = message.as_slice().to_vec();
- let converted = convert_request_message(namespace, message, ChecksumMode::Skip)
- .expect("an exact canonical batch passes untouched");
- assert_eq!(converted.as_slice(), expected.as_slice());
- }
-
- #[test]
- fn decode_prepare_slice_rejects_trailing_bytes_past_batch_length() {
- // Replica ingest must reject bytes beyond `batch_length` because no
- // per-message checksum covers them.
- for junk in TRAILING_JUNK_CASES {
- let owned = prepare_with_trailing_bytes(junk);
- assert!(
- matches!(
- decode_prepare_slice(owned.as_slice()),
- Err(IggyError::InvalidCommand)
- ),
- "{} trailing bytes must fail the validating decode",
- junk.len(),
- );
- }
- }
-}
diff --git a/core/shard/src/lib.rs b/core/shard/src/lib.rs
index d11ef93..726bcb0 100644
--- a/core/shard/src/lib.rs
+++ b/core/shard/src/lib.rs
@@ -39,7 +39,7 @@
use crossfire::TrySendError;
use futures::FutureExt;
use iggy_binary_protocol::{
- CHECKSUM_UNSEALED, Command2, CommitHeader, ConsensusHeader, DoViewChangeHeader,
+ CHECKSUM_UNSEALED, Command, CommitHeader, ConsensusHeader, DoViewChangeHeader,
ForwardLogoutHeader, ForwardLogoutResultHeader, ForwardRegisterHeader,
ForwardRegisterResultHeader, GenericHeader, Operation, PrepareHeader, PrepareOkHeader,
RepairPrepareHeader, RepairRangeReplyHeader, RequestPreparesHeader, RequestStartViewHeader,
@@ -2309,7 +2309,7 @@
/// No client on this node: nothing to answer, nothing recovers it.
fn is_replicated(&self) -> bool {
- self.message.header().command != Command2::Request
+ self.message.header().command != Command::Request
}
}
@@ -3006,7 +3006,7 @@
/// return is what makes callers bump
/// `frame_drops_total{variant=partition,reason=park_dropped}`.
fn deny_parked_client_request(&self, frame: ParkedFrame) -> bool {
- if frame.message.header().command == Command2::Request
+ if frame.message.header().command == Command::Request
&& let Ok(request) = frame.message.try_into_typed::<RoutedRequestHeader>()
{
return self.stage_transient_deny(request.header());
@@ -3105,7 +3105,7 @@
.streams()
.created_revision_for_namespace(namespace);
let frame_cost = parked_footprint(message.as_slice().len());
- let replicated = message.header().command() != Command2::Request;
+ let replicated = message.header().command() != Command::Request;
let mut pending = self.pending_partition_frames.borrow_mut();
let parked_bytes = self.parked_partition_bytes.get();
// Read the entry without `entry().or_default()`: inserting first would
@@ -4108,7 +4108,7 @@
cluster,
self_id,
target,
- Command2::RangeEvicted,
+ Command::RangeEvicted,
header.nonce,
from_op,
header.group,
@@ -4118,7 +4118,7 @@
cluster,
self_id,
target,
- Command2::RepairDone,
+ Command::RepairDone,
header.nonce,
header.from_op.saturating_sub(1),
header.group,
@@ -4131,7 +4131,7 @@
cluster,
self_id,
target,
- Command2::RangeEvicted,
+ Command::RangeEvicted,
header.nonce,
from_op,
header.group,
@@ -4160,7 +4160,7 @@
cluster,
self_id,
target,
- Command2::RepairDone,
+ Command::RepairDone,
header.nonce,
served_through,
header.group,
@@ -4239,7 +4239,7 @@
cluster,
self_id,
target,
- Command2::RangeEvicted,
+ Command::RangeEvicted,
header.nonce,
retained_from,
header.group,
@@ -4262,7 +4262,7 @@
cluster,
self_id,
target,
- Command2::RepairDone,
+ Command::RepairDone,
header.nonce,
served_through,
header.group,
@@ -4307,7 +4307,7 @@
// run full prepare validation on it.
let msg = msg.transmute_header(|old: RepairPrepareHeader, new: &mut PrepareHeader| {
*new = old.0;
- new.command = Command2::Prepare;
+ new.command = Command::Prepare;
});
let header = *msg.header();
let planes = self.plane.inner();
@@ -4481,7 +4481,7 @@
return;
}
match header.command {
- Command2::RepairDone => {
+ Command::RepairDone => {
let before = consensus.commit_min();
planes.0.commit_journal().await;
// Completion is decided by the LOCAL walk, not the
@@ -4516,7 +4516,7 @@
.await;
}
}
- Command2::RangeEvicted => {
+ Command::RangeEvicted => {
// Journal repair cannot close this gap: the serving peer
// compacted past it, so the ops this replica is missing no
// longer exist as WAL entries anywhere. This is the one
@@ -4617,12 +4617,12 @@
return;
}
match header.command {
- Command2::RangeEvicted => {
+ Command::RangeEvicted => {
if let Some(repair) = partition.repair.as_mut() {
repair.floor = Some(header.op.saturating_sub(1));
}
}
- Command2::RepairDone => {
+ Command::RepairDone => {
// `complete_repair` walks the window and clears the session
// only when the LOCAL commit frontier reached the requested
// op (the peer's served-through claim proves nothing about
@@ -4733,7 +4733,7 @@
{
let msg = Message::<RequestPreparesHeader>::new(size_of::<RequestPreparesHeader>())
.transmute_header(|_, h: &mut RequestPreparesHeader| {
- h.command = Command2::RequestPrepares;
+ h.command = Command::RequestPrepares;
h.cluster = cluster;
h.replica = self_id;
h.nonce = nonce;
@@ -4779,7 +4779,7 @@
const COMMAND_OFFSET: usize = std::mem::offset_of!(GenericHeader, command);
let mut owned =
server_common::iobuf::Owned::<MESSAGE_ALIGN>::copy_from_slice(entry.as_slice());
- owned.as_mut_slice()[COMMAND_OFFSET] = Command2::RepairPrepare as u8;
+ owned.as_mut_slice()[COMMAND_OFFSET] = Command::RepairPrepare as u8;
let Ok(message) = Message::<GenericHeader>::try_from(owned) else {
tracing::warn!(
shard = self.id,
@@ -4800,7 +4800,7 @@
cluster: u128,
self_id: u8,
target: u8,
- command: Command2,
+ command: Command,
nonce: u128,
op: u64,
namespace: u64,
@@ -5297,7 +5297,7 @@
let msg =
Message::<RequestStateTransferHeader>::new(size_of::<RequestStateTransferHeader>())
.transmute_header(|_, h: &mut RequestStateTransferHeader| {
- h.command = Command2::RequestStateTransfer;
+ h.command = Command::RequestStateTransfer;
h.cluster = consensus.cluster();
h.replica = consensus.replica();
h.nonce = nonce;
@@ -5341,7 +5341,7 @@
msg.as_mut_slice()[size_of::<StateTransferTargetHeader>()..].copy_from_slice(manifest);
}
let msg = msg.transmute_header(|_, h: &mut StateTransferTargetHeader| {
- h.command = Command2::StateTransferTarget;
+ h.command = Command::StateTransferTarget;
h.cluster = cluster;
h.replica = self_id;
h.nonce = nonce;
@@ -5385,7 +5385,7 @@
{
let msg = Message::<RequestStateChunkHeader>::new(size_of::<RequestStateChunkHeader>())
.transmute_header(|_, h: &mut RequestStateChunkHeader| {
- h.command = Command2::RequestStateChunk;
+ h.command = Command::RequestStateChunk;
h.cluster = cluster;
h.replica = self_id;
h.nonce = nonce;
@@ -5933,7 +5933,7 @@
chunk.as_mut_slice()[size_of::<StateChunkHeader>()..].copy_from_slice(payload);
Some(ChunkReply::Chunk(chunk.transmute_header(
|_, h: &mut StateChunkHeader| {
- h.command = Command2::StateChunk;
+ h.command = Command::StateChunk;
h.cluster = cluster;
h.replica = self_id;
h.nonce = header.nonce;
@@ -6926,7 +6926,7 @@
chunk.as_mut_slice()[size_of::<StateChunkHeader>()..].copy_from_slice(payload);
ChunkAttempt::Reply(Some(ChunkReply::Chunk(chunk.transmute_header(
|_, h: &mut StateChunkHeader| {
- h.command = Command2::StateChunk;
+ h.command = Command::StateChunk;
h.cluster = cluster;
h.replica = self_id;
h.nonce = header.nonce;
@@ -8988,7 +8988,7 @@
VsrAction::SendStartViewChange { view, group } => {
let msg = Message::<StartViewChangeHeader>::new(size_of::<StartViewChangeHeader>())
.transmute_header(|_, h: &mut StartViewChangeHeader| {
- h.command = Command2::StartViewChange;
+ h.command = Command::StartViewChange;
h.cluster = cluster;
h.replica = self_id;
h.view = *view;
@@ -9017,7 +9017,7 @@
let nack_bitset = suffix.nack_bitset();
let present_bitset = suffix.present_bitset();
let msg = msg.transmute_header(|_, h: &mut DoViewChangeHeader| {
- h.command = Command2::DoViewChange;
+ h.command = Command::DoViewChange;
h.cluster = cluster;
h.replica = self_id;
h.view = *view;
@@ -9045,7 +9045,7 @@
let msg =
Message::<RequestStartViewHeader>::new(size_of::<RequestStartViewHeader>())
.transmute_header(|_, h: &mut RequestStartViewHeader| {
- h.command = Command2::RequestStartView;
+ h.command = Command::RequestStartView;
h.cluster = cluster;
h.replica = self_id;
h.view = *view;
@@ -9073,7 +9073,7 @@
let body_checksum = control_body_checksum(&msg.as_slice()[header_size..total_size]);
let msg = msg.transmute_header(|_, h: &mut StartViewHeader| {
h.checksum_body = body_checksum;
- h.command = Command2::StartView;
+ h.command = Command::StartView;
h.cluster = cluster;
h.replica = self_id;
h.view = *view;
@@ -9111,7 +9111,7 @@
let prepare_header = *prepare_header;
let msg = Message::<PrepareOkHeader>::new(size_of::<PrepareOkHeader>())
.transmute_header(|_, h: &mut PrepareOkHeader| {
- h.command = Command2::PrepareOk;
+ h.command = Command::PrepareOk;
h.cluster = cluster;
h.replica = self_id;
h.view = *view;
@@ -9171,7 +9171,7 @@
} => {
let msg = Message::<CommitHeader>::new(size_of::<CommitHeader>()).transmute_header(
|_, h: &mut CommitHeader| {
- h.command = Command2::Commit;
+ h.command = Command::Commit;
h.cluster = cluster;
h.replica = self_id;
h.view = *view;
@@ -9245,7 +9245,7 @@
};
let msg = Message::<PrepareOkHeader>::new(size_of::<PrepareOkHeader>())
.transmute_header(|_, h: &mut PrepareOkHeader| {
- h.command = Command2::PrepareOk;
+ h.command = Command::PrepareOk;
h.cluster = cluster;
h.replica = self_id;
h.view = *view;
@@ -9362,11 +9362,11 @@
//! Who parked the log decides what it means.
use super::{MergedLog, repair_op_in_scope, repair_serve_ceiling};
- use iggy_binary_protocol::{Command2, PrepareHeader};
+ use iggy_binary_protocol::{Command, PrepareHeader};
fn header(op: u64) -> PrepareHeader {
PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
op,
..Default::default()
}
@@ -9440,11 +9440,11 @@
//! Holding an op is not holding the view's op.
use super::{MergedLog, first_op_not_covered};
- use iggy_binary_protocol::{Command2, Operation, PrepareHeader};
+ use iggy_binary_protocol::{Command, Operation, PrepareHeader};
fn sealed(op: u64, request: u64) -> PrepareHeader {
let mut header = PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
operation: Operation::CreateStream,
op,
request,
@@ -9511,13 +9511,13 @@
//! that premise, so it cannot be quietly reintroduced.
use super::{DVC_HEADERS_MAX, build_dvc_suffix};
- use iggy_binary_protocol::{Command2, Operation, PrepareHeader};
+ use iggy_binary_protocol::{Command, Operation, PrepareHeader};
/// A real prepare at `op`. The operation must not be `Reserved`: that is
/// exactly `dvc_blank`, and `dvc_header_kind` classifies by equality with it.
fn held(op: u64) -> PrepareHeader {
PrepareHeader {
- command: Command2::Prepare,
+ command: Command::Prepare,
operation: Operation::CreateStream,
op,
..Default::default()
@@ -9695,7 +9695,7 @@
//! off. Keying on `checksum_body` looking sealed is bypassable by zeroing it.
use super::{control_body_checksum, control_suffix_body_verified};
- use iggy_binary_protocol::{Command2, DoViewChangeHeader, PrepareHeader};
+ use iggy_binary_protocol::{Command, DoViewChangeHeader, PrepareHeader};
use server_common::Message;
use std::mem::size_of;
@@ -9711,7 +9711,7 @@
*byte = u8::try_from(index % 251).expect("modulus fits u8");
}
msg.transmute_header(|_, header: &mut DoViewChangeHeader| {
- header.command = Command2::DoViewChange;
+ header.command = Command::DoViewChange;
header.checksum_body = checksum_body;
header.size = u32::try_from(total).expect("frame fits u32");
})
diff --git a/core/simulator/src/client.rs b/core/simulator/src/client.rs
index db73cfb..3e7742e 100644
--- a/core/simulator/src/client.rs
+++ b/core/simulator/src/client.rs
@@ -22,8 +22,7 @@
CreateConsumerGroupRequest, DeleteConsumerGroupRequest,
};
use iggy_binary_protocol::requests::consumer_offsets::{
- DeleteConsumerOffset2Request, DeleteConsumerOffsetRequest, StoreConsumerOffset2Request,
- StoreConsumerOffsetRequest,
+ DeleteConsumerOffsetRequest, StoreConsumerOffsetRequest,
};
use iggy_binary_protocol::requests::messages::{
PollMessagesRequest, RawMessage, SendMessagesEncoder,
@@ -71,12 +70,11 @@
/// [`SimClient::request_id_for`].
partition_counter: Cell<u64>,
/// Deterministic per-message id source for produced messages. The real SDK
- /// sends `id: 0` and lets the server mint a random UUID
- /// (`transcode_legacy_request` -> `random_id::get_uuid`); that
- /// mint is unseeded, so under the deterministic executor a produce's
- /// replicated body bytes (and their checksums) would differ run to run,
- /// silently breaking seeded replay. Stamping a deterministic id here keeps
- /// the body a pure function of the seed. See [`SimClient::next_message_id`].
+ /// mints a random UUID for a zero message id before encoding; that mint is
+ /// unseeded, so under the deterministic executor a produce's replicated
+ /// body bytes (and their checksums) would differ run to run, silently
+ /// breaking seeded replay. Stamping a deterministic id here keeps the body
+ /// a pure function of the seed. See [`SimClient::next_message_id`].
message_counter: Cell<u64>,
session: Cell<u64>,
}
@@ -165,7 +163,7 @@
pub fn register(&self) -> Message<RoutedRequestHeader> {
let header_size = std::mem::size_of::<RoutedRequestHeader>();
let header = RoutedRequestHeader {
- command: iggy_binary_protocol::Command2::Request,
+ command: iggy_binary_protocol::Command::Request,
operation: Operation::Register,
size: header_size as u32,
client: self.client_id,
@@ -214,7 +212,7 @@
let header_size = std::mem::size_of::<RoutedRequestHeader>();
let total_size = header_size + body.len();
let header = RoutedRequestHeader {
- command: iggy_binary_protocol::Command2::Request,
+ command: iggy_binary_protocol::Command::Request,
operation: Operation::Register,
size: total_size as u32,
client: self.client_id,
@@ -513,12 +511,12 @@
self.build_request(Operation::DeletePersonalAccessToken, &wire.to_bytes())
}
- /// Build a `SendMessages` request in the legacy `SendMessagesEncoder` wire
- /// shape, byte-compatible with what the real SDK sends (`common` binary
- /// client). VSR clients resolve to an explicit partition before sending, so
- /// the sim always emits `WirePartitioning::PartitionId`: that is the shape
- /// the shell's `resolve_partition_request_namespace` decodes, and the raw
- /// path converts it to `SendMessages2` via `transcode_legacy_request`.
+ /// Build a `SendMessages` request in the `SendMessagesEncoder` wire shape,
+ /// byte-compatible with what the real SDK sends (`common` binary client).
+ /// VSR clients resolve to an explicit partition before sending, so the sim
+ /// always emits `WirePartitioning::PartitionId`: that is the shape the
+ /// shell's `resolve_partition_request_namespace` decodes before admission
+ /// strips the metadata and stamps the batch.
///
/// # Panics
/// Panics if a group id exceeds `u32` or the request buffer is invalid.
@@ -549,7 +547,8 @@
let size = SendMessagesEncoder::encoded_size(&stream_id, &topic_id, &partitioning, &raw);
let mut buf = BytesMut::with_capacity(size);
- SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &raw);
+ SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &raw)
+ .expect("simulator send batch encodes");
self.build_request_with_namespace(Operation::SendMessages, &buf, group)
}
@@ -584,7 +583,7 @@
let mut reserved = [0u8; 52];
reserved[..4].copy_from_slice(&POLL_MESSAGES_CODE.to_le_bytes());
let header = RoutedRequestHeader {
- command: iggy_binary_protocol::Command2::Request,
+ command: iggy_binary_protocol::Command::Request,
operation: Operation::NonReplicated,
size: total_size as u32,
client: self.client_id,
@@ -602,55 +601,13 @@
.expect("poll request must be valid")
}
- pub fn store_consumer_offset(
- &self,
- group: IggyNamespace,
- consumer_kind: u8,
- consumer_id: u32,
- offset: u64,
- ) -> Message<RoutedRequestHeader> {
- let (stream_id, topic_id, partition_id) = namespace_ids(group);
- let request = StoreConsumerOffsetRequest {
- consumer: namespace_consumer(consumer_kind, consumer_id),
- stream_id,
- topic_id,
- partition_id,
- offset,
- };
- self.build_request_with_namespace(
- Operation::StoreConsumerOffset,
- &request.to_bytes(),
- group,
- )
- }
-
- pub fn delete_consumer_offset(
- &self,
- group: IggyNamespace,
- consumer_kind: u8,
- consumer_id: u32,
- ) -> Message<RoutedRequestHeader> {
- let (stream_id, topic_id, partition_id) = namespace_ids(group);
- let request = DeleteConsumerOffsetRequest {
- consumer: namespace_consumer(consumer_kind, consumer_id),
- stream_id,
- topic_id,
- partition_id,
- };
- self.build_request_with_namespace(
- Operation::DeleteConsumerOffset,
- &request.to_bytes(),
- group,
- )
- }
-
/// Store offset with explicit `AckLevel`. `NoAck` takes the primary's
/// fast path (no replication); `Quorum` goes through VSR.
///
/// # Panics
/// Panics on payload too large for `Owned::<4096>` or invalid
/// `Message<RoutedRequestHeader>` parse; both are simulator misconfig.
- pub fn store_consumer_offset_2(
+ pub fn store_consumer_offset(
&self,
group: IggyNamespace,
consumer_kind: u8,
@@ -659,7 +616,7 @@
ack: AckLevel,
) -> Message<RoutedRequestHeader> {
let (stream_id, topic_id, partition_id) = namespace_ids(group);
- let request = StoreConsumerOffset2Request {
+ let request = StoreConsumerOffsetRequest {
consumer: namespace_consumer(consumer_kind, consumer_id),
stream_id,
topic_id,
@@ -668,7 +625,7 @@
ack,
};
self.build_request_with_namespace(
- Operation::StoreConsumerOffset2,
+ Operation::StoreConsumerOffset,
&request.to_bytes(),
group,
)
@@ -679,7 +636,7 @@
/// # Panics
/// Panics on payload too large for `Owned::<4096>` or invalid
/// `Message<RoutedRequestHeader>` parse; both are simulator misconfig.
- pub fn delete_consumer_offset_2(
+ pub fn delete_consumer_offset(
&self,
group: IggyNamespace,
consumer_kind: u8,
@@ -687,7 +644,7 @@
ack: AckLevel,
) -> Message<RoutedRequestHeader> {
let (stream_id, topic_id, partition_id) = namespace_ids(group);
- let request = DeleteConsumerOffset2Request {
+ let request = DeleteConsumerOffsetRequest {
consumer: namespace_consumer(consumer_kind, consumer_id),
stream_id,
topic_id,
@@ -695,7 +652,7 @@
ack,
};
self.build_request_with_namespace(
- Operation::DeleteConsumerOffset2,
+ Operation::DeleteConsumerOffset,
&request.to_bytes(),
group,
)
@@ -742,7 +699,7 @@
#[allow(clippy::cast_possible_truncation)]
fn header(&self, operation: Operation, group: u64, total_size: usize) -> RoutedRequestHeader {
RoutedRequestHeader {
- command: iggy_binary_protocol::Command2::Request,
+ command: iggy_binary_protocol::Command::Request,
operation,
size: total_size as u32,
cluster: 0, // TODO: Get from config
diff --git a/core/simulator/src/lib.rs b/core/simulator/src/lib.rs
index 70f24b7..fa5b3e3 100644
--- a/core/simulator/src/lib.rs
+++ b/core/simulator/src/lib.rs
@@ -1674,14 +1674,16 @@
// correctly because `hash % 1 == 0` at one shard per replica.
// Re-locked again when replies stopped echoing a group id (the
// client wire lost its namespace field): the reply-hash tuple
- // dropped that component.
+ // dropped that component. Re-locked when the v1 consumer-offset ops
+ // were removed and the v2 pair became the only store/delete actions:
+ // `Action` lost two variants, shifting discriminants and draw order.
assert_eq!(
- h1, 0xCF1F_BC79_B44A_65F7,
+ h1, 0x14BF_3C3F_41D4_F69D,
"workload reply hash drifted from locked baseline"
);
}
- /// Drive workload with uniform weights across all 25 `Action` variants.
+ /// Drive workload with near-uniform weights across all 23 `Action` variants.
/// Assert it runs without panic and observes at least one reply.
/// Per-op coverage not asserted: some ops can starve the in-flight slot
/// at single-client / 1-slot pipeline limits.
@@ -1720,9 +1722,12 @@
sim.init_partition(ns_b);
sim.register_client_with_primary(&client);
- // 25 variants × 4 = 100.
- assert_eq!(Action::COUNT, 25, "Action::COUNT changed; adjust weights");
- let entries: Vec<(Action, u8)> = Action::iter().map(|a| (a, 4)).collect();
+ // 23 variants: 8 x 5 + 15 x 4 = 100 (weights must sum to 100).
+ assert_eq!(Action::COUNT, 23, "Action::COUNT changed; adjust weights");
+ let entries: Vec<(Action, u8)> = Action::iter()
+ .enumerate()
+ .map(|(index, action)| (action, if index < 8 { 5 } else { 4 }))
+ .collect();
let weights = ActionWeights::new(&entries);
let mut options = WorkloadOptions::new(0xC0FF_EE00, replica_count, vec![ns_a, ns_b]);
@@ -1787,8 +1792,11 @@
sim.init_partition(ns_b);
sim.register_client_with_primary(&client);
- assert_eq!(Action::COUNT, 25, "Action::COUNT changed; adjust weights");
- let entries: Vec<(Action, u8)> = Action::iter().map(|a| (a, 4)).collect();
+ assert_eq!(Action::COUNT, 23, "Action::COUNT changed; adjust weights");
+ let entries: Vec<(Action, u8)> = Action::iter()
+ .enumerate()
+ .map(|(index, action)| (action, if index < 8 { 5 } else { 4 }))
+ .collect();
let mut options = WorkloadOptions::new(0xC0FF_EE00, replica_count, vec![ns_a, ns_b]);
options.weights = ActionWeights::new(&entries);
let mut wl = Workload::new(options);
@@ -2148,7 +2156,7 @@
options.weights = ActionWeights::new(&[
(Action::CreateStream, 5),
(Action::SendMessages, 70),
- (Action::StoreConsumerOffset2, 25),
+ (Action::StoreConsumerOffset, 25),
]);
let mut wl = Workload::new(options);
@@ -2305,7 +2313,7 @@
options.weights = ActionWeights::new(&[
(Action::CreateStream, 5),
(Action::SendMessages, 70),
- (Action::StoreConsumerOffset2, 25),
+ (Action::StoreConsumerOffset, 25),
]);
let mut wl = Workload::new(options);
@@ -3042,7 +3050,7 @@
#[test]
fn per_partition_consensus_independence() {
use consensus::PIPELINE_PREPARE_QUEUE_MAX;
- use iggy_binary_protocol::{Command2, PrepareOkHeader};
+ use iggy_binary_protocol::{Command, PrepareOkHeader};
use packet::Packet;
use std::sync::atomic::{AtomicU64, Ordering};
@@ -3052,7 +3060,7 @@
static BLOCKED_NS: AtomicU64 = AtomicU64::new(0);
fn drop_blocked_prepare_ok(packet: &Packet) -> bool {
- if packet.message.header().command != Command2::PrepareOk {
+ if packet.message.header().command != Command::PrepareOk {
return false;
}
let header: &PrepareOkHeader = bytemuck::checked::from_bytes(
diff --git a/core/simulator/src/packet.rs b/core/simulator/src/packet.rs
index 093615a..921b035 100644
--- a/core/simulator/src/packet.rs
+++ b/core/simulator/src/packet.rs
@@ -23,7 +23,7 @@
//! - Automatic network partitioning with configurable lifecycle
//! - Automatic path clogging with exponential duration
//! - Link capacity limits with random eviction
-//! - Per-command link filtering via `LinkFilter` (`EnumSet<Command2>`)
+//! - Per-command link filtering via `LinkFilter` (`EnumSet<Command>`)
//!
//! Partitions are implemented via per-link `LinkFilter`s. When a link's
//! filter is empty, all packets are silently dropped. When specific commands
@@ -40,18 +40,18 @@
use crate::ready_queue::{Ready, ReadyQueue};
use enumset::EnumSet;
-use iggy_binary_protocol::{Command2, GenericHeader};
+use iggy_binary_protocol::{Command, GenericHeader};
use rand::RngExt;
use rand_xoshiro::Xoshiro256Plus;
use rand_xoshiro::rand_core::SeedableRng;
use server_common::Message;
use std::collections::HashMap;
-/// Per-link command filter. An `EnumSet<Command2>` where:
+/// Per-link command filter. An `EnumSet<Command>` where:
/// - [`ALLOW_ALL`] = all commands pass (link fully enabled)
/// - [`BLOCK_ALL`] = all commands blocked (link fully disabled/partitioned)
/// - Custom sets = only matching commands pass through
-pub type LinkFilter = EnumSet<Command2>;
+pub type LinkFilter = EnumSet<Command>;
/// Link filter that allows all commands through (link fully enabled).
pub const ALLOW_ALL: LinkFilter = EnumSet::all();
@@ -432,7 +432,7 @@
}
/// Returns a mutable reference to the link's filter.
- /// This is the per-link command filter — `EnumSet<Command2>`.
+ /// This is the per-link command filter — `EnumSet<Command>`.
/// Set to [`BLOCK_ALL`] to block all packets (partition).
/// Set to [`ALLOW_ALL`] to allow all packets (default).
/// Remove specific commands to selectively filter.
@@ -787,7 +787,7 @@
}
#[allow(clippy::cast_possible_truncation)]
- fn create_test_message_with_command(command: Command2) -> Message<GenericHeader> {
+ fn create_test_message_with_command(command: Command) -> Message<GenericHeader> {
let size = std::mem::size_of::<GenericHeader>();
let mut buf = vec![0u8; size];
let header: &mut GenericHeader =
@@ -1030,11 +1030,11 @@
// Set filter to only allow Ping on link 0->1
let filter = sim.link_filter(ProcessId::Replica(0), ProcessId::Replica(1));
- *filter = EnumSet::only(Command2::Ping);
+ *filter = EnumSet::only(Command::Ping);
// Submit a Ping message and a Prepare message
- let ping_msg = create_test_message_with_command(Command2::Ping);
- let prepare_msg = create_test_message_with_command(Command2::Prepare);
+ let ping_msg = create_test_message_with_command(Command::Ping);
+ let prepare_msg = create_test_message_with_command(Command::Prepare);
sim.submit(ProcessId::Replica(0), ProcessId::Replica(1), ping_msg);
sim.submit(ProcessId::Replica(0), ProcessId::Replica(1), prepare_msg);
@@ -1048,7 +1048,7 @@
// Only the Ping should be delivered
assert_eq!(delivered.len(), 1);
- assert_eq!(delivered[0].message.header().command, Command2::Ping);
+ assert_eq!(delivered[0].message.header().command, Command::Ping);
// Nothing left in flight
assert_eq!(sim.packets_in_flight(), 0);
diff --git a/core/simulator/src/workload/actions.rs b/core/simulator/src/workload/actions.rs
index e0592da..f830bbe 100644
--- a/core/simulator/src/workload/actions.rs
+++ b/core/simulator/src/workload/actions.rs
@@ -27,7 +27,7 @@
// DO NOT REORDER (hash baseline depends on these indices).
CreateStream,
SendMessages,
- StoreConsumerOffset2,
+ StoreConsumerOffset,
DeleteStream,
UpdateStream,
PurgeStream,
@@ -47,9 +47,7 @@
UpdatePermissions,
CreatePersonalAccessToken,
DeletePersonalAccessToken,
- StoreConsumerOffset,
DeleteConsumerOffset,
- DeleteConsumerOffset2,
}
// Lock the discriminants of the first three variants. Inserting a new variant before
@@ -58,5 +56,5 @@
const _: () = {
assert!(Action::CreateStream as u8 == 0);
assert!(Action::SendMessages as u8 == 1);
- assert!(Action::StoreConsumerOffset2 as u8 == 2);
+ assert!(Action::StoreConsumerOffset as u8 == 2);
};
diff --git a/core/simulator/src/workload/ops/delete_consumer_offset.rs b/core/simulator/src/workload/ops/delete_consumer_offset.rs
index c05c796..45e61ae 100644
--- a/core/simulator/src/workload/ops/delete_consumer_offset.rs
+++ b/core/simulator/src/workload/ops/delete_consumer_offset.rs
@@ -15,9 +15,9 @@
// specific language governing permissions and limitations
// under the License.
-//! `DeleteConsumerOffset` op. Live namespace via shadow.
+//! `DeleteConsumerOffset` op. Namespace-routed with `AckLevel`.
-use iggy_binary_protocol::RoutedRequestHeader;
+use iggy_binary_protocol::{AckLevel, RoutedRequestHeader};
use rand::RngExt;
use rand_xoshiro::Xoshiro256Plus;
use server_common::Message;
@@ -33,6 +33,7 @@
pub ns: IggyNamespace,
pub consumer_kind: u8,
pub consumer_id: u32,
+ pub ack: AckLevel,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
@@ -53,10 +54,17 @@
let ns = shadow.pick_namespace(prng)?;
let consumer_kind: u8 = u8::from(prng.random::<bool>());
let consumer_id: u32 = prng.random_range(0..options.consumer_pool_size.max(1));
+ let f: f32 = prng.random();
+ let ack = if f < options.ack_quorum_ratio {
+ AckLevel::Quorum
+ } else {
+ AckLevel::NoAck
+ };
Some(Input {
ns,
consumer_kind,
consumer_id,
+ ack,
})
}
}
@@ -64,7 +72,7 @@
#[must_use]
pub fn build_message(client: &SimClient, input: &Input) -> Message<RoutedRequestHeader> {
- client.delete_consumer_offset(input.ns, input.consumer_kind, input.consumer_id)
+ client.delete_consumer_offset(input.ns, input.consumer_kind, input.consumer_id, input.ack)
}
#[must_use]
diff --git a/core/simulator/src/workload/ops/delete_consumer_offset_2.rs b/core/simulator/src/workload/ops/delete_consumer_offset_2.rs
deleted file mode 100644
index ad2a620..0000000
--- a/core/simulator/src/workload/ops/delete_consumer_offset_2.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-// 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.
-
-//! `DeleteConsumerOffset2` op. Namespace-routed with `AckLevel`.
-
-use iggy_binary_protocol::{AckLevel, RoutedRequestHeader};
-use rand::RngExt;
-use rand_xoshiro::Xoshiro256Plus;
-use server_common::Message;
-use server_common::sharding::IggyNamespace;
-
-use crate::client::SimClient;
-use crate::workload::effect::Effect;
-use crate::workload::options::WorkloadOptions;
-use crate::workload::shadow::Shadow;
-
-#[derive(Debug, Clone)]
-pub struct Input {
- pub ns: IggyNamespace,
- pub consumer_kind: u8,
- pub consumer_id: u32,
- pub ack: AckLevel,
-}
-
-#[derive(Copy, Clone, Eq, PartialEq, Debug)]
-pub enum Outcome {
- Success,
-}
-
-pub const OUTCOMES: &[Outcome] = &[Outcome::Success];
-
-pub fn sample(
- shadow: &mut Shadow,
- outcome: Outcome,
- prng: &mut Xoshiro256Plus,
- options: &WorkloadOptions,
-) -> Option<Input> {
- match outcome {
- Outcome::Success => {
- let ns = shadow.pick_namespace(prng)?;
- let consumer_kind: u8 = u8::from(prng.random::<bool>());
- let consumer_id: u32 = prng.random_range(0..options.consumer_pool_size.max(1));
- let f: f32 = prng.random();
- let ack = if f < options.ack_quorum_ratio {
- AckLevel::Quorum
- } else {
- AckLevel::NoAck
- };
- Some(Input {
- ns,
- consumer_kind,
- consumer_id,
- ack,
- })
- }
- }
-}
-
-#[must_use]
-pub fn build_message(client: &SimClient, input: &Input) -> Message<RoutedRequestHeader> {
- client.delete_consumer_offset_2(input.ns, input.consumer_kind, input.consumer_id, input.ack)
-}
-
-#[must_use]
-pub const fn classify_reply(_code: u32) -> Outcome {
- Outcome::Success
-}
-
-#[must_use]
-pub const fn predicted_effect(input: &Input, outcome: Outcome) -> Effect {
- match outcome {
- Outcome::Success => Effect::OffsetDeleted {
- key: (input.ns, input.consumer_kind, input.consumer_id),
- },
- }
-}
diff --git a/core/simulator/src/workload/ops/mod.rs b/core/simulator/src/workload/ops/mod.rs
index df6ffed..604e110 100644
--- a/core/simulator/src/workload/ops/mod.rs
+++ b/core/simulator/src/workload/ops/mod.rs
@@ -38,7 +38,6 @@
pub mod create_user;
pub mod delete_consumer_group;
pub mod delete_consumer_offset;
-pub mod delete_consumer_offset_2;
pub mod delete_partitions;
pub mod delete_personal_access_token;
pub mod delete_segments;
@@ -49,7 +48,6 @@
pub mod purge_topic;
pub mod send_messages;
pub mod store_consumer_offset;
-pub mod store_consumer_offset_2;
pub mod update_permissions;
pub mod update_stream;
pub mod update_topic;
@@ -172,7 +170,7 @@
// First three positions lock the hash baseline (do not reorder).
CreateStream => create_stream,
SendMessages => send_messages,
- StoreConsumerOffset2 => store_consumer_offset_2,
+ StoreConsumerOffset => store_consumer_offset,
// Append-only; mirrors actions::Action declaration order.
DeleteStream => delete_stream,
UpdateStream => update_stream,
@@ -193,7 +191,5 @@
UpdatePermissions => update_permissions,
CreatePersonalAccessToken => create_personal_access_token,
DeletePersonalAccessToken => delete_personal_access_token,
- StoreConsumerOffset => store_consumer_offset,
DeleteConsumerOffset => delete_consumer_offset,
- DeleteConsumerOffset2 => delete_consumer_offset_2,
}
diff --git a/core/simulator/src/workload/ops/store_consumer_offset.rs b/core/simulator/src/workload/ops/store_consumer_offset.rs
index edefbfc..1f2821b 100644
--- a/core/simulator/src/workload/ops/store_consumer_offset.rs
+++ b/core/simulator/src/workload/ops/store_consumer_offset.rs
@@ -15,10 +15,15 @@
// specific language governing permissions and limitations
// under the License.
-//! `StoreConsumerOffset` op. Pre-`AckLevel` manual encoding. Live
-//! namespace via shadow, fabricated consumer kind/id. Samples Success.
+//! `StoreConsumerOffset` op. Samples only `Success`. PRNG draw order:
+//!
+//! 1. namespace pick
+//! 2. `consumer_kind` boolean draw
+//! 3. `consumer_id` range draw
+//! 4. `offset` range draw
+//! 5. `ack` ratio draw
-use iggy_binary_protocol::RoutedRequestHeader;
+use iggy_binary_protocol::{AckLevel, RoutedRequestHeader};
use rand::RngExt;
use rand_xoshiro::Xoshiro256Plus;
use server_common::Message;
@@ -35,6 +40,7 @@
pub consumer_kind: u8,
pub consumer_id: u32,
pub offset: u64,
+ pub ack: AckLevel,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
@@ -62,11 +68,18 @@
let raw: u64 = prng.random_range(0..options.max_offset.max(1));
let high = shadow.sends_committed(ns).max(1);
let offset = raw % high;
+ let f: f32 = prng.random();
+ let ack = if f < options.ack_quorum_ratio {
+ AckLevel::Quorum
+ } else {
+ AckLevel::NoAck
+ };
Some(Input {
ns,
consumer_kind,
consumer_id,
offset,
+ ack,
})
}
}
@@ -79,6 +92,7 @@
input.consumer_kind,
input.consumer_id,
input.offset,
+ input.ack,
)
}
diff --git a/core/simulator/src/workload/ops/store_consumer_offset_2.rs b/core/simulator/src/workload/ops/store_consumer_offset_2.rs
deleted file mode 100644
index baac692..0000000
--- a/core/simulator/src/workload/ops/store_consumer_offset_2.rs
+++ /dev/null
@@ -1,112 +0,0 @@
-// 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.
-
-//! `StoreConsumerOffset2` op. Samples only `Success`. PRNG draw order:
-//!
-//! 1. namespace pick
-//! 2. `consumer_kind` boolean draw
-//! 3. `consumer_id` range draw
-//! 4. `offset` range draw
-//! 5. `ack` ratio draw
-
-use iggy_binary_protocol::{AckLevel, RoutedRequestHeader};
-use rand::RngExt;
-use rand_xoshiro::Xoshiro256Plus;
-use server_common::Message;
-use server_common::sharding::IggyNamespace;
-
-use crate::client::SimClient;
-use crate::workload::effect::Effect;
-use crate::workload::options::WorkloadOptions;
-use crate::workload::shadow::Shadow;
-
-#[derive(Debug, Clone)]
-pub struct Input {
- pub ns: IggyNamespace,
- pub consumer_kind: u8,
- pub consumer_id: u32,
- pub offset: u64,
- pub ack: AckLevel,
-}
-
-#[derive(Copy, Clone, Eq, PartialEq, Debug)]
-pub enum Outcome {
- Success,
-}
-
-pub const OUTCOMES: &[Outcome] = &[Outcome::Success];
-
-pub fn sample(
- shadow: &mut Shadow,
- outcome: Outcome,
- prng: &mut Xoshiro256Plus,
- options: &WorkloadOptions,
-) -> Option<Input> {
- match outcome {
- Outcome::Success => {
- let ns = shadow.pick_namespace(prng)?;
- let consumer_kind: u8 = u8::from(prng.random::<bool>());
- let consumer_id: u32 = prng.random_range(0..options.consumer_pool_size.max(1));
- // Draw against the configured ceiling, then clamp to committed
- // reality so the offset is reachable. Clamping post-draw keeps
- // the PRNG draw order (and determinism hash baseline) intact
- // while staying valid once the server validates offsets.
- let raw: u64 = prng.random_range(0..options.max_offset.max(1));
- let high = shadow.sends_committed(ns).max(1);
- let offset = raw % high;
- let f: f32 = prng.random();
- let ack = if f < options.ack_quorum_ratio {
- AckLevel::Quorum
- } else {
- AckLevel::NoAck
- };
- Some(Input {
- ns,
- consumer_kind,
- consumer_id,
- offset,
- ack,
- })
- }
- }
-}
-
-#[must_use]
-pub fn build_message(client: &SimClient, input: &Input) -> Message<RoutedRequestHeader> {
- client.store_consumer_offset_2(
- input.ns,
- input.consumer_kind,
- input.consumer_id,
- input.offset,
- input.ack,
- )
-}
-
-#[must_use]
-pub const fn classify_reply(_code: u32) -> Outcome {
- Outcome::Success
-}
-
-#[must_use]
-pub const fn predicted_effect(input: &Input, outcome: Outcome) -> Effect {
- match outcome {
- Outcome::Success => Effect::OffsetStored {
- key: (input.ns, input.consumer_kind, input.consumer_id),
- value: input.offset,
- },
- }
-}
diff --git a/core/simulator/src/workload/options.rs b/core/simulator/src/workload/options.rs
index a6c0bbe..31ab544 100644
--- a/core/simulator/src/workload/options.rs
+++ b/core/simulator/src/workload/options.rs
@@ -58,7 +58,7 @@
Self::new(&[
(Action::CreateStream, 5),
(Action::SendMessages, 70),
- (Action::StoreConsumerOffset2, 25),
+ (Action::StoreConsumerOffset, 25),
])
}
}
@@ -80,7 +80,7 @@
/// Send-batch size = `batch_size_min + prng.range(batch_size_span)`.
pub batch_size_min: u32,
pub batch_size_span: u32,
- /// Probability a `StoreConsumerOffset2` request uses `Quorum` vs `NoAck`.
+ /// Probability a `StoreConsumerOffset` request uses `Quorum` vs `NoAck`.
pub ack_quorum_ratio: f32,
/// Probability a request targets a non-primary replica (exercises the
/// redirect / forward path).
@@ -88,9 +88,9 @@
/// Probability that a request is intentionally constructed to fail
/// validation. Currently unused; reserved.
pub invalid_request_ratio: f32,
- /// Distinct consumer ids round-robined in `StoreConsumerOffset2`.
+ /// Distinct consumer ids round-robined in `StoreConsumerOffset`.
pub consumer_pool_size: u32,
- /// Upper bound on offset carried by `StoreConsumerOffset2`.
+ /// Upper bound on offset carried by `StoreConsumerOffset`.
pub max_offset: u64,
/// Probability per tick that the driver crashes one live non-primary
/// replica (crash-only, no restart). `0.0` disables injection: the fault
diff --git a/examples/go/go.mod b/examples/go/go.mod
index c41e580..a3db555 100644
--- a/examples/go/go.mod
+++ b/examples/go/go.mod
@@ -10,4 +10,7 @@
github.com/avast/retry-go/v5 v5.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.19.2 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.10 // indirect
+ github.com/zeebo/xxh3 v1.1.0 // indirect
+ golang.org/x/sys v0.30.0 // indirect
)
diff --git a/examples/go/go.sum b/examples/go/go.sum
index 54cad5a..438f23b 100644
--- a/examples/go/go.sum
+++ b/examples/go/go.sum
@@ -8,9 +8,17 @@
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
+github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
+github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
+github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/examples/python/uv.lock b/examples/python/uv.lock
index 137bd9a..ec009c7 100644
--- a/examples/python/uv.lock
+++ b/examples/python/uv.lock
@@ -8,7 +8,7 @@
[[package]]
name = "apache-iggy"
-version = "0.9.0.dev3"
+version = "0.9.0.dev4"
source = { directory = "../../foreign/python" }
[package.metadata]
diff --git a/foreign/csharp/Iggy_SDK/Contracts/Tcp/TcpContracts.cs b/foreign/csharp/Iggy_SDK/Contracts/Tcp/TcpContracts.cs
index 20486f2..b367cc2 100644
--- a/foreign/csharp/Iggy_SDK/Contracts/Tcp/TcpContracts.cs
+++ b/foreign/csharp/Iggy_SDK/Contracts/Tcp/TcpContracts.cs
@@ -17,6 +17,7 @@
using System.Buffers;
using System.Buffers.Binary;
+using System.IO.Hashing;
using System.Runtime.CompilerServices;
using System.Text;
using Apache.Iggy.Contracts.Auth;
@@ -37,6 +38,9 @@
/// <summary>Frames wider than this are built on the heap instead of the stack.</summary>
private const int MaxStackAllocBytes = 1024;
+ /// <summary>Offset mutations always request quorum acknowledgement.</summary>
+ private const byte AckQuorum = 1;
+
internal static byte[] LoginWithPersonalAccessToken(string token)
{
var tokenLength = Encoding.UTF8.GetByteCount(token);
@@ -400,9 +404,19 @@
bytes[position + 18] = autoCommit ? (byte)1 : (byte)0;
}
+ /// <summary>
+ /// Encodes a SendMessages body: routing metadata followed by one canonical batch record
+ /// (a 256-byte batch header plus per-message frames). The server stamps
+ /// <c>partition_id</c>, <c>base_offset</c>, and <c>base_timestamp</c>; they stay zero here.
+ /// </summary>
internal static int CreateMessage(Span<byte> bytes, Identifier streamId, Identifier topicId,
Partitioning partitioning, ReadOnlySpan<Message> messages, IMessageEncryptor? encryptor = null)
{
+ if (messages.IsEmpty)
+ {
+ throw new ArgumentException("Batch must contain at least one message.", nameof(messages));
+ }
+
var metadataLength = 2 + streamId.Length + 2 + topicId.Length + 2 + partitioning.Length + 4;
BinaryPrimitives.WriteInt32LittleEndian(bytes[..4], metadataLength);
bytes.WriteBytesFromStreamAndTopicIdentifiers(streamId, topicId, 4);
@@ -412,10 +426,23 @@
BinaryPrimitives.WriteInt32LittleEndian(bytes[position..(position + 4)], messages.Length);
position += 4;
- var indexPosition = position;
- position += 16 * messages.Length;
+ // The producer owns message ids: a zero id is minted before the frame checksum covers it.
+ var originTimestamp = ulong.MaxValue;
+ foreach (var message in messages)
+ {
+ if (message.Header.Id == 0)
+ {
+ message.Header = message.Header with { Id = Guid.NewGuid().ToUInt128() };
+ }
- var msgSize = 0;
+ originTimestamp = Math.Min(originTimestamp, message.Header.OriginTimestamp);
+ }
+
+ var batchStart = position;
+ bytes[batchStart..(batchStart + BatchWireFormat.BATCH_HEADER_SIZE)].Clear();
+ position += BatchWireFormat.BATCH_HEADER_SIZE;
+ var blobStart = position;
+ var offsetDelta = 0u;
// One scratch buffer reused across the batch (grown on demand); holds plaintext headers, so it is
// returned cleared on every path, including a mid-batch throw from the encryptor or header serialization.
@@ -425,7 +452,16 @@
foreach (var message in messages)
{
var header = message.Header;
- var payloadStart = position + 64;
+ var timestampDelta = header.OriginTimestamp - originTimestamp;
+ if (timestampDelta > uint.MaxValue)
+ {
+ throw new ArgumentException(
+ $"Message origin timestamp runs {timestampDelta} microseconds ahead of the batch's " +
+ $"earliest one; the frame field holds at most {uint.MaxValue}.", nameof(messages));
+ }
+
+ var frameStart = position;
+ var payloadStart = frameStart + BatchWireFormat.FRAME_HEADER_SIZE;
int payloadLength;
if (encryptor is null)
@@ -489,26 +525,19 @@
}
}
- BinaryPrimitives.WriteUInt64LittleEndian(bytes[position..(position + 8)], 0);
- BinaryPrimitives.WriteUInt128LittleEndian(bytes[(position + 8)..(position + 24)], header.Id);
- BinaryPrimitives.WriteUInt64LittleEndian(bytes[(position + 24)..(position + 32)], header.Offset);
- BinaryPrimitives.WriteUInt64LittleEndian(bytes[(position + 32)..(position + 40)],
- DateTimeOffsetUtils.ToUnixTimeMicroSeconds(header.Timestamp));
- BinaryPrimitives.WriteUInt64LittleEndian(bytes[(position + 40)..(position + 48)],
- header.OriginTimestamp);
- BinaryPrimitives.WriteInt32LittleEndian(bytes[(position + 48)..(position + 52)], headersLength);
- BinaryPrimitives.WriteInt32LittleEndian(bytes[(position + 52)..(position + 56)], payloadLength);
+ BinaryPrimitives.WriteUInt128LittleEndian(bytes[(frameStart + 8)..(frameStart + 24)], header.Id);
+ BinaryPrimitives.WriteUInt32LittleEndian(bytes[(frameStart + 24)..(frameStart + 28)], offsetDelta);
+ BinaryPrimitives.WriteUInt32LittleEndian(bytes[(frameStart + 28)..(frameStart + 32)],
+ (uint)timestampDelta);
+ BinaryPrimitives.WriteInt32LittleEndian(bytes[(frameStart + 32)..(frameStart + 36)], headersLength);
+ BinaryPrimitives.WriteInt32LittleEndian(bytes[(frameStart + 36)..(frameStart + 40)], payloadLength);
// Reserved must be zero on the wire; the server rejects non-zero values.
- BinaryPrimitives.WriteUInt64LittleEndian(bytes[(position + 56)..(position + 64)], 0);
+ BinaryPrimitives.WriteUInt64LittleEndian(bytes[(frameStart + 40)..(frameStart + 48)], 0);
- position += 64 + payloadLength + headersLength;
-
- msgSize += 64 + payloadLength + headersLength;
-
- BinaryPrimitives.WriteInt32LittleEndian(bytes[indexPosition..(indexPosition + 4)], 0);
- BinaryPrimitives.WriteInt32LittleEndian(bytes[(indexPosition + 4)..(indexPosition + 8)], msgSize);
- BinaryPrimitives.WriteInt64LittleEndian(bytes[(indexPosition + 8)..(indexPosition + 16)], 0);
- indexPosition += 16;
+ position = headersStart + headersLength;
+ var frameChecksum = XxHash3.HashToUInt64(bytes[(frameStart + 8)..position]);
+ BinaryPrimitives.WriteUInt64LittleEndian(bytes[frameStart..(frameStart + 8)], frameChecksum);
+ offsetDelta++;
}
}
finally
@@ -519,9 +548,38 @@
}
}
+ var batchLength = (ulong)(BatchWireFormat.BATCH_HEADER_SIZE + (position - blobStart));
+ BinaryPrimitives.WriteUInt64LittleEndian(bytes[(batchStart + 24)..(batchStart + 32)], originTimestamp);
+ BinaryPrimitives.WriteUInt64LittleEndian(bytes[(batchStart + 32)..(batchStart + 40)], batchLength);
+ BinaryPrimitives.WriteUInt32LittleEndian(bytes[(batchStart + 48)..(batchStart + 52)], (uint)messages.Length);
+ var batchChecksum = CalculateBatchChecksum(bytes, batchStart, blobStart, position);
+ BinaryPrimitives.WriteUInt64LittleEndian(bytes[(batchStart + 40)..(batchStart + 48)], batchChecksum);
+
return position;
}
+ /// <summary>
+ /// Batch checksum: XXH3-64 over the batch header meta fields followed by each frame's stored
+ /// 8-byte checksum field in message order. Bodies are bound transitively through the
+ /// per-frame checksums. The header fields must already be backpatched into <paramref name="bytes" />.
+ /// </summary>
+ private static ulong CalculateBatchChecksum(ReadOnlySpan<byte> bytes, int batchStart, int blobStart, int blobEnd)
+ {
+ var hasher = new XxHash3();
+ hasher.Append(bytes.Slice(batchStart, 40));
+ hasher.Append(bytes.Slice(batchStart + 48, 4));
+ var cursor = blobStart;
+ while (cursor < blobEnd)
+ {
+ hasher.Append(bytes.Slice(cursor, 8));
+ var headersLength = BinaryPrimitives.ReadInt32LittleEndian(bytes[(cursor + 32)..(cursor + 36)]);
+ var payloadLength = BinaryPrimitives.ReadInt32LittleEndian(bytes[(cursor + 36)..(cursor + 40)]);
+ cursor += BatchWireFormat.FRAME_HEADER_SIZE + payloadLength + headersLength;
+ }
+
+ return hasher.GetCurrentHashAsUInt64();
+ }
+
internal static int HeadersByteLength(Dictionary<HeaderKey, HeaderValue>? headers)
{
if (headers is null)
@@ -810,7 +868,7 @@
uint? partitionId)
{
Span<byte> bytes =
- stackalloc byte[2 + streamId.Length + 2 + topicId.Length + 13 + 1 + 2 + consumer.ConsumerId.Length];
+ stackalloc byte[2 + streamId.Length + 2 + topicId.Length + 14 + 1 + 2 + consumer.ConsumerId.Length];
bytes[0] = GetConsumerTypeByte(consumer.Type);
bytes.WriteBytesFromIdentifier(consumer.ConsumerId, 1);
var position = 1 + consumer.ConsumerId.Length + 2;
@@ -830,6 +888,7 @@
}
BinaryPrimitives.WriteUInt64LittleEndian(bytes[(position + 5)..(position + 13)], offset);
+ bytes[position + 13] = AckQuorum;
return bytes.ToArray();
}
@@ -933,7 +992,7 @@
internal static byte[] DeleteOffset(Identifier streamId, Identifier topicId, Consumer consumer, uint? partitionId)
{
Span<byte> bytes =
- stackalloc byte[2 + streamId.Length + 2 + topicId.Length + 5 + 1 + 2 + consumer.ConsumerId.Length];
+ stackalloc byte[2 + streamId.Length + 2 + topicId.Length + 6 + 1 + 2 + consumer.ConsumerId.Length];
bytes[0] = GetConsumerTypeByte(consumer.Type);
bytes.WriteBytesFromIdentifier(consumer.ConsumerId, 1);
var position = 1 + consumer.ConsumerId.Length + 2;
@@ -952,6 +1011,7 @@
BinaryPrimitives.WriteUInt32LittleEndian(bytes[(position + 1)..(position + 5)], 0); // Padding
}
+ bytes[position + 5] = AckQuorum;
return bytes.ToArray();
}
}
diff --git a/foreign/csharp/Iggy_SDK/IggyClient/Implementations/TcpMessageStream.Vsr.cs b/foreign/csharp/Iggy_SDK/IggyClient/Implementations/TcpMessageStream.Vsr.cs
index f70a3b2..1885c69 100644
--- a/foreign/csharp/Iggy_SDK/IggyClient/Implementations/TcpMessageStream.Vsr.cs
+++ b/foreign/csharp/Iggy_SDK/IggyClient/Implementations/TcpMessageStream.Vsr.cs
@@ -725,7 +725,7 @@
await ReadExactVsrAsync(stream, _vsrReplyHeaderBuffer, readCancellation.Token, token);
var command = VsrHeader.PeekCommand(_vsrReplyHeaderBuffer);
- if (command == Command2.Eviction)
+ if (command == Command.Eviction)
{
var eviction = VsrHeader.ReadEviction(_vsrReplyHeaderBuffer);
_logger.LogWarning("Consensus session evicted by the server: {Reason}", eviction.Reason);
@@ -734,7 +734,7 @@
throw new VsrSessionEvictedException(VsrReplyDecoder.ToException(eviction));
}
- if (command != Command2.Reply)
+ if (command != Command.Reply)
{
// Neither a reply nor an eviction: this frame was never an answer to the outstanding request, so
// whatever the peer does send for it would be read as the next request's reply and handed to the
diff --git a/foreign/csharp/Iggy_SDK/Iggy_SDK.csproj b/foreign/csharp/Iggy_SDK/Iggy_SDK.csproj
index c0fe627..6a22ab0 100644
--- a/foreign/csharp/Iggy_SDK/Iggy_SDK.csproj
+++ b/foreign/csharp/Iggy_SDK/Iggy_SDK.csproj
@@ -27,7 +27,7 @@
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<AssemblyName>Apache.Iggy</AssemblyName>
<RootNamespace>Apache.Iggy</RootNamespace>
- <Version>0.9.0-edge.3</Version>
+ <Version>0.9.0-edge.4</Version>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/foreign/csharp/Iggy_SDK/Mappers/BinaryMapper.cs b/foreign/csharp/Iggy_SDK/Mappers/BinaryMapper.cs
index 5b443fc..51fb8d8 100644
--- a/foreign/csharp/Iggy_SDK/Mappers/BinaryMapper.cs
+++ b/foreign/csharp/Iggy_SDK/Mappers/BinaryMapper.cs
@@ -33,8 +33,6 @@
internal static class BinaryMapper
{
- private const int PropertiesSize = 64;
-
internal static RawPersonalAccessToken MapRawPersonalAccessToken(ReadOnlySpan<byte> payload)
{
var tokenLength = payload[0];
@@ -376,87 +374,90 @@
}
}
- var maxMessages = (length - 16) / PropertiesSize;
+ var maxMessages = (length - 16) / BatchWireFormat.FRAME_HEADER_SIZE;
var capacity = (int)Math.Min(messagesCount, (uint)maxMessages);
List<RentedMessageResponse> messages = new(capacity);
while (position < length)
{
- if (!TryReadFrameLengths(span, length, position, out var headersLength, out var payloadLength))
+ var batchEnd = ReadBatchExtent(span, length, position, out var baseOffset, out var baseTimestamp,
+ out var batchOriginTimestamp);
+ // Broker append time is stamped once per batch record; the per-frame delta applies to the
+ // origin timestamp only.
+ var timestamp = DateTimeOffsetUtils.FromUnixTimeMicroSeconds(baseTimestamp);
+ var cursor = position + BatchWireFormat.BATCH_HEADER_SIZE;
+ while (cursor < batchEnd)
{
- break;
- }
+ ReadFrameLengths(span, cursor, batchEnd, out var headersLength, out var payloadLength);
- var checksum = BinaryPrimitives.ReadUInt64LittleEndian(span[position..(position + 8)]);
- var id = BinaryPrimitives.ReadUInt128LittleEndian(span[(position + 8)..(position + 24)]);
- var offset = BinaryPrimitives.ReadUInt64LittleEndian(span[(position + 24)..(position + 32)]);
- var timestamp = BinaryPrimitives.ReadUInt64LittleEndian(span[(position + 32)..(position + 40)]);
- var originTimestamp = BinaryPrimitives.ReadUInt64LittleEndian(span[(position + 40)..(position + 48)]);
- var reserved = BinaryPrimitives.ReadUInt64LittleEndian(span[(position + 56)..(position + 64)]);
+ var checksum = BinaryPrimitives.ReadUInt64LittleEndian(span[cursor..(cursor + 8)]);
+ var id = BinaryPrimitives.ReadUInt128LittleEndian(span[(cursor + 8)..(cursor + 24)]);
+ var offsetDelta = BinaryPrimitives.ReadUInt32LittleEndian(span[(cursor + 24)..(cursor + 28)]);
+ var timestampDelta = BinaryPrimitives.ReadUInt32LittleEndian(span[(cursor + 28)..(cursor + 32)]);
+ var offset = baseOffset + offsetDelta;
- var payloadRangeStart = position + 64;
- var headersRangeStart = payloadRangeStart + payloadLength;
+ var payloadRangeStart = cursor + BatchWireFormat.FRAME_HEADER_SIZE;
+ var headersRangeStart = payloadRangeStart + payloadLength;
- ReadOnlyMemory<byte> payloadSlice = payload.Slice(payloadRangeStart, payloadLength);
- ReadOnlyMemory<byte> rawHeaders = headersLength > 0
- ? payload.Slice(headersRangeStart, headersLength)
- : ReadOnlyMemory<byte>.Empty;
+ ReadOnlyMemory<byte> payloadSlice = payload.Slice(payloadRangeStart, payloadLength);
+ ReadOnlyMemory<byte> rawHeaders = headersLength > 0
+ ? payload.Slice(headersRangeStart, headersLength)
+ : ReadOnlyMemory<byte>.Empty;
- // Decrypt into the shared buffer so the message looks like plaintext downstream. Wire lengths
- // still drive the cursor advance; only the decrypted lengths land on the header.
- var storedPayloadLength = payloadLength;
- var storedHeadersLength = headersLength;
- if (encryptor is not null)
- {
- try
+ // Decrypt into the shared buffer so the message looks like plaintext downstream. Wire lengths
+ // still drive the cursor advance; only the decrypted lengths land on the header.
+ var storedPayloadLength = payloadLength;
+ var storedHeadersLength = headersLength;
+ if (encryptor is not null)
{
- // Bound each destination to this message's reserved slice so an encryptor that overruns
- // its contract fails fast here instead of corrupting the next message's region.
- Memory<byte> payloadDest =
- plaintext.Slice(plainCursor, encryptor.GetMaxDecryptedLength(payloadLength));
- var writtenPayload = encryptor.Decrypt(payloadSlice.Span, payloadDest.Span);
- payloadSlice = payloadDest.Slice(0, writtenPayload);
- storedPayloadLength = writtenPayload;
- plainCursor += writtenPayload;
-
- if (!rawHeaders.IsEmpty)
+ try
{
- Memory<byte> headersDest =
- plaintext.Slice(plainCursor, encryptor.GetMaxDecryptedLength(headersLength));
- var writtenHeaders = encryptor.Decrypt(rawHeaders.Span, headersDest.Span);
- rawHeaders = headersDest.Slice(0, writtenHeaders);
- storedHeadersLength = writtenHeaders;
- plainCursor += writtenHeaders;
+ // Bound each destination to this message's reserved slice so an encryptor that overruns
+ // its contract fails fast here instead of corrupting the next message's region.
+ Memory<byte> payloadDest =
+ plaintext.Slice(plainCursor, encryptor.GetMaxDecryptedLength(payloadLength));
+ var writtenPayload = encryptor.Decrypt(payloadSlice.Span, payloadDest.Span);
+ payloadSlice = payloadDest.Slice(0, writtenPayload);
+ storedPayloadLength = writtenPayload;
+ plainCursor += writtenPayload;
+
+ if (!rawHeaders.IsEmpty)
+ {
+ Memory<byte> headersDest =
+ plaintext.Slice(plainCursor, encryptor.GetMaxDecryptedLength(headersLength));
+ var writtenHeaders = encryptor.Decrypt(rawHeaders.Span, headersDest.Span);
+ rawHeaders = headersDest.Slice(0, writtenHeaders);
+ storedHeadersLength = writtenHeaders;
+ plainCursor += writtenHeaders;
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new MessageDecryptionException(offset, (uint)partitionId, ex);
}
}
- catch (Exception ex)
+
+ messages.Add(new RentedMessageResponse
{
- throw new MessageDecryptionException(offset, (uint)partitionId, ex);
- }
+ Header = new MessageHeader
+ {
+ Checksum = checksum,
+ Id = id,
+ Offset = offset,
+ OriginTimestamp = batchOriginTimestamp + timestampDelta,
+ PayloadLength = storedPayloadLength,
+ Timestamp = timestamp,
+ UserHeadersLength = storedHeadersLength,
+ Reserved = 0
+ },
+ RawUserHeaders = rawHeaders,
+ Payload = payloadSlice
+ });
+
+ cursor = headersRangeStart + headersLength;
}
- messages.Add(new RentedMessageResponse
- {
- Header = new MessageHeader
- {
- Checksum = checksum,
- Id = id,
- Offset = offset,
- OriginTimestamp = originTimestamp,
- PayloadLength = storedPayloadLength,
- Timestamp = DateTimeOffsetUtils.FromUnixTimeMicroSeconds(timestamp),
- UserHeadersLength = storedHeadersLength,
- Reserved = reserved
- },
- RawUserHeaders = rawHeaders,
- Payload = payloadSlice
- });
-
- position += 64 + payloadLength + headersLength;
- if (position + PropertiesSize >= length)
- {
- break;
- }
+ position = batchEnd;
}
return new PolledMessagesRental(payloadOwner, plaintextOwner)
@@ -474,60 +475,84 @@
}
// Shared by the decrypt sizing pre-pass and the main map loop so both agree on which frames are included;
- // drift would mis-size the shared plaintext buffer. Returns false at buffer end or on a frame running past
- // the buffer; throws on a negative length so a poison frame surfaces instead of being re-polled forever.
- private static bool TryReadFrameLengths(ReadOnlySpan<byte> span, int length, int position,
- out int headersLength, out int payloadLength)
+ // drift would mis-size the shared plaintext buffer.
+ private static int ReadBatchExtent(ReadOnlySpan<byte> span, int length, int position, out ulong baseOffset,
+ out ulong baseTimestamp, out ulong originTimestamp)
{
- headersLength = 0;
- payloadLength = 0;
- if (position + PropertiesSize > length)
+ if (position + BatchWireFormat.BATCH_HEADER_SIZE > length)
{
- return false;
+ throw new MalformedResponseException(
+ $"Malformed batch record at byte {position}: {length - position} bytes cannot hold a batch header.");
}
- headersLength = BinaryPrimitives.ReadInt32LittleEndian(span[(position + 48)..(position + 52)]);
- payloadLength = BinaryPrimitives.ReadInt32LittleEndian(span[(position + 52)..(position + 56)]);
+ baseOffset = BinaryPrimitives.ReadUInt64LittleEndian(span[(position + 8)..(position + 16)]);
+ baseTimestamp = BinaryPrimitives.ReadUInt64LittleEndian(span[(position + 16)..(position + 24)]);
+ originTimestamp = BinaryPrimitives.ReadUInt64LittleEndian(span[(position + 24)..(position + 32)]);
+ var batchLength = BinaryPrimitives.ReadUInt64LittleEndian(span[(position + 32)..(position + 40)]);
+ if (batchLength < BatchWireFormat.BATCH_HEADER_SIZE || (ulong)position + batchLength > (ulong)length)
+ {
+ throw new MalformedResponseException(
+ $"Malformed batch record at byte {position}: batch length {batchLength} does not fit the response.");
+ }
+
+ return position + (int)batchLength;
+ }
+
+ private static void ReadFrameLengths(ReadOnlySpan<byte> span, int cursor, int batchEnd,
+ out int headersLength, out int payloadLength)
+ {
+ if (cursor + BatchWireFormat.FRAME_HEADER_SIZE > batchEnd)
+ {
+ throw new MalformedResponseException(
+ $"Malformed message frame at byte {cursor}: {batchEnd - cursor} bytes cannot hold a frame header.");
+ }
+
+ headersLength = BinaryPrimitives.ReadInt32LittleEndian(span[(cursor + 32)..(cursor + 36)]);
+ payloadLength = BinaryPrimitives.ReadInt32LittleEndian(span[(cursor + 36)..(cursor + 40)]);
if (headersLength < 0 || payloadLength < 0)
{
throw new MalformedResponseException(
- $"Malformed message frame at byte {position}: negative payload ({payloadLength}) or header " +
+ $"Malformed message frame at byte {cursor}: negative payload ({payloadLength}) or header " +
$"({headersLength}) length.");
}
- // Overflow-safe: server-controlled lengths can approach int.MaxValue, so compute the bound in long.
- if ((long)position + 64 + payloadLength + headersLength > length)
+ if (BinaryPrimitives.ReadUInt64LittleEndian(span[(cursor + 40)..(cursor + 48)]) != 0)
{
- return false;
+ throw new MalformedResponseException(
+ $"Malformed message frame at byte {cursor}: reserved bytes must be zero.");
}
- return true;
+ // Overflow-safe: server-controlled lengths can approach int.MaxValue, so compute the bound in long.
+ if ((long)cursor + BatchWireFormat.FRAME_HEADER_SIZE + payloadLength + headersLength > batchEnd)
+ {
+ throw new MalformedResponseException(
+ $"Malformed message frame at byte {cursor}: frame runs past its batch record.");
+ }
}
- // Pre-pass summing upper-bound plaintext length so the shared buffer is rented exactly once. Same
- // TryReadFrameLengths walk as the main loop, so both agree on which messages are included.
+ // Pre-pass summing upper-bound plaintext length so the shared buffer is rented exactly once. Same batch
+ // and frame walk as the main loop, so both agree on which messages are included.
private static int SumMaxDecryptedLength(ReadOnlySpan<byte> span, int length, IMessageEncryptor encryptor)
{
var position = 16;
var total = 0;
while (position < length)
{
- if (!TryReadFrameLengths(span, length, position, out var headersLength, out var payloadLength))
+ var batchEnd = ReadBatchExtent(span, length, position, out _, out _, out _);
+ var cursor = position + BatchWireFormat.BATCH_HEADER_SIZE;
+ while (cursor < batchEnd)
{
- break;
+ ReadFrameLengths(span, cursor, batchEnd, out var headersLength, out var payloadLength);
+ total += encryptor.GetMaxDecryptedLength(payloadLength);
+ if (headersLength > 0)
+ {
+ total += encryptor.GetMaxDecryptedLength(headersLength);
+ }
+
+ cursor += BatchWireFormat.FRAME_HEADER_SIZE + payloadLength + headersLength;
}
- total += encryptor.GetMaxDecryptedLength(payloadLength);
- if (headersLength > 0)
- {
- total += encryptor.GetMaxDecryptedLength(headersLength);
- }
-
- position += 64 + payloadLength + headersLength;
- if (position + PropertiesSize >= length)
- {
- break;
- }
+ position = batchEnd;
}
return total;
diff --git a/foreign/csharp/Iggy_SDK/Vsr/Command2.cs b/foreign/csharp/Iggy_SDK/Messages/BatchWireFormat.cs
similarity index 67%
copy from foreign/csharp/Iggy_SDK/Vsr/Command2.cs
copy to foreign/csharp/Iggy_SDK/Messages/BatchWireFormat.cs
index c8f7a76..b67c33d 100644
--- a/foreign/csharp/Iggy_SDK/Vsr/Command2.cs
+++ b/foreign/csharp/Iggy_SDK/Messages/BatchWireFormat.cs
@@ -15,16 +15,16 @@
// specific language governing permissions and limitations
// under the License.
-namespace Apache.Iggy.Vsr;
+namespace Apache.Iggy.Messages;
/// <summary>
-/// VSR frame discriminant, byte 60 of every consensus header. Only the frames a client emits or
-/// receives are named; every other discriminant decodes as <see cref="Reserved" />.
+/// Sizes of the canonical message batch record shared by send requests and poll responses:
+/// a 256-byte batch header followed by per-message frames of
+/// <c>[48-byte frame header][payload][user headers]</c>.
/// </summary>
-internal enum Command2 : byte
+internal static class BatchWireFormat
{
- Reserved = 0,
- Request = 5,
- Reply = 8,
- Eviction = 13
+ internal const int BATCH_HEADER_SIZE = 256;
+
+ internal const int FRAME_HEADER_SIZE = 48;
}
diff --git a/foreign/csharp/Iggy_SDK/Utils/CommandCodes.cs b/foreign/csharp/Iggy_SDK/Utils/CommandCodes.cs
index 71e945a..067e097 100644
--- a/foreign/csharp/Iggy_SDK/Utils/CommandCodes.cs
+++ b/foreign/csharp/Iggy_SDK/Utils/CommandCodes.cs
@@ -48,8 +48,6 @@
internal const int GET_CONSUMER_OFFSET_CODE = 120;
internal const int STORE_CONSUMER_OFFSET_CODE = 121;
internal const int DELETE_CONSUMER_OFFSET_CODE = 122;
- internal const int STORE_CONSUMER_OFFSET_2_CODE = 123;
- internal const int DELETE_CONSUMER_OFFSET_2_CODE = 124;
internal const int GET_STREAM_CODE = 200;
internal const int GET_STREAMS_CODE = 201;
internal const int CREATE_STREAM_CODE = 202;
diff --git a/foreign/csharp/Iggy_SDK/Utils/TcpMessageStreamHelpers.cs b/foreign/csharp/Iggy_SDK/Utils/TcpMessageStreamHelpers.cs
index 6f0dc13..49b6ffe 100644
--- a/foreign/csharp/Iggy_SDK/Utils/TcpMessageStreamHelpers.cs
+++ b/foreign/csharp/Iggy_SDK/Utils/TcpMessageStreamHelpers.cs
@@ -46,7 +46,7 @@
internal static int CalculateMessageBytesCount(ReadOnlySpan<Message> messages, IMessageEncryptor? encryptor)
{
- var bytesCount = 0;
+ var bytesCount = BatchWireFormat.BATCH_HEADER_SIZE;
foreach (var message in messages)
{
var payloadLength = message.Payload.Length;
@@ -63,7 +63,7 @@
}
}
- bytesCount += 16 + 64 + payloadLength + headersLength;
+ bytesCount += BatchWireFormat.FRAME_HEADER_SIZE + payloadLength + headersLength;
}
return bytesCount;
diff --git a/foreign/csharp/Iggy_SDK/Vsr/Command2.cs b/foreign/csharp/Iggy_SDK/Vsr/Command.cs
similarity index 97%
rename from foreign/csharp/Iggy_SDK/Vsr/Command2.cs
rename to foreign/csharp/Iggy_SDK/Vsr/Command.cs
index c8f7a76..a124cc8 100644
--- a/foreign/csharp/Iggy_SDK/Vsr/Command2.cs
+++ b/foreign/csharp/Iggy_SDK/Vsr/Command.cs
@@ -21,7 +21,7 @@
/// VSR frame discriminant, byte 60 of every consensus header. Only the frames a client emits or
/// receives are named; every other discriminant decodes as <see cref="Reserved" />.
/// </summary>
-internal enum Command2 : byte
+internal enum Command : byte
{
Reserved = 0,
Request = 5,
diff --git a/foreign/csharp/Iggy_SDK/Vsr/VsrHeader.cs b/foreign/csharp/Iggy_SDK/Vsr/VsrHeader.cs
index ad58e0c..3de6211 100644
--- a/foreign/csharp/Iggy_SDK/Vsr/VsrHeader.cs
+++ b/foreign/csharp/Iggy_SDK/Vsr/VsrHeader.cs
@@ -81,7 +81,7 @@
var totalSize = HEADER_SIZE + payload.Length;
BinaryPrimitives.WriteUInt32LittleEndian(header[SIZE_OFFSET..], (uint)totalSize);
- header[COMMAND_OFFSET] = (byte)Command2.Request;
+ header[COMMAND_OFFSET] = (byte)Command.Request;
WriteUInt128(header[REQUEST_CLIENT_OFFSET..], frame.ClientId);
BinaryPrimitives.WriteUInt64LittleEndian(header[REQUEST_TIMESTAMP_OFFSET..], 0);
BinaryPrimitives.WriteUInt64LittleEndian(header[REQUEST_ID_OFFSET..], frame.RequestId);
@@ -96,13 +96,13 @@
return totalSize;
}
- internal static Command2 PeekCommand(ReadOnlySpan<byte> header)
+ internal static Command PeekCommand(ReadOnlySpan<byte> header)
{
return header[COMMAND_OFFSET] switch
{
- (byte)Command2.Reply => Command2.Reply,
- (byte)Command2.Eviction => Command2.Eviction,
- _ => Command2.Reserved
+ (byte)Command.Reply => Command.Reply,
+ (byte)Command.Eviction => Command.Eviction,
+ _ => Command.Reserved
};
}
diff --git a/foreign/csharp/Iggy_SDK/Vsr/VsrOperation.cs b/foreign/csharp/Iggy_SDK/Vsr/VsrOperation.cs
index 5653a23..f46eeca 100644
--- a/foreign/csharp/Iggy_SDK/Vsr/VsrOperation.cs
+++ b/foreign/csharp/Iggy_SDK/Vsr/VsrOperation.cs
@@ -61,9 +61,7 @@
SendMessages = 160,
StoreConsumerOffset = 161,
- DeleteConsumerOffset = 162,
- StoreConsumerOffset2 = 164,
- DeleteConsumerOffset2 = 165
+ DeleteConsumerOffset = 162
}
internal static class VsrOperations
@@ -128,8 +126,6 @@
CommandCodes.SEND_MESSAGES_CODE => VsrOperation.SendMessages,
CommandCodes.STORE_CONSUMER_OFFSET_CODE => VsrOperation.StoreConsumerOffset,
CommandCodes.DELETE_CONSUMER_OFFSET_CODE => VsrOperation.DeleteConsumerOffset,
- CommandCodes.STORE_CONSUMER_OFFSET_2_CODE => VsrOperation.StoreConsumerOffset2,
- CommandCodes.DELETE_CONSUMER_OFFSET_2_CODE => VsrOperation.DeleteConsumerOffset2,
CommandCodes.CREATE_STREAM_CODE => VsrOperation.CreateStream,
CommandCodes.DELETE_STREAM_CODE => VsrOperation.DeleteStream,
CommandCodes.UPDATE_STREAM_CODE => VsrOperation.UpdateStream,
@@ -170,8 +166,7 @@
// overwrite, on a plane that keeps no client table to dedup against, so a replay lands on the same
// value. Denying the retry here reports an unknown outcome for a blip on an offset commit, which
// takes down the consume loop over a write that was safe to repeat.
- if (operation is VsrOperation.StoreConsumerOffset or VsrOperation.StoreConsumerOffset2
- or VsrOperation.DeleteConsumerOffset or VsrOperation.DeleteConsumerOffset2)
+ if (operation is VsrOperation.StoreConsumerOffset or VsrOperation.DeleteConsumerOffset)
{
return true;
}
@@ -206,8 +201,8 @@
true,
>= VsrOperation.CreateTopicWithAssignments and <= VsrOperation.TruncatePartition => true,
>= VsrOperation.CreateStream and <= VsrOperation.LeaveConsumerGroup => true,
- VsrOperation.SendMessages or VsrOperation.StoreConsumerOffset or VsrOperation.DeleteConsumerOffset
- or VsrOperation.StoreConsumerOffset2 or VsrOperation.DeleteConsumerOffset2 => true,
+ VsrOperation.SendMessages or VsrOperation.StoreConsumerOffset or VsrOperation.DeleteConsumerOffset =>
+ true,
_ => false
};
}
@@ -268,8 +263,6 @@
internal static bool IsResultFramed(this VsrOperation operation)
{
return operation.IsMetadata() || operation is VsrOperation.StoreConsumerOffset
- or VsrOperation.StoreConsumerOffset2
- or VsrOperation.DeleteConsumerOffset
- or VsrOperation.DeleteConsumerOffset2;
+ or VsrOperation.DeleteConsumerOffset;
}
}
diff --git a/foreign/csharp/Iggy_SDK/Vsr/VsrReplyDecoder.cs b/foreign/csharp/Iggy_SDK/Vsr/VsrReplyDecoder.cs
index efb2130..72a5e10 100644
--- a/foreign/csharp/Iggy_SDK/Vsr/VsrReplyDecoder.cs
+++ b/foreign/csharp/Iggy_SDK/Vsr/VsrReplyDecoder.cs
@@ -42,9 +42,9 @@
switch (VsrHeader.PeekCommand(header))
{
- case Command2.Eviction:
+ case Command.Eviction:
throw ToException(VsrHeader.ReadEviction(header));
- case Command2.Reply:
+ case Command.Reply:
break;
default:
throw VsrError.Exception(VsrError.INVALID_COMMAND,
diff --git a/foreign/csharp/Iggy_SDK_Tests/ContractsTests/MessageBatchGoldenVectorTests.cs b/foreign/csharp/Iggy_SDK_Tests/ContractsTests/MessageBatchGoldenVectorTests.cs
new file mode 100644
index 0000000..da54ff9
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK_Tests/ContractsTests/MessageBatchGoldenVectorTests.cs
@@ -0,0 +1,182 @@
+// 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.
+
+using System.Buffers.Binary;
+using Apache.Iggy.Contracts.Tcp;
+using Apache.Iggy.Extensions;
+using Apache.Iggy.IggyClient.Implementations;
+using Apache.Iggy.Kinds;
+using Apache.Iggy.Messages;
+using Apache.Iggy.Utils;
+
+namespace Apache.Iggy.Tests.ContractsTests;
+
+/// <summary>
+/// Cross-SDK golden vectors for the canonical message batch, produced by the Rust encoder
+/// (core/binary_protocol/src/requests/messages/send_messages.rs). These bytes are the
+/// contract: a change to the batch layout has to break every SDK's copy of them together.
+///
+/// The produce vector is the SendMessages body for stream 1, topic 2, balanced partitioning,
+/// and two messages: {id 7, origin timestamp 1000, payload "first-payload"} and
+/// {id 8, origin timestamp 1050, payload "second-payload", user headers "user-header-bytes"}.
+/// The poll vector serves that batch back stamped partition 3, base offset 100,
+/// base timestamp 5000, current offset 101.
+/// </summary>
+public sealed class MessageBatchGoldenVectorTests
+{
+ private const string ProduceBodyFull =
+ "12000000010401000000010402000000010002000000000000000000000000000000000000000000000000000000e803" +
+ "0000000000008c01000000000000a91f38c86307267c0200000000000000000000000000000000000000000000000000" +
+ "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "0000000000000000000000000000000000000000000000000000000000000000000000000000bfd2b9205a7596750700" +
+ "00000000000000000000000000000000000000000000000000000d000000000000000000000066697273742d7061796c" +
+ "6f6164d66b7e1c758eb7c0080000000000000000000000000000000100000032000000110000000e0000000000000000" +
+ "0000007365636f6e642d7061796c6f6164757365722d6865616465722d6279746573";
+
+ private const string ProduceBatchOnly =
+ "000000000000000000000000000000000000000000000000e8030000000000008c01000000000000a91f38c86307267c" +
+ "020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "00000000000000000000000000000000bfd2b9205a759675070000000000000000000000000000000000000000000000" +
+ "000000000d000000000000000000000066697273742d7061796c6f6164d66b7e1c758eb7c00800000000000000000000" +
+ "00000000000100000032000000110000000e00000000000000000000007365636f6e642d7061796c6f6164757365722d" +
+ "6865616465722d6279746573";
+
+ private const string PollBody =
+ "03000000650000000000000002000000030000000000000064000000000000008813000000000000e803000000000000" +
+ "8c01000000000000c96826b38a8feed20200000000000000000000000000000000000000000000000000000000000000" +
+ "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "0000000000000000000000000000000000000000000000000000000000000000bfd2b9205a7596750700000000000000" +
+ "00000000000000000000000000000000000000000d000000000000000000000066697273742d7061796c6f6164d66b7e" +
+ "1c758eb7c0080000000000000000000000000000000100000032000000110000000e0000000000000000000000736563" +
+ "6f6e642d7061796c6f6164757365722d6865616465722d6279746573";
+
+ [Fact]
+ public void CreateMessage_EncodesTheProduceGoldenVector()
+ {
+ var expected = Convert.FromHexString(ProduceBodyFull);
+ var messages = GoldenMessages();
+ var streamId = Identifier.Numeric(1);
+ var topicId = Identifier.Numeric(2);
+ var partitioning = Partitioning.None();
+
+ var buffer = new byte[TcpMessageStreamHelpers.CalculateMessageBytesCount(messages, null)
+ + 2 + streamId.Length + 2 + topicId.Length + 2 + partitioning.Length + 4 + 4];
+ var written = TcpContracts.CreateMessage(buffer, streamId, topicId, partitioning, messages);
+
+ Assert.Equal(expected.Length, written);
+ Assert.Equal(expected, buffer.AsSpan(0, written).ToArray());
+
+ var metadataLength = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(0, 4));
+ Assert.Equal(Convert.FromHexString(ProduceBatchOnly),
+ buffer.AsSpan(4 + metadataLength, written - 4 - metadataLength).ToArray());
+ }
+
+ [Fact]
+ public void CreateMessage_MintsAZeroMessageIdBeforeEncoding()
+ {
+ var message = new Message(UInt128.Zero, "payload"u8.ToArray());
+ var streamId = Identifier.Numeric(1);
+ var topicId = Identifier.Numeric(2);
+ var partitioning = Partitioning.None();
+
+ var buffer = new byte[TcpMessageStreamHelpers.CalculateMessageBytesCount([message], null)
+ + 2 + streamId.Length + 2 + topicId.Length + 2 + partitioning.Length + 4 + 4];
+ var written = TcpContracts.CreateMessage(buffer, streamId, topicId, partitioning, [message]);
+
+ Assert.NotEqual(UInt128.Zero, message.Header.Id);
+ var metadataLength = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(0, 4));
+ var frameStart = 4 + metadataLength + 256;
+ var wireId = BinaryPrimitives.ReadUInt128LittleEndian(buffer.AsSpan(frameStart + 8, 16));
+ Assert.Equal(message.Header.Id, wireId);
+ Assert.Equal(buffer.Length, written);
+ }
+
+ [Fact]
+ public void CreateMessage_EmptyBatch_Throws()
+ {
+ Assert.Throws<ArgumentException>(() => TcpContracts.CreateMessage(new byte[512], Identifier.Numeric(1),
+ Identifier.Numeric(2), Partitioning.None(), ReadOnlySpan<Message>.Empty));
+ }
+
+ [Fact]
+ public void CreateMessage_TimestampDeltaOverflow_Throws()
+ {
+ var messages = new[]
+ {
+ new Message(new UInt128(0, 1), "a"u8.ToArray()),
+ new Message(new UInt128(0, 2), "b"u8.ToArray())
+ };
+ messages[1].Header = messages[1].Header with { OriginTimestamp = (ulong)uint.MaxValue + 1 };
+
+ Assert.Throws<ArgumentException>(() => TcpContracts.CreateMessage(new byte[1024], Identifier.Numeric(1),
+ Identifier.Numeric(2), Partitioning.None(), messages));
+ }
+
+ [Fact]
+ public void MapRentedMessages_DecodesThePollGoldenVector()
+ {
+ var pollBody = Convert.FromHexString(PollBody);
+
+ using var rental =
+ Mappers.BinaryMapper.MapRentedMessages(pollBody, TcpMessageStream.EmptyMemoryOwner.Instance);
+
+ Assert.Equal(3, rental.PartitionId);
+ Assert.Equal(101ul, rental.CurrentOffset);
+ Assert.Equal(2, rental.Messages.Count);
+
+ var first = rental.Messages[0];
+ Assert.Equal(100ul, first.Header.Offset);
+ Assert.Equal(DateTimeOffsetUtils.FromUnixTimeMicroSeconds(5000), first.Header.Timestamp);
+ Assert.Equal(1000ul, first.Header.OriginTimestamp);
+ Assert.Equal(new UInt128(0, 7), first.Header.Id);
+ Assert.Equal("first-payload"u8.ToArray(), first.Payload.ToArray());
+ Assert.True(first.RawUserHeaders.IsEmpty);
+ Assert.Equal(BinaryPrimitives.ReadUInt64LittleEndian(pollBody.AsSpan(16 + 256, 8)), first.Header.Checksum);
+
+ var second = rental.Messages[1];
+ Assert.Equal(101ul, second.Header.Offset);
+ Assert.Equal(DateTimeOffsetUtils.FromUnixTimeMicroSeconds(5000), second.Header.Timestamp);
+ Assert.Equal(1050ul, second.Header.OriginTimestamp);
+ Assert.Equal(new UInt128(0, 8), second.Header.Id);
+ Assert.Equal("second-payload"u8.ToArray(), second.Payload.ToArray());
+ Assert.Equal("user-header-bytes"u8.ToArray(), second.RawUserHeaders.ToArray());
+ Assert.Equal(
+ BinaryPrimitives.ReadUInt64LittleEndian(pollBody.AsSpan(16 + 256 + 48 + "first-payload"u8.Length, 8)),
+ second.Header.Checksum);
+ }
+
+ private static Message[] GoldenMessages()
+ {
+ var messages = new[]
+ {
+ new Message(new UInt128(0, 7), "first-payload"u8.ToArray()),
+ new Message(new UInt128(0, 8), "second-payload"u8.ToArray())
+ };
+ messages[0].Header = messages[0].Header with { OriginTimestamp = 1000 };
+ messages[1].Header = messages[1].Header with { OriginTimestamp = 1050 };
+ messages[1].RawUserHeaders = "user-header-bytes"u8.ToArray();
+
+ return messages;
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK_Tests/MapperTests/BinaryMapper.cs b/foreign/csharp/Iggy_SDK_Tests/MapperTests/BinaryMapper.cs
index 0324871..d86b018 100644
--- a/foreign/csharp/Iggy_SDK_Tests/MapperTests/BinaryMapper.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/MapperTests/BinaryMapper.cs
@@ -79,17 +79,15 @@
public void MapMessages_NoHeaders_ReturnsValidMessageResponses()
{
// Arrange
- var (offset, timestamp, guid, headersLength, checkSum, payload) = MessageFactory.CreateMessageResponseFields();
- var msgOnePayload = BinaryFactory.CreateMessagePayload(offset, timestamp, 0, checkSum,
- guid, payload);
- var (offset1, timestamp1, guid1, headersLength2, checkSum2, payload1)
- = MessageFactory.CreateMessageResponseFields();
- var msgTwoPayload = BinaryFactory.CreateMessagePayload(offset1, timestamp1, 0, checkSum2,
- guid1, payload1);
+ var (offset, timestamp, guid, _, checkSum, payload) = MessageFactory.CreateMessageResponseFields();
+ var msgOneFrame = BinaryFactory.CreateMessageFrame(checkSum, guid, 0, 0, [], payload);
+ var (_, _, guid1, _, checkSum2, payload1) = MessageFactory.CreateMessageResponseFields();
+ var msgTwoFrame = BinaryFactory.CreateMessageFrame(checkSum2, guid1, 1, 5, [], payload1);
+ var record = BinaryFactory.CreateBatchRecord(offset, timestamp, timestamp, msgOneFrame, msgTwoFrame);
- var combinedPayload = new byte[16 + msgOnePayload.Length + msgTwoPayload.Length];
- msgOnePayload.CopyTo(combinedPayload.AsSpan(16));
- msgTwoPayload.CopyTo(combinedPayload.AsSpan(16 + msgOnePayload.Length));
+ var combinedPayload = new byte[16 + record.Length];
+ BinaryPrimitives.WriteUInt32LittleEndian(combinedPayload.AsSpan(12, 4), 2);
+ record.CopyTo(combinedPayload.AsSpan(16));
// Act
var responses
@@ -101,9 +99,13 @@
var response1 = responses.Messages.ElementAt(0);
Assert.Equal(payload, response1.Payload);
+ Assert.Equal(offset, response1.Header.Offset);
+ Assert.Equal(timestamp, response1.Header.OriginTimestamp);
var response2 = responses.Messages.ElementAt(1);
Assert.Equal(payload1, response2.Payload);
+ Assert.Equal(offset + 1, response2.Header.Offset);
+ Assert.Equal(timestamp + 5, response2.Header.OriginTimestamp);
}
[Fact]
@@ -435,15 +437,15 @@
var headers1 = "first-secret-headers"u8.ToArray();
var payload2 = "second-secret-payload"u8.ToArray();
- var frame1 = BuildEncryptedFrame(encryptor, 100, payload1, headers1);
- var frame2 = BuildEncryptedFrame(encryptor, 101, payload2, ReadOnlySpan<byte>.Empty);
+ var frame1 = BuildEncryptedFrame(encryptor, 0, payload1, headers1);
+ var frame2 = BuildEncryptedFrame(encryptor, 1, payload2, ReadOnlySpan<byte>.Empty);
+ var record = BinaryFactory.CreateBatchRecord(100, 12345, 12345, frame1, frame2);
- var combined = new byte[16 + frame1.Length + frame2.Length];
+ var combined = new byte[16 + record.Length];
BinaryPrimitives.WriteInt32LittleEndian(combined.AsSpan(0, 4), 7);
BinaryPrimitives.WriteUInt64LittleEndian(combined.AsSpan(4, 8), 101);
BinaryPrimitives.WriteUInt32LittleEndian(combined.AsSpan(12, 4), 2);
- frame1.CopyTo(combined.AsSpan(16));
- frame2.CopyTo(combined.AsSpan(16 + frame1.Length));
+ record.CopyTo(combined.AsSpan(16));
using var rental = Mappers.BinaryMapper.MapRentedMessages(combined, TcpMessageStream.EmptyMemoryOwner.Instance,
encryptor);
@@ -475,33 +477,50 @@
{
var encryptor = new AesMessageEncryptor(AesMessageEncryptor.GenerateKey());
- var frame = new byte[64];
- BinaryPrimitives.WriteInt32LittleEndian(frame.AsSpan(48, 4), 0); // headersLength
- BinaryPrimitives.WriteInt32LittleEndian(frame.AsSpan(52, 4), -64); // payloadLength
+ var frame = new byte[48];
+ BinaryPrimitives.WriteInt32LittleEndian(frame.AsSpan(32, 4), 0); // headersLength
+ BinaryPrimitives.WriteInt32LittleEndian(frame.AsSpan(36, 4), -48); // payloadLength
+ var record = BinaryFactory.CreateBatchRecord(1, 12345, 12345, frame);
- var combined = new byte[16 + frame.Length];
+ var combined = new byte[16 + record.Length];
BinaryPrimitives.WriteInt32LittleEndian(combined.AsSpan(0, 4), 7);
BinaryPrimitives.WriteUInt64LittleEndian(combined.AsSpan(4, 8), 1);
BinaryPrimitives.WriteUInt32LittleEndian(combined.AsSpan(12, 4), 1);
- frame.CopyTo(combined.AsSpan(16));
+ record.CopyTo(combined.AsSpan(16));
Assert.Throws<MalformedResponseException>(() =>
Mappers.BinaryMapper.MapRentedMessages(combined, TcpMessageStream.EmptyMemoryOwner.Instance, encryptor));
}
[Fact]
+ public void MapRentedMessages_NonzeroFrameReserved_Throws()
+ {
+ var frame = BinaryFactory.CreateMessageFrame(0, Guid.NewGuid(), 0, 0, [], "payload"u8);
+ BinaryPrimitives.WriteUInt64LittleEndian(frame.AsSpan(40, 8), 1);
+ var record = BinaryFactory.CreateBatchRecord(1, 12345, 12345, frame);
+
+ var combined = new byte[16 + record.Length];
+ BinaryPrimitives.WriteUInt32LittleEndian(combined.AsSpan(12, 4), 1);
+ record.CopyTo(combined.AsSpan(16));
+
+ Assert.Throws<MalformedResponseException>(() =>
+ Mappers.BinaryMapper.MapRentedMessages(combined, TcpMessageStream.EmptyMemoryOwner.Instance));
+ }
+
+ [Fact]
public void MapRentedMessages_WithEncryptor_TamperedCiphertext_ThrowsMessageDecryptionException()
{
var encryptor = new AesMessageEncryptor(AesMessageEncryptor.GenerateKey());
- var frame = BuildEncryptedFrame(encryptor, 42, "secret-payload"u8, ReadOnlySpan<byte>.Empty);
+ var frame = BuildEncryptedFrame(encryptor, 0, "secret-payload"u8, ReadOnlySpan<byte>.Empty);
- frame[64 + 12] ^= 0xFF;
+ frame[48 + 12] ^= 0xFF;
+ var record = BinaryFactory.CreateBatchRecord(42, 12345, 12345, frame);
- var combined = new byte[16 + frame.Length];
+ var combined = new byte[16 + record.Length];
BinaryPrimitives.WriteInt32LittleEndian(combined.AsSpan(0, 4), 7);
BinaryPrimitives.WriteUInt64LittleEndian(combined.AsSpan(4, 8), 42);
BinaryPrimitives.WriteUInt32LittleEndian(combined.AsSpan(12, 4), 1);
- frame.CopyTo(combined.AsSpan(16));
+ record.CopyTo(combined.AsSpan(16));
var ex = Assert.Throws<MessageDecryptionException>(() =>
Mappers.BinaryMapper.MapRentedMessages(combined, TcpMessageStream.EmptyMemoryOwner.Instance, encryptor));
@@ -587,25 +606,12 @@
Assert.Throws<InvalidResponseException>(() => Mappers.BinaryMapper.MapSendMessages(payload));
}
- private static byte[] BuildEncryptedFrame(AesMessageEncryptor encryptor, ulong offset,
+ private static byte[] BuildEncryptedFrame(AesMessageEncryptor encryptor, uint offsetDelta,
ReadOnlySpan<byte> plainPayload, ReadOnlySpan<byte> plainHeaders)
{
var cipherPayload = encryptor.EncryptToArray(plainPayload);
var cipherHeaders = plainHeaders.Length > 0 ? encryptor.EncryptToArray(plainHeaders) : [];
- var frame = new byte[64 + cipherPayload.Length + cipherHeaders.Length];
- Span<byte> span = frame.AsSpan();
- BinaryPrimitives.WriteUInt64LittleEndian(span[..8], 0);
- BinaryPrimitives.WriteUInt128LittleEndian(span[8..24], Guid.NewGuid().ToUInt128());
- BinaryPrimitives.WriteUInt64LittleEndian(span[24..32], offset);
- BinaryPrimitives.WriteUInt64LittleEndian(span[32..40], 12345);
- BinaryPrimitives.WriteUInt64LittleEndian(span[40..48], 12345);
- BinaryPrimitives.WriteInt32LittleEndian(span[48..52], cipherHeaders.Length);
- BinaryPrimitives.WriteInt32LittleEndian(span[52..56], cipherPayload.Length);
- BinaryPrimitives.WriteUInt64LittleEndian(span[56..64], 0);
- cipherPayload.CopyTo(span[64..]);
- cipherHeaders.CopyTo(span[(64 + cipherPayload.Length)..]);
-
- return frame;
+ return BinaryFactory.CreateMessageFrame(0, Guid.NewGuid(), offsetDelta, 0, cipherHeaders, cipherPayload);
}
}
diff --git a/foreign/csharp/Iggy_SDK_Tests/PublisherTests/MessageEncryptionTests.cs b/foreign/csharp/Iggy_SDK_Tests/PublisherTests/MessageEncryptionTests.cs
index 7e64a2b..2302817 100644
--- a/foreign/csharp/Iggy_SDK_Tests/PublisherTests/MessageEncryptionTests.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/PublisherTests/MessageEncryptionTests.cs
@@ -151,21 +151,21 @@
Span<byte> span = buffer.AsSpan(0, written);
var metadataLength = BinaryPrimitives.ReadInt32LittleEndian(span[..4]);
- var position = 4 + metadataLength + 16 * messages.Length;
+ var position = 4 + metadataLength + 256;
- var firstPayloadLength = BinaryPrimitives.ReadInt32LittleEndian(span[(position + 52)..(position + 56)]);
+ var firstPayloadLength = BinaryPrimitives.ReadInt32LittleEndian(span[(position + 36)..(position + 40)]);
Assert.Equal(encryptor.GetMaxEncryptedLength("first-payload"u8.Length), firstPayloadLength);
Assert.Equal("first-payload"u8.ToArray(),
- encryptor.DecryptToArray(span.Slice(position + 64, firstPayloadLength)));
+ encryptor.DecryptToArray(span.Slice(position + 48, firstPayloadLength)));
- position += 64 + firstPayloadLength;
- var secondHeadersLength = BinaryPrimitives.ReadInt32LittleEndian(span[(position + 48)..(position + 52)]);
- var secondPayloadLength = BinaryPrimitives.ReadInt32LittleEndian(span[(position + 52)..(position + 56)]);
+ position += 48 + firstPayloadLength;
+ var secondHeadersLength = BinaryPrimitives.ReadInt32LittleEndian(span[(position + 32)..(position + 36)]);
+ var secondPayloadLength = BinaryPrimitives.ReadInt32LittleEndian(span[(position + 36)..(position + 40)]);
Assert.Equal("second-payload"u8.ToArray(),
- encryptor.DecryptToArray(span.Slice(position + 64, secondPayloadLength)));
+ encryptor.DecryptToArray(span.Slice(position + 48, secondPayloadLength)));
var plainHeaders = encryptor.DecryptToArray(
- span.Slice(position + 64 + secondPayloadLength, secondHeadersLength));
+ span.Slice(position + 48 + secondPayloadLength, secondHeadersLength));
Assert.Single(BinaryMapper.MapHeaders(plainHeaders));
}
}
diff --git a/foreign/csharp/Iggy_SDK_Tests/Utils/BinaryFactory.cs b/foreign/csharp/Iggy_SDK_Tests/Utils/BinaryFactory.cs
index 5204326..d0c6078 100644
--- a/foreign/csharp/Iggy_SDK_Tests/Utils/BinaryFactory.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/Utils/BinaryFactory.cs
@@ -42,25 +42,50 @@
return payload;
}
- internal static byte[] CreateMessagePayload(ulong offset, ulong timestamp, int headersLength, uint checkSum,
- Guid guid, ReadOnlySpan<byte> payload)
+ internal static byte[] CreateMessageFrame(ulong checksum, Guid guid, uint offsetDelta, uint timestampDelta,
+ ReadOnlySpan<byte> userHeaders, ReadOnlySpan<byte> payload)
{
- var messageLength = payload.Length;
- var totalSize = 64 + payload.Length;
- Span<byte> payloadBuffer = new byte[totalSize].AsSpan();
+ Span<byte> frame = new byte[48 + payload.Length + userHeaders.Length].AsSpan();
- BinaryPrimitives.WriteUInt64LittleEndian(payloadBuffer[..8], checkSum);
- BinaryPrimitives.WriteUInt128LittleEndian(payloadBuffer[8..24], guid.ToUInt128());
- BinaryPrimitives.WriteUInt64LittleEndian(payloadBuffer[24..32], offset);
- BinaryPrimitives.WriteUInt64LittleEndian(payloadBuffer[32..+40], timestamp);
- BinaryPrimitives.WriteUInt64LittleEndian(payloadBuffer[40..48], timestamp);
- BinaryPrimitives.WriteInt32LittleEndian(payloadBuffer[48..52], headersLength);
- BinaryPrimitives.WriteInt32LittleEndian(payloadBuffer[52..56], payload.Length);
- BinaryPrimitives.WriteUInt64LittleEndian(payloadBuffer[56..64], 0); // reserved
+ BinaryPrimitives.WriteUInt64LittleEndian(frame[..8], checksum);
+ BinaryPrimitives.WriteUInt128LittleEndian(frame[8..24], guid.ToUInt128());
+ BinaryPrimitives.WriteUInt32LittleEndian(frame[24..28], offsetDelta);
+ BinaryPrimitives.WriteUInt32LittleEndian(frame[28..32], timestampDelta);
+ BinaryPrimitives.WriteUInt32LittleEndian(frame[32..36], (uint)userHeaders.Length);
+ BinaryPrimitives.WriteUInt32LittleEndian(frame[36..40], (uint)payload.Length);
+ BinaryPrimitives.WriteUInt64LittleEndian(frame[40..48], 0); // reserved
- payload.CopyTo(payloadBuffer[64..(64 + messageLength)]);
+ payload.CopyTo(frame[48..(48 + payload.Length)]);
+ userHeaders.CopyTo(frame[(48 + payload.Length)..]);
- return payloadBuffer.ToArray();
+ return frame.ToArray();
+ }
+
+ /// <summary>
+ /// One batch record: a 256-byte batch header followed by the given frames. The batch checksum is
+ /// left zero; the poll decoder does not verify it.
+ /// </summary>
+ internal static byte[] CreateBatchRecord(ulong baseOffset, ulong baseTimestamp, ulong originTimestamp,
+ params byte[][] frames)
+ {
+ var blobLength = frames.Sum(frame => frame.Length);
+ var record = new byte[256 + blobLength];
+ Span<byte> header = record.AsSpan(0, 256);
+
+ BinaryPrimitives.WriteUInt64LittleEndian(header[8..16], baseOffset);
+ BinaryPrimitives.WriteUInt64LittleEndian(header[16..24], baseTimestamp);
+ BinaryPrimitives.WriteUInt64LittleEndian(header[24..32], originTimestamp);
+ BinaryPrimitives.WriteUInt64LittleEndian(header[32..40], (ulong)record.Length);
+ BinaryPrimitives.WriteUInt32LittleEndian(header[48..52], (uint)frames.Length);
+
+ var position = 256;
+ foreach (var frame in frames)
+ {
+ frame.CopyTo(record.AsSpan(position));
+ position += frame.Length;
+ }
+
+ return record;
}
internal static byte[] CreateStreamPayload(uint id, int topicsCount, string name, ulong sizeBytes,
diff --git a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrHeaderTests.cs b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrHeaderTests.cs
index 73eb1fb..2780370 100644
--- a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrHeaderTests.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrHeaderTests.cs
@@ -81,7 +81,7 @@
Assert.Equal(VsrHeader.HEADER_SIZE + payload.Length, totalSize);
Assert.Equal((uint)totalSize, ReadUInt32(header, VsrHeader.SIZE_OFFSET));
- Assert.Equal((byte)Command2.Request, header[VsrHeader.COMMAND_OFFSET]);
+ Assert.Equal((byte)Command.Request, header[VsrHeader.COMMAND_OFFSET]);
Assert.Equal((byte)VsrOperation.Register, header[VsrHeader.REQUEST_OPERATION_OFFSET]);
Assert.Equal(0UL, ReadUInt64(header, VsrHeader.REQUEST_ID_OFFSET));
Assert.Equal(0UL, ReadUInt64(header, VsrHeader.REQUEST_SESSION_OFFSET));
@@ -213,14 +213,14 @@
{
var header = new byte[VsrHeader.HEADER_SIZE];
- header[VsrHeader.COMMAND_OFFSET] = (byte)Command2.Reply;
- Assert.Equal(Command2.Reply, VsrHeader.PeekCommand(header));
+ header[VsrHeader.COMMAND_OFFSET] = (byte)Command.Reply;
+ Assert.Equal(Command.Reply, VsrHeader.PeekCommand(header));
- header[VsrHeader.COMMAND_OFFSET] = (byte)Command2.Eviction;
- Assert.Equal(Command2.Eviction, VsrHeader.PeekCommand(header));
+ header[VsrHeader.COMMAND_OFFSET] = (byte)Command.Eviction;
+ Assert.Equal(Command.Eviction, VsrHeader.PeekCommand(header));
header[VsrHeader.COMMAND_OFFSET] = 6;
- Assert.Equal(Command2.Reserved, VsrHeader.PeekCommand(header));
+ Assert.Equal(Command.Reserved, VsrHeader.PeekCommand(header));
}
[Fact]
diff --git a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrOperationTests.cs b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrOperationTests.cs
index bd5686e..6888db3 100644
--- a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrOperationTests.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrOperationTests.cs
@@ -35,8 +35,6 @@
[InlineData(CommandCodes.SEND_MESSAGES_CODE, (byte)VsrOperation.SendMessages)]
[InlineData(CommandCodes.STORE_CONSUMER_OFFSET_CODE, (byte)VsrOperation.StoreConsumerOffset)]
[InlineData(CommandCodes.DELETE_CONSUMER_OFFSET_CODE, (byte)VsrOperation.DeleteConsumerOffset)]
- [InlineData(CommandCodes.STORE_CONSUMER_OFFSET_2_CODE, (byte)VsrOperation.StoreConsumerOffset2)]
- [InlineData(CommandCodes.DELETE_CONSUMER_OFFSET_2_CODE, (byte)VsrOperation.DeleteConsumerOffset2)]
[InlineData(CommandCodes.CREATE_STREAM_CODE, (byte)VsrOperation.CreateStream)]
[InlineData(CommandCodes.DELETE_STREAM_CODE, (byte)VsrOperation.DeleteStream)]
[InlineData(CommandCodes.UPDATE_STREAM_CODE, (byte)VsrOperation.UpdateStream)]
@@ -110,7 +108,7 @@
{
Assert.True(VsrOperation.CreateStream.IsResultFramed());
Assert.True(VsrOperation.StoreConsumerOffset.IsResultFramed());
- Assert.True(VsrOperation.DeleteConsumerOffset2.IsResultFramed());
+ Assert.True(VsrOperation.DeleteConsumerOffset.IsResultFramed());
Assert.False(VsrOperation.SendMessages.IsResultFramed());
Assert.False(VsrOperation.NonReplicated.IsResultFramed());
Assert.False(VsrOperation.Register.IsResultFramed());
@@ -122,6 +120,8 @@
{
Assert.True(VsrOperations.IsKnown((byte)VsrOperation.SendMessages));
Assert.False(VsrOperations.IsKnown(163));
+ Assert.False(VsrOperations.IsKnown(164));
+ Assert.False(VsrOperations.IsKnown(165));
Assert.False(VsrOperations.IsKnown(200));
}
@@ -156,7 +156,7 @@
[Theory]
[InlineData((byte)VsrOperation.SendMessages)]
[InlineData((byte)VsrOperation.StoreConsumerOffset)]
- [InlineData((byte)VsrOperation.DeleteConsumerOffset2)]
+ [InlineData((byte)VsrOperation.DeleteConsumerOffset)]
[InlineData((byte)VsrOperation.DeleteSegments)]
[InlineData((byte)VsrOperation.NonReplicated)]
[InlineData((byte)VsrOperation.Logout)]
diff --git a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrProtocolDriftTests.cs b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrProtocolDriftTests.cs
index afc9a6b..2fa5bcc 100644
--- a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrProtocolDriftTests.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrProtocolDriftTests.cs
@@ -128,11 +128,11 @@
}
[Fact]
- public void Command2Discriminants_MatchTheRustEnum()
+ public void CommandDiscriminants_MatchTheRustEnum()
{
- IReadOnlyDictionary<string, int> rust = RustEnum.Discriminants(ReadRustSource(CommandPath), "Command2");
+ IReadOnlyDictionary<string, int> rust = RustEnum.Discriminants(ReadRustSource(CommandPath), "Command");
- AssertSubsetMatches<Command2>(rust);
+ AssertSubsetMatches<Command>(rust);
}
[Fact]
@@ -723,7 +723,7 @@
["u64"] = (8, 8),
["u128"] = (16, 16),
// Every enum the headers embed is `#[repr(u8)]`.
- ["Command2"] = (1, 1),
+ ["Command"] = (1, 1),
["Operation"] = (1, 1),
["EvictionReason"] = (1, 1)
};
diff --git a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrReplyDecoderTests.cs b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrReplyDecoderTests.cs
index a6707b8..c778817 100644
--- a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrReplyDecoderTests.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrReplyDecoderTests.cs
@@ -26,7 +26,7 @@
private static byte[] ReplyHeader(VsrOperation operation, int bodyLength, uint status = 0)
{
var header = new byte[VsrHeader.HEADER_SIZE];
- header[VsrHeader.COMMAND_OFFSET] = (byte)Command2.Reply;
+ header[VsrHeader.COMMAND_OFFSET] = (byte)Command.Reply;
header[VsrHeader.REPLY_OPERATION_OFFSET] = (byte)operation;
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(VsrHeader.SIZE_OFFSET),
(uint)(VsrHeader.HEADER_SIZE + bodyLength));
@@ -38,7 +38,7 @@
private static byte[] EvictionHeader(EvictionReason reason, uint version = 0, uint versionMin = 0)
{
var header = new byte[VsrHeader.HEADER_SIZE];
- header[VsrHeader.COMMAND_OFFSET] = (byte)Command2.Eviction;
+ header[VsrHeader.COMMAND_OFFSET] = (byte)Command.Eviction;
header[VsrHeader.EVICTION_REASON_OFFSET] = (byte)reason;
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(VsrHeader.EVICTION_PROTOCOL_VERSION_OFFSET), version);
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(VsrHeader.EVICTION_PROTOCOL_VERSION_MIN_OFFSET),
diff --git a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrTestPayloads.cs b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrTestPayloads.cs
index 1e49147..c7c18d9 100644
--- a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrTestPayloads.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrTestPayloads.cs
@@ -71,7 +71,9 @@
BinaryPrimitives.WriteUInt32LittleEndian(partition.AsSpan(1), partitionId.Value);
}
- return Concat([1], NumericIdentifier(1), streamId, topicId, partition);
+ byte[] ack = [1];
+
+ return Concat([1], NumericIdentifier(1), streamId, topicId, partition, ack);
}
internal static byte[] DeleteSegments(byte[] streamId, byte[] topicId, uint partitionId, uint segmentsCount = 1)
diff --git a/foreign/go/binary_serialization/binary_response_deserializer.go b/foreign/go/binary_serialization/binary_response_deserializer.go
index ef6803f..04abe22 100644
--- a/foreign/go/binary_serialization/binary_response_deserializer.go
+++ b/foreign/go/binary_serialization/binary_response_deserializer.go
@@ -26,6 +26,7 @@
iggcon "github.com/apache/iggy/foreign/go/contracts"
ierror "github.com/apache/iggy/foreign/go/errors"
+ "github.com/apache/iggy/foreign/go/internal/batch"
"github.com/klauspost/compress/s2"
)
@@ -161,13 +162,16 @@
return options, consumed, nil
}
-// pollBatchHeaderLength covers [partition_id u32][current_offset u64][count u32].
-const pollBatchHeaderLength = 16
+// pollPrefixLength covers [partition_id u32][current_offset u64][count u32].
+const pollPrefixLength = 16
-// DeserializeFetchMessagesResponse decodes a poll reply. A truncated body is
-// a decode error rather than a shorter batch: silently dropping the tail
-// would let a consumer that commits CurrentOffset skip messages it never saw.
-// The returned messages alias the reply buffer; a retained message pins it.
+// DeserializeFetchMessagesResponse decodes a poll reply: the 16-byte prefix
+// followed by batch records ([256-byte batch header][frames]) walked by their
+// batch length, with each frame's deltas resolved to absolute values. A
+// truncated body is a decode error rather than a shorter batch: silently
+// dropping the tail would let a consumer that commits CurrentOffset skip
+// messages it never saw. The returned messages alias the reply buffer; a
+// retained message pins it.
func DeserializeFetchMessagesResponse(payload []byte, compression iggcon.IggyMessageCompression) (*iggcon.PolledMessage, error) {
if len(payload) == 0 {
return &iggcon.PolledMessage{
@@ -178,63 +182,80 @@
}
length := len(payload)
- if length < pollBatchHeaderLength {
- return nil, fmt.Errorf("poll response: %d bytes is short of the batch header", length)
+ if length < pollPrefixLength {
+ return nil, fmt.Errorf("poll response: %d bytes is short of the reply prefix", length)
}
partitionId := binary.LittleEndian.Uint32(payload[0:4])
currentOffset := binary.LittleEndian.Uint64(payload[4:12])
messagesCount := binary.LittleEndian.Uint32(payload[12:16])
- position := pollBatchHeaderLength
+ position := pollPrefixLength
// The declared count is server-controlled; the allocation hint is capped
// by what the body could possibly hold.
- maxMessages := (length - pollBatchHeaderLength) / iggcon.MessageHeaderSize
+ maxMessages := (length - pollPrefixLength) / batch.MessageHeaderSize
if int(messagesCount) < maxMessages {
maxMessages = int(messagesCount)
}
messages := make([]iggcon.IggyMessage, 0, maxMessages)
for position < length {
- if position+iggcon.MessageHeaderSize > length {
- return nil, fmt.Errorf("poll response: truncated message header at byte %d", position)
- }
- header, err := iggcon.MessageHeaderFromBytes(payload[position : position+iggcon.MessageHeaderSize])
+ record, err := batch.DecodeHeader(payload[position:])
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("poll response: %w", err)
}
- position += iggcon.MessageHeaderSize
- if uint64(header.PayloadLength) > uint64(length-position) {
+ if record.BatchLength > uint64(length-position) {
return nil, fmt.Errorf(
- "poll response: message payload of %d bytes overruns the body", header.PayloadLength)
+ "poll response: batch record of %d bytes overruns the body", record.BatchLength)
}
- payloadSlice := payload[position : position+int(header.PayloadLength)]
- position += int(header.PayloadLength)
-
- if uint64(header.UserHeaderLength) > uint64(length-position) {
- return nil, fmt.Errorf(
- "poll response: user headers of %d bytes overrun the body", header.UserHeaderLength)
- }
- var userHeaders []byte
- if header.UserHeaderLength > 0 {
- userHeaders = payload[position : position+int(header.UserHeaderLength)]
- }
- position += int(header.UserHeaderLength)
-
- switch compression {
- case iggcon.MESSAGE_COMPRESSION_S2, iggcon.MESSAGE_COMPRESSION_S2_BETTER, iggcon.MESSAGE_COMPRESSION_S2_BEST:
- if length < 32 {
- break
- }
- payloadSlice, err = s2.Decode(nil, payloadSlice)
+ recordEnd := position + int(record.BatchLength)
+ cursor := position + batch.HeaderSize
+ for cursor < recordEnd {
+ frame, err := batch.DecodeMessageHeader(payload[cursor:recordEnd])
if err != nil {
- return nil, fmt.Errorf("failed to decode s2 payload: %w", err)
+ return nil, fmt.Errorf("poll response: %w", err)
}
- }
+ payloadStart := cursor + batch.MessageHeaderSize
+ payloadEnd := payloadStart + int(frame.PayloadLength)
+ userHeadersEnd := payloadEnd + int(frame.UserHeadersLength)
+ if userHeadersEnd > recordEnd {
+ return nil, fmt.Errorf(
+ "poll response: message of %d payload and %d user-header bytes overruns the batch record",
+ frame.PayloadLength, frame.UserHeadersLength)
+ }
+ payloadSlice := payload[payloadStart:payloadEnd]
+ var userHeaders []byte
+ if frame.UserHeadersLength > 0 {
+ userHeaders = payload[payloadEnd:userHeadersEnd]
+ }
+ cursor = userHeadersEnd
- messages = append(messages, iggcon.IggyMessage{
- Header: *header,
- Payload: payloadSlice,
- UserHeaders: userHeaders,
- })
+ switch compression {
+ case iggcon.MESSAGE_COMPRESSION_S2, iggcon.MESSAGE_COMPRESSION_S2_BETTER, iggcon.MESSAGE_COMPRESSION_S2_BEST:
+ payloadSlice, err = s2.Decode(nil, payloadSlice)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode s2 payload: %w", err)
+ }
+ }
+
+ messages = append(messages, iggcon.IggyMessage{
+ Header: iggcon.MessageHeader{
+ Checksum: frame.Checksum,
+ Id: iggcon.MessageID(frame.Id),
+ // A record may be a server-sliced view of a larger stored
+ // batch: BaseOffset stays put and the first frame's delta
+ // positions it, so the sum is the absolute offset either way.
+ Offset: record.BaseOffset + uint64(frame.OffsetDelta),
+ // Broker append time is stamped once per batch; the
+ // per-message delta applies to OriginTimestamp only.
+ Timestamp: record.BaseTimestamp,
+ OriginTimestamp: record.OriginTimestamp + uint64(frame.TimestampDelta),
+ UserHeaderLength: frame.UserHeadersLength,
+ PayloadLength: frame.PayloadLength,
+ },
+ Payload: payloadSlice,
+ UserHeaders: userHeaders,
+ })
+ }
+ position = recordEnd
}
if uint32(len(messages)) != messagesCount {
return nil, fmt.Errorf(
@@ -242,7 +263,6 @@
len(messages), messagesCount)
}
- // !TODO: Add message offset ordering
return &iggcon.PolledMessage{
PartitionId: partitionId,
CurrentOffset: currentOffset,
diff --git a/foreign/go/binary_serialization/binary_response_deserializer_test.go b/foreign/go/binary_serialization/binary_response_deserializer_test.go
index fe41b6b..410c8ff 100644
--- a/foreign/go/binary_serialization/binary_response_deserializer_test.go
+++ b/foreign/go/binary_serialization/binary_response_deserializer_test.go
@@ -27,17 +27,10 @@
)
func buildFetchPayload(payloadBody []byte) []byte {
- header := iggcon.NewMessageHeader(iggcon.MessageID{}, uint32(len(payloadBody)), 0)
- headerBytes := header.ToBytes()
-
- // 4 (partitionId) + 8 (currentOffset) + 4 (messagesCount) + header + body
- buf := make([]byte, 16+len(headerBytes)+len(payloadBody))
- binary.LittleEndian.PutUint32(buf[0:4], 1)
- binary.LittleEndian.PutUint64(buf[4:12], 0)
- binary.LittleEndian.PutUint32(buf[12:16], 1)
- copy(buf[16:], headerBytes)
- copy(buf[16+len(headerBytes):], payloadBody)
- return buf
+ buf := binary.LittleEndian.AppendUint32(nil, 1)
+ buf = binary.LittleEndian.AppendUint64(buf, 0)
+ buf = binary.LittleEndian.AppendUint32(buf, 1)
+ return appendBatchRecord(buf, 0, 0, 0, batchFrame{payload: payloadBody})
}
func TestDeserializeFetchMessages_MalformedS2ReturnsError(t *testing.T) {
diff --git a/foreign/go/binary_serialization/vsr_response_deserializer_test.go b/foreign/go/binary_serialization/vsr_response_deserializer_test.go
index 2ef115c..49b8f39 100644
--- a/foreign/go/binary_serialization/vsr_response_deserializer_test.go
+++ b/foreign/go/binary_serialization/vsr_response_deserializer_test.go
@@ -19,10 +19,12 @@
import (
"encoding/binary"
+ "encoding/hex"
"testing"
iggcon "github.com/apache/iggy/foreign/go/contracts"
ierror "github.com/apache/iggy/foreign/go/errors"
+ "github.com/apache/iggy/foreign/go/internal/batch"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -167,22 +169,53 @@
assert.Error(t, err)
}
-// pollPayload builds a poll reply body carrying the given messages.
+// batchFrame is one message frame of a fabricated batch record. Frame
+// checksums stay zero: a poll decode passes them through unverified.
+type batchFrame struct {
+ offsetDelta uint32
+ timestampDelta uint32
+ payload []byte
+ userHeaders []byte
+}
+
+// appendBatchRecord appends one batch record carrying the given frames.
+func appendBatchRecord(body []byte, baseOffset, baseTimestamp, originTimestamp uint64, frames ...batchFrame) []byte {
+ recordStart := len(body)
+ body = append(body, make([]byte, batch.HeaderSize)...)
+ for _, frame := range frames {
+ frameStart := len(body)
+ body = append(body, make([]byte, batch.MessageHeaderSize)...)
+ binary.LittleEndian.PutUint32(body[frameStart+24:], frame.offsetDelta)
+ binary.LittleEndian.PutUint32(body[frameStart+28:], frame.timestampDelta)
+ binary.LittleEndian.PutUint32(body[frameStart+32:], uint32(len(frame.userHeaders)))
+ binary.LittleEndian.PutUint32(body[frameStart+36:], uint32(len(frame.payload)))
+ body = append(body, frame.payload...)
+ body = append(body, frame.userHeaders...)
+ }
+ header := batch.Header{
+ BaseOffset: baseOffset,
+ BaseTimestamp: baseTimestamp,
+ OriginTimestamp: originTimestamp,
+ BatchLength: uint64(len(body) - recordStart),
+ MessageCount: uint32(len(frames)),
+ }
+ header.EncodeInto(body[recordStart:])
+ return body
+}
+
+// pollPayload builds a poll reply body carrying the given messages in one
+// batch record.
func pollPayload(t *testing.T, partitionId uint32, payloads ...[]byte) []byte {
t.Helper()
body := binary.LittleEndian.AppendUint32(nil, partitionId)
body = binary.LittleEndian.AppendUint64(body, 42)
body = binary.LittleEndian.AppendUint32(body, uint32(len(payloads)))
- for _, payload := range payloads {
- message, err := iggcon.NewIggyMessage(payload)
- require.NoError(t, err)
- headerBytes, err := message.Header.AppendBinary(nil)
- require.NoError(t, err)
- body = append(body, headerBytes...)
- body = append(body, message.Payload...)
+ frames := make([]batchFrame, 0, len(payloads))
+ for index, payload := range payloads {
+ frames = append(frames, batchFrame{offsetDelta: uint32(index), payload: payload})
}
- return body
+ return appendBatchRecord(body, 0, 0, 0, frames...)
}
func TestDeserializeFetchMessagesResponse_DecodesABatch(t *testing.T) {
@@ -224,20 +257,72 @@
}
func TestDeserializeFetchMessagesResponse_RejectsOverrunningUserHeaders(t *testing.T) {
- message, err := iggcon.NewIggyMessage([]byte("payload"))
- require.NoError(t, err)
- message.Header.UserHeaderLength = 64
+ body := pollPayload(t, 1, []byte("payload"))
+ // Claim 64 user-header bytes the batch record does not carry.
+ binary.LittleEndian.PutUint32(body[pollPrefixLength+batch.HeaderSize+32:], 64)
+ _, err := DeserializeFetchMessagesResponse(body, iggcon.MESSAGE_COMPRESSION_NONE)
+ assert.Error(t, err, "user headers past the batch record must not panic or pass")
+}
+
+func TestDeserializeFetchMessagesResponse_RejectsNonzeroReservedFrameBytes(t *testing.T) {
+ body := pollPayload(t, 1, []byte("payload"))
+ body[pollPrefixLength+batch.HeaderSize+40] = 1
+
+ _, err := DeserializeFetchMessagesResponse(body, iggcon.MESSAGE_COMPRESSION_NONE)
+ assert.Error(t, err, "a frame with nonzero reserved bytes must not decode")
+}
+
+func TestDeserializeFetchMessagesResponse_ResolvesASlicedRecordsLeadingDelta(t *testing.T) {
+ // A server-sliced record keeps the stored base offset; the first frame's
+ // delta positions it inside the original batch.
body := binary.LittleEndian.AppendUint32(nil, 1)
- body = binary.LittleEndian.AppendUint64(body, 0)
+ body = binary.LittleEndian.AppendUint64(body, 53)
body = binary.LittleEndian.AppendUint32(body, 1)
- headerBytes, err := message.Header.AppendBinary(nil)
- require.NoError(t, err)
- body = append(body, headerBytes...)
- body = append(body, message.Payload...)
+ body = appendBatchRecord(body, 50, 9000, 0,
+ batchFrame{offsetDelta: 3, payload: []byte("tail")})
- _, err = DeserializeFetchMessagesResponse(body, iggcon.MESSAGE_COMPRESSION_NONE)
- assert.Error(t, err, "user headers past the body must not panic or pass")
+ polled, err := DeserializeFetchMessagesResponse(body, iggcon.MESSAGE_COMPRESSION_NONE)
+ require.NoError(t, err)
+ require.Len(t, polled.Messages, 1)
+ assert.Equal(t, uint64(53), polled.Messages[0].Header.Offset)
+ assert.Equal(t, uint64(9000), polled.Messages[0].Header.Timestamp)
+}
+
+// goldenPollBody is a poll reply generated by the Rust encoder: partition 3,
+// current offset 101, and one batch record stamped with base offset 100 and
+// base timestamp 5000 carrying two messages.
+const goldenPollBody = "03000000650000000000000002000000030000000000000064000000000000008813000000000000e8030000000000008c01000000000000c96826b38a8feed202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bfd2b9205a759675070000000000000000000000000000000000000000000000000000000d000000000000000000000066697273742d7061796c6f6164d66b7e1c758eb7c0080000000000000000000000000000000100000032000000110000000e00000000000000000000007365636f6e642d7061796c6f6164757365722d6865616465722d6279746573"
+
+func TestDeserializeFetchMessagesResponse_DecodesTheGoldenVector(t *testing.T) {
+ body, err := hex.DecodeString(goldenPollBody)
+ require.NoError(t, err)
+
+ polled, err := DeserializeFetchMessagesResponse(body, iggcon.MESSAGE_COMPRESSION_NONE)
+ require.NoError(t, err)
+
+ assert.Equal(t, uint32(3), polled.PartitionId)
+ assert.Equal(t, uint64(101), polled.CurrentOffset)
+ assert.Equal(t, uint32(2), polled.MessageCount)
+ require.Len(t, polled.Messages, 2)
+
+ first := polled.Messages[0]
+ assert.Equal(t, iggcon.MessageID{7}, first.Header.Id)
+ assert.Equal(t, uint64(100), first.Header.Offset)
+ assert.Equal(t, uint64(5000), first.Header.Timestamp)
+ assert.Equal(t, uint64(1000), first.Header.OriginTimestamp)
+ assert.Equal(t, uint64(0x7596755a20b9d2bf), first.Header.Checksum)
+ assert.Equal(t, []byte("first-payload"), first.Payload)
+ assert.Empty(t, first.UserHeaders)
+
+ second := polled.Messages[1]
+ assert.Equal(t, iggcon.MessageID{8}, second.Header.Id)
+ assert.Equal(t, uint64(101), second.Header.Offset)
+ assert.Equal(t, uint64(5000), second.Header.Timestamp)
+ assert.Equal(t, uint64(1050), second.Header.OriginTimestamp)
+ assert.Equal(t, uint64(0xc0b78e751c7e6bd6), second.Header.Checksum)
+ assert.Equal(t, []byte("second-payload"), second.Payload)
+ assert.Equal(t, []byte("user-header-bytes"), second.UserHeaders)
}
func TestDeserializeToTopic_PinsTheFieldOrderOfTheWireLayout(t *testing.T) {
diff --git a/foreign/go/contracts/message_header.go b/foreign/go/contracts/message_header.go
index 8b61292..70b58a7 100644
--- a/foreign/go/contracts/message_header.go
+++ b/foreign/go/contracts/message_header.go
@@ -18,15 +18,14 @@
package iggcon
import (
- "encoding/binary"
- "errors"
"time"
)
-const MessageHeaderSize = 8 + 16 + 8 + 8 + 8 + 4 + 4 + 8
-
type MessageID [16]byte
+// MessageHeader carries a message's metadata. On send, Id and
+// OriginTimestamp are the producer's (a zero Id is minted on encode); on
+// poll, Offset and Timestamp are the absolute values stamped by the server.
type MessageHeader struct {
Checksum uint64 `json:"checksum"`
Id MessageID `json:"id"`
@@ -46,45 +45,3 @@
UserHeaderLength: userHeaderLength,
}
}
-
-func MessageHeaderFromBytes(data []byte) (*MessageHeader, error) {
-
- if len(data) != MessageHeaderSize {
- return nil, errors.New("data has incorrect size, must be 64")
- }
- checksum := binary.LittleEndian.Uint64(data[0:8])
- id := data[8:24]
- offset := binary.LittleEndian.Uint64(data[24:32])
- timestamp := binary.LittleEndian.Uint64(data[32:40])
- originTimestamp := binary.LittleEndian.Uint64(data[40:48])
- userHeaderLength := binary.LittleEndian.Uint32(data[48:52])
- payloadLength := binary.LittleEndian.Uint32(data[52:56])
- reserved := binary.LittleEndian.Uint64(data[56:64])
-
- return &MessageHeader{
- Checksum: checksum,
- Id: MessageID(id),
- Offset: offset,
- Timestamp: timestamp,
- OriginTimestamp: originTimestamp,
- UserHeaderLength: userHeaderLength,
- PayloadLength: payloadLength,
- Reserved: reserved,
- }, nil
-}
-
-func (mh *MessageHeader) ToBytes() []byte {
- bytes, _ := mh.AppendBinary(make([]byte, 0, MessageHeaderSize))
- return bytes
-}
-
-func (mh *MessageHeader) AppendBinary(b []byte) ([]byte, error) {
- b = binary.LittleEndian.AppendUint64(b, mh.Checksum)
- b = append(b, mh.Id[:]...)
- b = binary.LittleEndian.AppendUint64(b, mh.Offset)
- b = binary.LittleEndian.AppendUint64(b, mh.Timestamp)
- b = binary.LittleEndian.AppendUint64(b, mh.OriginTimestamp)
- b = binary.LittleEndian.AppendUint32(b, mh.UserHeaderLength)
- b = binary.LittleEndian.AppendUint32(b, mh.PayloadLength)
- return binary.LittleEndian.AppendUint64(b, mh.Reserved), nil
-}
diff --git a/foreign/go/contracts/version.go b/foreign/go/contracts/version.go
index ba3f36b..9f51ca3 100644
--- a/foreign/go/contracts/version.go
+++ b/foreign/go/contracts/version.go
@@ -17,4 +17,4 @@
package iggcon
-const Version = "0.9.0-edge.3"
+const Version = "0.9.0-edge.4"
diff --git a/foreign/go/go.mod b/foreign/go/go.mod
index 70e9dfa..cfd8f65 100644
--- a/foreign/go/go.mod
+++ b/foreign/go/go.mod
@@ -8,13 +8,16 @@
github.com/google/uuid v1.6.0
github.com/klauspost/compress v1.19.2
github.com/stretchr/testify v1.11.1
+ github.com/zeebo/xxh3 v1.1.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
+ golang.org/x/sys v0.30.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
diff --git a/foreign/go/go.sum b/foreign/go/go.sum
index c9dd51e..36e95e8 100644
--- a/foreign/go/go.sum
+++ b/foreign/go/go.sum
@@ -9,6 +9,8 @@
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -24,6 +26,12 @@
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
+github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
+github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
+github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/foreign/go/internal/batch/batch.go b/foreign/go/internal/batch/batch.go
new file mode 100644
index 0000000..c3762c3
--- /dev/null
+++ b/foreign/go/internal/batch/batch.go
@@ -0,0 +1,129 @@
+// 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.
+
+// Package batch implements the canonical message batch record shared by
+// produce request bodies and poll reply bodies: a 256-byte batch header
+// followed by message frames of [48-byte header][payload][user headers].
+package batch
+
+import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+
+ "github.com/zeebo/xxh3"
+)
+
+// HeaderSize is the size of the batch header. Bytes past the declared
+// fields are reserved and stay zero.
+const HeaderSize = 256
+
+// MessageHeaderSize is the size of a message frame header.
+const MessageHeaderSize = 48
+
+// Header is the batch header. A producer leaves PartitionId, BaseOffset,
+// and BaseTimestamp zero; the server stamps them.
+type Header struct {
+ PartitionId uint64
+ BaseOffset uint64
+ BaseTimestamp uint64
+ OriginTimestamp uint64
+ BatchLength uint64
+ BatchChecksum uint64
+ MessageCount uint32
+}
+
+// DecodeHeader reads a batch header from the front of data.
+func DecodeHeader(data []byte) (Header, error) {
+ if len(data) < HeaderSize {
+ return Header{}, fmt.Errorf("batch header needs %d bytes, got %d", HeaderSize, len(data))
+ }
+ header := Header{
+ PartitionId: binary.LittleEndian.Uint64(data[0:8]),
+ BaseOffset: binary.LittleEndian.Uint64(data[8:16]),
+ BaseTimestamp: binary.LittleEndian.Uint64(data[16:24]),
+ OriginTimestamp: binary.LittleEndian.Uint64(data[24:32]),
+ BatchLength: binary.LittleEndian.Uint64(data[32:40]),
+ BatchChecksum: binary.LittleEndian.Uint64(data[40:48]),
+ MessageCount: binary.LittleEndian.Uint32(data[48:52]),
+ }
+ if header.BatchLength < HeaderSize {
+ return Header{}, fmt.Errorf(
+ "batch length %d does not cover the %d-byte batch header", header.BatchLength, HeaderSize)
+ }
+ return header, nil
+}
+
+// EncodeInto writes the header into the first HeaderSize bytes of b and
+// zeroes the reserved tail.
+func (h Header) EncodeInto(b []byte) {
+ clear(b[:HeaderSize])
+ binary.LittleEndian.PutUint64(b[0:8], h.PartitionId)
+ binary.LittleEndian.PutUint64(b[8:16], h.BaseOffset)
+ binary.LittleEndian.PutUint64(b[16:24], h.BaseTimestamp)
+ binary.LittleEndian.PutUint64(b[24:32], h.OriginTimestamp)
+ binary.LittleEndian.PutUint64(b[32:40], h.BatchLength)
+ binary.LittleEndian.PutUint64(b[40:48], h.BatchChecksum)
+ binary.LittleEndian.PutUint32(b[48:52], h.MessageCount)
+}
+
+// Checksum computes the batch checksum: XXH3-64 over the six header meta
+// fields followed by every frame's stored 8-byte checksum field in message
+// order. Message bodies are bound transitively through the frame checksums.
+func (h Header) Checksum(frameChecksums []byte) uint64 {
+ input := make([]byte, 0, 44+len(frameChecksums))
+ input = binary.LittleEndian.AppendUint64(input, h.PartitionId)
+ input = binary.LittleEndian.AppendUint64(input, h.BaseOffset)
+ input = binary.LittleEndian.AppendUint64(input, h.BaseTimestamp)
+ input = binary.LittleEndian.AppendUint64(input, h.OriginTimestamp)
+ input = binary.LittleEndian.AppendUint64(input, h.BatchLength)
+ input = binary.LittleEndian.AppendUint32(input, h.MessageCount)
+ input = append(input, frameChecksums...)
+ return xxh3.Hash(input)
+}
+
+// MessageHeader is a message frame header. OffsetDelta and TimestampDelta
+// resolve against the batch header's stamped bases; Checksum is XXH3-64
+// over frame[8:48] followed by the payload and the user headers.
+type MessageHeader struct {
+ Checksum uint64
+ Id [16]byte
+ OffsetDelta uint32
+ TimestampDelta uint32
+ UserHeadersLength uint32
+ PayloadLength uint32
+}
+
+// DecodeMessageHeader reads a frame header from the front of data,
+// rejecting nonzero reserved bytes.
+func DecodeMessageHeader(data []byte) (MessageHeader, error) {
+ if len(data) < MessageHeaderSize {
+ return MessageHeader{}, fmt.Errorf(
+ "message frame header needs %d bytes, got %d", MessageHeaderSize, len(data))
+ }
+ if binary.LittleEndian.Uint64(data[40:48]) != 0 {
+ return MessageHeader{}, errors.New("message frame reserved bytes must be zero")
+ }
+ return MessageHeader{
+ Checksum: binary.LittleEndian.Uint64(data[0:8]),
+ Id: [16]byte(data[8:24]),
+ OffsetDelta: binary.LittleEndian.Uint32(data[24:28]),
+ TimestampDelta: binary.LittleEndian.Uint32(data[28:32]),
+ UserHeadersLength: binary.LittleEndian.Uint32(data[32:36]),
+ PayloadLength: binary.LittleEndian.Uint32(data[36:40]),
+ }, nil
+}
diff --git a/foreign/go/internal/command/code.go b/foreign/go/internal/command/code.go
index 057a829..e62b816 100644
--- a/foreign/go/internal/command/code.go
+++ b/foreign/go/internal/command/code.go
@@ -20,58 +20,56 @@
type Code int
const (
- PingCode Code = 1
- GetStatsCode Code = 10
- GetSnapshotFileCode Code = 11
- GetClusterMetadataCode Code = 12
- DescribeOptionsCode Code = 13
- GetMeCode Code = 20
- GetClientCode Code = 21
- GetClientsCode Code = 22
- GetUserCode Code = 31
- GetUsersCode Code = 32
- CreateUserCode Code = 33
- DeleteUserCode Code = 34
- UpdateUserCode Code = 35
- UpdatePermissionsCode Code = 36
- ChangePasswordCode Code = 37
- LoginUserCode Code = 38
- LogoutUserCode Code = 39
- LoginRegisterCode Code = 40
- GetAccessTokensCode Code = 41
- CreateAccessTokenCode Code = 42
- DeleteAccessTokenCode Code = 43
- LoginWithAccessTokenCode Code = 44
- LoginRegisterWithPATCode Code = 45
- PollMessagesCode Code = 100
- SendMessagesCode Code = 101
- GetOffsetCode Code = 120
- StoreOffsetCode Code = 121
- DeleteConsumerOffsetCode Code = 122
- StoreOffset2Code Code = 123
- DeleteConsumerOffset2Code Code = 124
- GetStreamCode Code = 200
- GetStreamsCode Code = 201
- CreateStreamCode Code = 202
- DeleteStreamCode Code = 203
- UpdateStreamCode Code = 204
- PurgeStreamCode Code = 205
- GetTopicCode Code = 300
- GetTopicsCode Code = 301
- CreateTopicCode Code = 302
- DeleteTopicCode Code = 303
- UpdateTopicCode Code = 304
- PurgeTopicCode Code = 305
- CreatePartitionsCode Code = 402
- DeletePartitionsCode Code = 403
- DeleteSegmentsCode Code = 503
- GetGroupCode Code = 600
- GetGroupsCode Code = 601
- CreateGroupCode Code = 602
- DeleteGroupCode Code = 603
- JoinGroupCode Code = 604
- LeaveGroupCode Code = 605
- SyncGroupCode Code = 606
+ PingCode Code = 1
+ GetStatsCode Code = 10
+ GetSnapshotFileCode Code = 11
+ GetClusterMetadataCode Code = 12
+ DescribeOptionsCode Code = 13
+ GetMeCode Code = 20
+ GetClientCode Code = 21
+ GetClientsCode Code = 22
+ GetUserCode Code = 31
+ GetUsersCode Code = 32
+ CreateUserCode Code = 33
+ DeleteUserCode Code = 34
+ UpdateUserCode Code = 35
+ UpdatePermissionsCode Code = 36
+ ChangePasswordCode Code = 37
+ LoginUserCode Code = 38
+ LogoutUserCode Code = 39
+ LoginRegisterCode Code = 40
+ GetAccessTokensCode Code = 41
+ CreateAccessTokenCode Code = 42
+ DeleteAccessTokenCode Code = 43
+ LoginWithAccessTokenCode Code = 44
+ LoginRegisterWithPATCode Code = 45
+ PollMessagesCode Code = 100
+ SendMessagesCode Code = 101
+ GetOffsetCode Code = 120
+ StoreOffsetCode Code = 121
+ DeleteConsumerOffsetCode Code = 122
+ GetStreamCode Code = 200
+ GetStreamsCode Code = 201
+ CreateStreamCode Code = 202
+ DeleteStreamCode Code = 203
+ UpdateStreamCode Code = 204
+ PurgeStreamCode Code = 205
+ GetTopicCode Code = 300
+ GetTopicsCode Code = 301
+ CreateTopicCode Code = 302
+ DeleteTopicCode Code = 303
+ UpdateTopicCode Code = 304
+ PurgeTopicCode Code = 305
+ CreatePartitionsCode Code = 402
+ DeletePartitionsCode Code = 403
+ DeleteSegmentsCode Code = 503
+ GetGroupCode Code = 600
+ GetGroupsCode Code = 601
+ CreateGroupCode Code = 602
+ DeleteGroupCode Code = 603
+ JoinGroupCode Code = 604
+ LeaveGroupCode Code = 605
+ SyncGroupCode Code = 606
)
// internal const int GET_PERSONAL_ACCESS_TOKENS_CODE = 41;
diff --git a/foreign/go/internal/command/message.go b/foreign/go/internal/command/message.go
index ef9bcc9..19d277a 100644
--- a/foreign/go/internal/command/message.go
+++ b/foreign/go/internal/command/message.go
@@ -19,18 +19,15 @@
import (
"encoding/binary"
+ "errors"
+ "fmt"
+ "math"
"github.com/apache/iggy/foreign/go/contracts"
+ "github.com/apache/iggy/foreign/go/internal/batch"
+ "github.com/google/uuid"
"github.com/klauspost/compress/s2"
-)
-
-const (
- partitionPresenceSize = 1
- partitionFieldSize = 4
- partitionStrategySize = partitionPresenceSize + partitionFieldSize + 1
- offsetSize = 12
- commitFlagSize = 1
- indexSize = 16
+ "github.com/zeebo/xxh3"
)
type SendMessages struct {
@@ -46,18 +43,24 @@
return SendMessagesCode
}
-// zeroIndex is the blank per-message index entry reserved ahead of the
-// message section and filled in as messages are appended.
-var zeroIndex [indexSize]byte
+// zeroBatchHeader is the blank batch header reserved ahead of the message
+// frames and backpatched once every frame checksum is known.
+var zeroBatchHeader [batch.HeaderSize]byte
func (s *SendMessages) MarshalBinary() ([]byte, error) {
return s.AppendBinary(nil)
}
// AppendBinary encodes the batch straight into b: [metadata_length u32]
-// [stream id][topic id][partitioning][messages_count u32], the per-message
-// index section, then each message as header, payload, user headers.
+// [stream id][topic id][partitioning][messages_count u32], then one canonical
+// batch record: a 256-byte batch header followed by one frame per message.
func (s *SendMessages) AppendBinary(b []byte) ([]byte, error) {
+ // The server rejects an empty batch at admission. Refuse it before the
+ // wire, matching every other SDK encoder.
+ if len(s.Messages) == 0 {
+ return b, errors.New("cannot encode an empty message batch")
+ }
+
s.compressPayloads()
metadataStart := len(b)
@@ -76,30 +79,65 @@
metadataLength := len(b) - metadataStart - 4
binary.LittleEndian.PutUint32(b[metadataStart:], uint32(metadataLength))
- indexesStart := len(b)
- for range s.Messages {
- b = append(b, zeroIndex[:]...)
+ var originTimestamp uint64
+ for i := range s.Messages {
+ if i == 0 || s.Messages[i].Header.OriginTimestamp < originTimestamp {
+ originTimestamp = s.Messages[i].Header.OriginTimestamp
+ }
}
- msgSize := uint32(0)
+ headerStart := len(b)
+ b = append(b, zeroBatchHeader[:]...)
+
+ blobStart := len(b)
+ frameChecksums := make([]byte, 0, len(s.Messages)*8)
for i := range s.Messages {
message := &s.Messages[i]
+ // The id sits under the frame checksum, so it must exist before the
+ // frame is hashed; the server never mints ids.
+ if message.Header.Id == (iggcon.MessageID{}) {
+ id, err := uuid.NewRandom()
+ if err != nil {
+ return b, err
+ }
+ message.Header.Id = iggcon.MessageID(id)
+ }
// The header lengths and the appended slices must agree, or every
// message boundary after a mismatch mis-frames; deriving both from
// the same slice makes the disagreement impossible.
message.Header.PayloadLength = uint32(len(message.Payload))
message.Header.UserHeaderLength = uint32(len(message.UserHeaders))
- if b, err = message.Header.AppendBinary(b); err != nil {
- return b, err
+ timestampDelta := message.Header.OriginTimestamp - originTimestamp
+ if timestampDelta > math.MaxUint32 {
+ return b, fmt.Errorf(
+ "message origin timestamp %d runs more than %d microseconds past the batch's earliest %d",
+ message.Header.OriginTimestamp, uint64(math.MaxUint32), originTimestamp)
}
+
+ frameStart := len(b)
+ b = binary.LittleEndian.AppendUint64(b, 0)
+ b = append(b, message.Header.Id[:]...)
+ b = binary.LittleEndian.AppendUint32(b, uint32(i))
+ b = binary.LittleEndian.AppendUint32(b, uint32(timestampDelta))
+ b = binary.LittleEndian.AppendUint32(b, message.Header.UserHeaderLength)
+ b = binary.LittleEndian.AppendUint32(b, message.Header.PayloadLength)
+ b = binary.LittleEndian.AppendUint64(b, 0)
b = append(b, message.Payload...)
b = append(b, message.UserHeaders...)
- msgSize += iggcon.MessageHeaderSize +
- message.Header.PayloadLength + message.Header.UserHeaderLength
- binary.LittleEndian.PutUint32(b[indexesStart+i*indexSize+4:], msgSize)
+ checksum := xxh3.Hash(b[frameStart+8:])
+ binary.LittleEndian.PutUint64(b[frameStart:], checksum)
+ message.Header.Checksum = checksum
+ frameChecksums = binary.LittleEndian.AppendUint64(frameChecksums, checksum)
}
+ batchHeader := batch.Header{
+ OriginTimestamp: originTimestamp,
+ BatchLength: uint64(batch.HeaderSize + len(b) - blobStart),
+ MessageCount: uint32(len(s.Messages)),
+ }
+ batchHeader.BatchChecksum = batchHeader.Checksum(frameChecksums)
+ batchHeader.EncodeInto(b[headerStart:blobStart])
return b, nil
}
diff --git a/foreign/go/internal/command/message_test.go b/foreign/go/internal/command/message_test.go
index 1fbe43c..d6944d6 100644
--- a/foreign/go/internal/command/message_test.go
+++ b/foreign/go/internal/command/message_test.go
@@ -19,9 +19,13 @@
import (
"bytes"
+ "encoding/binary"
+ "encoding/hex"
+ "math"
"testing"
"github.com/apache/iggy/foreign/go/contracts"
+ "github.com/apache/iggy/foreign/go/internal/batch"
"github.com/google/uuid"
"github.com/klauspost/compress/s2"
)
@@ -89,6 +93,54 @@
return true
}
+// Golden vectors generated by the Rust encoder for stream 1, topic 2,
+// balanced partitioning, and two messages: {id 7, origin timestamp 1000,
+// payload "first-payload"} and {id 8, origin timestamp 1050, payload
+// "second-payload", user headers "user-header-bytes"}.
+const (
+ goldenProduceBodyFull = "12000000010401000000010402000000010002000000000000000000000000000000000000000000000000000000e8030000000000008c01000000000000a91f38c86307267c02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bfd2b9205a759675070000000000000000000000000000000000000000000000000000000d000000000000000000000066697273742d7061796c6f6164d66b7e1c758eb7c0080000000000000000000000000000000100000032000000110000000e00000000000000000000007365636f6e642d7061796c6f6164757365722d6865616465722d6279746573"
+ goldenProduceBatchOnly = "000000000000000000000000000000000000000000000000e8030000000000008c01000000000000a91f38c86307267c02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bfd2b9205a759675070000000000000000000000000000000000000000000000000000000d000000000000000000000066697273742d7061796c6f6164d66b7e1c758eb7c0080000000000000000000000000000000100000032000000110000000e00000000000000000000007365636f6e642d7061796c6f6164757365722d6865616465722d6279746573"
+)
+
+func TestSerialize_SendMessagesMatchesTheGoldenVector(t *testing.T) {
+ streamId, _ := iggcon.NewIdentifier(uint32(1))
+ topicId, _ := iggcon.NewIdentifier(uint32(2))
+ request := SendMessages{
+ StreamId: streamId,
+ TopicId: topicId,
+ Partitioning: iggcon.None(),
+ Messages: []iggcon.IggyMessage{
+ {
+ Header: iggcon.MessageHeader{Id: iggcon.MessageID{7}, OriginTimestamp: 1000},
+ Payload: []byte("first-payload"),
+ },
+ {
+ Header: iggcon.MessageHeader{Id: iggcon.MessageID{8}, OriginTimestamp: 1050},
+ Payload: []byte("second-payload"),
+ UserHeaders: []byte("user-header-bytes"),
+ },
+ },
+ Compression: iggcon.MESSAGE_COMPRESSION_NONE,
+ }
+
+ serialized, err := request.MarshalBinary()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if got := hex.EncodeToString(serialized); got != goldenProduceBodyFull {
+ t.Errorf("the full produce body diverges from the golden vector.\nExpected:\t%s\nGot:\t\t%s",
+ goldenProduceBodyFull, got)
+ }
+
+ metadataLength := binary.LittleEndian.Uint32(serialized[0:4])
+ batchOnly := serialized[4+metadataLength:]
+ if got := hex.EncodeToString(batchOnly); got != goldenProduceBatchOnly {
+ t.Errorf("the batch section diverges from the golden vector.\nExpected:\t%s\nGot:\t\t%s",
+ goldenProduceBatchOnly, got)
+ }
+}
+
func TestSerialize_SendMessagesRequest(t *testing.T) {
message1 := generateTestMessage("data1")
streamId, _ := iggcon.NewIdentifier("test_stream_id")
@@ -109,8 +161,7 @@
t.Error(err)
}
- // Expected serialized bytes based on the provided sample request
- expected := []byte{
+ expectedMetadata := []byte{
0x29, 0x0, 0x0, 0x0, // metadataLength
0x02, // StreamId Kind (StringId)
0x0E, // StreamId Length (14)
@@ -121,17 +172,119 @@
0x74, 0x65, 0x73, 0x74, 0x5F, 0x74, 0x6F, 0x70, 0x69, 0x63, 0x5F, 0x69, 0x64, // TopicId
0x02, // PartitionIdKind
0x04, // Partitioning Length
- 0x01, 0x00, 0x00, 0x00, // PartitionId (123)
+ 0x01, 0x00, 0x00, 0x00, // PartitionId (1)
0x01, 0x0, 0x0, 0x0, // MessageCount
- 0, 0, 0, 0, 120, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Index (16*1) bytes
}
- expected = append(expected, message1.Header.ToBytes()...)
- expected = append(expected, message1.Payload...)
- expected = append(expected, message1.UserHeaders...)
+ if !bytes.Equal(serialized[:len(expectedMetadata)], expectedMetadata) {
+ t.Fatalf("the metadata section is incorrect.\nExpected:\t%v\nGot:\t\t%v",
+ expectedMetadata, serialized[:len(expectedMetadata)])
+ }
- // Check if the serialized bytes match the expected bytes
- if !bytes.Equal(serialized, expected) {
- t.Errorf("Serialized bytes are incorrect. \nExpected:\t%v\nGot:\t\t%v", expected, serialized)
+ record := serialized[len(expectedMetadata):]
+ header, err := batch.DecodeHeader(record)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if header.PartitionId != 0 || header.BaseOffset != 0 || header.BaseTimestamp != 0 {
+ t.Errorf("the server-stamped fields must be zero, got %+v", header)
+ }
+ if header.OriginTimestamp != message1.Header.OriginTimestamp {
+ t.Errorf("batch origin timestamp = %d, want %d",
+ header.OriginTimestamp, message1.Header.OriginTimestamp)
+ }
+ if header.BatchLength != uint64(len(record)) {
+ t.Errorf("batch length = %d, the record holds %d bytes", header.BatchLength, len(record))
+ }
+ if header.MessageCount != 1 {
+ t.Errorf("message count = %d, want 1", header.MessageCount)
+ }
+ if got := header.Checksum(record[batch.HeaderSize : batch.HeaderSize+8]); got != header.BatchChecksum {
+ t.Errorf("batch checksum = %d, recomputed %d", header.BatchChecksum, got)
+ }
+
+ frame, err := batch.DecodeMessageHeader(record[batch.HeaderSize:])
+ if err != nil {
+ t.Fatal(err)
+ }
+ if frame.Id != [16]byte(message1.Header.Id) {
+ t.Errorf("frame id = %v, want %v", frame.Id, message1.Header.Id)
+ }
+ if frame.OffsetDelta != 0 || frame.TimestampDelta != 0 {
+ t.Errorf("a single-message batch has zero deltas, got %+v", frame)
+ }
+ if int(frame.PayloadLength) != len(message1.Payload) ||
+ int(frame.UserHeadersLength) != len(message1.UserHeaders) {
+ t.Errorf("frame lengths %d/%d do not match the message %d/%d",
+ frame.PayloadLength, frame.UserHeadersLength,
+ len(message1.Payload), len(message1.UserHeaders))
+ }
+
+ body := record[batch.HeaderSize+batch.MessageHeaderSize:]
+ expectedBody := append(append([]byte{}, message1.Payload...), message1.UserHeaders...)
+ if !bytes.Equal(body, expectedBody) {
+ t.Errorf("the frame body is incorrect.\nExpected:\t%v\nGot:\t\t%v", expectedBody, body)
+ }
+}
+
+func TestSerialize_SendMessagesMintsAZeroMessageId(t *testing.T) {
+ message, err := iggcon.NewIggyMessage([]byte("payload"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ streamId, _ := iggcon.NewIdentifier(uint32(1))
+ topicId, _ := iggcon.NewIdentifier(uint32(1))
+ request := SendMessages{
+ StreamId: streamId,
+ TopicId: topicId,
+ Partitioning: iggcon.PartitionId(0),
+ Messages: []iggcon.IggyMessage{message},
+ Compression: iggcon.MESSAGE_COMPRESSION_NONE,
+ }
+
+ serialized, err := request.MarshalBinary()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ minted := request.Messages[0].Header.Id
+ if minted == (iggcon.MessageID{}) {
+ t.Fatal("a zero id must be minted before encoding")
+ }
+ metadataLength := binary.LittleEndian.Uint32(serialized[0:4])
+ frame, err := batch.DecodeMessageHeader(serialized[4+int(metadataLength)+batch.HeaderSize:])
+ if err != nil {
+ t.Fatal(err)
+ }
+ if frame.Id != [16]byte(minted) {
+ t.Errorf("the wire carries id %v, the message holds %v", frame.Id, minted)
+ }
+}
+
+func TestSerialize_SendMessagesRejectsATimestampDeltaPastU32(t *testing.T) {
+ streamId, _ := iggcon.NewIdentifier(uint32(1))
+ topicId, _ := iggcon.NewIdentifier(uint32(1))
+ request := SendMessages{
+ StreamId: streamId,
+ TopicId: topicId,
+ Partitioning: iggcon.PartitionId(0),
+ Messages: []iggcon.IggyMessage{
+ {
+ Header: iggcon.MessageHeader{Id: iggcon.MessageID{1}, OriginTimestamp: 0},
+ Payload: []byte("early"),
+ },
+ {
+ Header: iggcon.MessageHeader{
+ Id: iggcon.MessageID{2},
+ OriginTimestamp: uint64(math.MaxUint32) + 1,
+ },
+ Payload: []byte("late"),
+ },
+ },
+ Compression: iggcon.MESSAGE_COMPRESSION_NONE,
+ }
+
+ if _, err := request.MarshalBinary(); err == nil {
+ t.Fatal("a timestamp delta past u32 must fail the encode")
}
}
@@ -166,20 +319,19 @@
t.Fatalf("the payload did not compress: %d >= %d", len(compressed.Payload), len(payload))
}
- // The message section must be framed by the compressed length: header,
- // then exactly PayloadLength payload bytes, and nothing after.
- messageStart := len(serialized) - int(compressed.Header.PayloadLength) - iggcon.MessageHeaderSize
- header, err := iggcon.MessageHeaderFromBytes(
- serialized[messageStart : messageStart+iggcon.MessageHeaderSize])
+ // The frame must be sized by the compressed length: header, then exactly
+ // PayloadLength payload bytes, and nothing after.
+ frameStart := len(serialized) - int(compressed.Header.PayloadLength) - batch.MessageHeaderSize
+ frame, err := batch.DecodeMessageHeader(serialized[frameStart:])
if err != nil {
t.Fatal(err)
}
- if header.PayloadLength != compressed.Header.PayloadLength {
- t.Fatalf("wire header claims %d, in-memory header %d",
- header.PayloadLength, compressed.Header.PayloadLength)
+ if frame.PayloadLength != compressed.Header.PayloadLength {
+ t.Fatalf("wire frame claims %d, in-memory header %d",
+ frame.PayloadLength, compressed.Header.PayloadLength)
}
- decoded, err := s2.Decode(nil, serialized[messageStart+iggcon.MessageHeaderSize:])
+ decoded, err := s2.Decode(nil, serialized[frameStart+batch.MessageHeaderSize:])
if err != nil {
t.Fatal(err)
}
diff --git a/foreign/go/internal/command/offset.go b/foreign/go/internal/command/offset.go
index ed187f8..e71c78e 100644
--- a/foreign/go/internal/command/offset.go
+++ b/foreign/go/internal/command/offset.go
@@ -23,6 +23,10 @@
iggcon "github.com/apache/iggy/foreign/go/contracts"
)
+// ackQuorum is the AckLevel the offset commands send: the server replies only
+// once the write is committed.
+const ackQuorum byte = 1
+
type StoreConsumerOffsetRequest struct {
StreamId iggcon.Identifier `json:"streamId"`
TopicId iggcon.Identifier `json:"topicId"`
@@ -54,8 +58,8 @@
if err != nil {
return nil, err
}
- // consumer + stream_id + topic_id + hasPartition(1) + partition(4) + offset(8)
- bytes := make([]byte, len(consumerBytes)+len(streamIdBytes)+len(topicIdBytes)+13)
+ // consumer + stream_id + topic_id + hasPartition(1) + partition(4) + offset(8) + ack(1)
+ bytes := make([]byte, len(consumerBytes)+len(streamIdBytes)+len(topicIdBytes)+14)
position := 0
copy(bytes[position:], consumerBytes)
position += len(consumerBytes)
@@ -66,6 +70,7 @@
bytes[position] = hasPartition
binary.LittleEndian.PutUint32(bytes[position+1:position+5], partition)
binary.LittleEndian.PutUint64(bytes[position+5:position+13], s.Offset)
+ bytes[position+13] = ackQuorum
return bytes, nil
}
@@ -143,8 +148,8 @@
if err != nil {
return nil, err
}
- // consumer + stream_id + topic_id + hasPartition(1) + partition(4)
- bytes := make([]byte, len(consumerBytes)+len(streamIdBytes)+len(topicIdBytes)+5)
+ // consumer + stream_id + topic_id + hasPartition(1) + partition(4) + ack(1)
+ bytes := make([]byte, len(consumerBytes)+len(streamIdBytes)+len(topicIdBytes)+6)
position := 0
copy(bytes[position:], consumerBytes)
position += len(consumerBytes)
@@ -154,5 +159,6 @@
position += len(topicIdBytes)
bytes[position] = hasPartition
binary.LittleEndian.PutUint32(bytes[position+1:position+5], partition)
+ bytes[position+5] = ackQuorum
return bytes, nil
}
diff --git a/foreign/go/internal/vsr/operation.go b/foreign/go/internal/vsr/operation.go
index f9c47ef..cc95c57 100644
--- a/foreign/go/internal/vsr/operation.go
+++ b/foreign/go/internal/vsr/operation.go
@@ -60,11 +60,9 @@
OperationJoinConsumerGroup Operation = 148
OperationLeaveConsumerGroup Operation = 149
- OperationSendMessages Operation = 160
- OperationStoreConsumerOffset Operation = 161
- OperationDeleteConsumerOffset Operation = 162
- OperationStoreConsumerOffset2 Operation = 164
- OperationDeleteConsumerOffset2 Operation = 165
+ OperationSendMessages Operation = 160
+ OperationStoreConsumerOffset Operation = 161
+ OperationDeleteConsumerOffset Operation = 162
)
// Band boundaries. The internal band is never client-sent.
@@ -111,8 +109,6 @@
OperationSendMessages,
OperationStoreConsumerOffset,
OperationDeleteConsumerOffset,
- OperationStoreConsumerOffset2,
- OperationDeleteConsumerOffset2,
}
var knownOperations = newOperationSet(allOperations)
@@ -122,33 +118,31 @@
// header field, leaving the server authoritative for commands this SDK build
// does not know.
var replicatedOperation = map[uint32]Operation{
- uint32(command.CreateUserCode): OperationCreateUser,
- uint32(command.DeleteUserCode): OperationDeleteUser,
- uint32(command.UpdateUserCode): OperationUpdateUser,
- uint32(command.UpdatePermissionsCode): OperationUpdatePermissions,
- uint32(command.ChangePasswordCode): OperationChangePassword,
- uint32(command.CreateAccessTokenCode): OperationCreatePersonalAccessToken,
- uint32(command.DeleteAccessTokenCode): OperationDeletePersonalAccessToken,
- uint32(command.SendMessagesCode): OperationSendMessages,
- uint32(command.StoreOffsetCode): OperationStoreConsumerOffset,
- uint32(command.DeleteConsumerOffsetCode): OperationDeleteConsumerOffset,
- uint32(command.StoreOffset2Code): OperationStoreConsumerOffset2,
- uint32(command.DeleteConsumerOffset2Code): OperationDeleteConsumerOffset2,
- uint32(command.CreateStreamCode): OperationCreateStream,
- uint32(command.DeleteStreamCode): OperationDeleteStream,
- uint32(command.UpdateStreamCode): OperationUpdateStream,
- uint32(command.PurgeStreamCode): OperationPurgeStream,
- uint32(command.CreateTopicCode): OperationCreateTopic,
- uint32(command.DeleteTopicCode): OperationDeleteTopic,
- uint32(command.UpdateTopicCode): OperationUpdateTopic,
- uint32(command.PurgeTopicCode): OperationPurgeTopic,
- uint32(command.CreatePartitionsCode): OperationCreatePartitions,
- uint32(command.DeletePartitionsCode): OperationDeletePartitions,
- uint32(command.DeleteSegmentsCode): OperationDeleteSegments,
- uint32(command.CreateGroupCode): OperationCreateConsumerGroup,
- uint32(command.DeleteGroupCode): OperationDeleteConsumerGroup,
- uint32(command.JoinGroupCode): OperationJoinConsumerGroup,
- uint32(command.LeaveGroupCode): OperationLeaveConsumerGroup,
+ uint32(command.CreateUserCode): OperationCreateUser,
+ uint32(command.DeleteUserCode): OperationDeleteUser,
+ uint32(command.UpdateUserCode): OperationUpdateUser,
+ uint32(command.UpdatePermissionsCode): OperationUpdatePermissions,
+ uint32(command.ChangePasswordCode): OperationChangePassword,
+ uint32(command.CreateAccessTokenCode): OperationCreatePersonalAccessToken,
+ uint32(command.DeleteAccessTokenCode): OperationDeletePersonalAccessToken,
+ uint32(command.SendMessagesCode): OperationSendMessages,
+ uint32(command.StoreOffsetCode): OperationStoreConsumerOffset,
+ uint32(command.DeleteConsumerOffsetCode): OperationDeleteConsumerOffset,
+ uint32(command.CreateStreamCode): OperationCreateStream,
+ uint32(command.DeleteStreamCode): OperationDeleteStream,
+ uint32(command.UpdateStreamCode): OperationUpdateStream,
+ uint32(command.PurgeStreamCode): OperationPurgeStream,
+ uint32(command.CreateTopicCode): OperationCreateTopic,
+ uint32(command.DeleteTopicCode): OperationDeleteTopic,
+ uint32(command.UpdateTopicCode): OperationUpdateTopic,
+ uint32(command.PurgeTopicCode): OperationPurgeTopic,
+ uint32(command.CreatePartitionsCode): OperationCreatePartitions,
+ uint32(command.DeletePartitionsCode): OperationDeletePartitions,
+ uint32(command.DeleteSegmentsCode): OperationDeleteSegments,
+ uint32(command.CreateGroupCode): OperationCreateConsumerGroup,
+ uint32(command.DeleteGroupCode): OperationDeleteConsumerGroup,
+ uint32(command.JoinGroupCode): OperationJoinConsumerGroup,
+ uint32(command.LeaveGroupCode): OperationLeaveConsumerGroup,
}
func newOperationSet(operations []Operation) map[Operation]struct{} {
@@ -227,8 +221,7 @@
return true
}
switch operation {
- case OperationStoreConsumerOffset, OperationStoreConsumerOffset2,
- OperationDeleteConsumerOffset, OperationDeleteConsumerOffset2:
+ case OperationStoreConsumerOffset, OperationDeleteConsumerOffset:
return true
default:
return false
diff --git a/foreign/go/internal/vsr/operation_test.go b/foreign/go/internal/vsr/operation_test.go
index ef08726..0b7b478 100644
--- a/foreign/go/internal/vsr/operation_test.go
+++ b/foreign/go/internal/vsr/operation_test.go
@@ -41,8 +41,6 @@
{code: command.SendMessagesCode, want: OperationSendMessages},
{code: command.StoreOffsetCode, want: OperationStoreConsumerOffset},
{code: command.DeleteConsumerOffsetCode, want: OperationDeleteConsumerOffset},
- {code: command.StoreOffset2Code, want: OperationStoreConsumerOffset2},
- {code: command.DeleteConsumerOffset2Code, want: OperationDeleteConsumerOffset2},
{code: command.CreateStreamCode, want: OperationCreateStream},
{code: command.DeleteStreamCode, want: OperationDeleteStream},
{code: command.UpdateStreamCode, want: OperationUpdateStream},
@@ -114,7 +112,7 @@
assert.True(t, IsKnownOperation(operation), "operation %d", operation)
}
- undeclared := []Operation{4, 63, 69, 127, 150, 159, 163, 166, 255}
+ undeclared := []Operation{4, 63, 69, 127, 150, 159, 163, 164, 165, 255}
for _, operation := range undeclared {
assert.False(t, IsKnownOperation(operation), "operation %d", operation)
}
@@ -153,7 +151,7 @@
OperationLogout,
OperationSendMessages,
OperationStoreConsumerOffset,
- OperationDeleteConsumerOffset2,
+ OperationDeleteConsumerOffset,
}
for _, operation := range nonMetadata {
assert.False(t, IsMetadata(operation), "operation %d", operation)
@@ -163,7 +161,7 @@
func TestIsPartition_CoversTheDataPlaneBand(t *testing.T) {
assert.True(t, IsPartition(OperationSendMessages))
assert.True(t, IsPartition(OperationStoreConsumerOffset))
- assert.True(t, IsPartition(OperationDeleteConsumerOffset2))
+ assert.True(t, IsPartition(OperationDeleteConsumerOffset))
assert.False(t, IsPartition(OperationLeaveConsumerGroup))
assert.False(t, IsPartition(OperationDeleteSegments))
}
@@ -177,8 +175,6 @@
OperationLeaveConsumerGroup,
OperationStoreConsumerOffset,
OperationDeleteConsumerOffset,
- OperationStoreConsumerOffset2,
- OperationDeleteConsumerOffset2,
}
for _, operation := range framed {
assert.True(t, IsResultFramed(operation), "operation %d", operation)
diff --git a/foreign/go/internal/vsr/protocol_parity_test.go b/foreign/go/internal/vsr/protocol_parity_test.go
index b510c8e..6cddd7f 100644
--- a/foreign/go/internal/vsr/protocol_parity_test.go
+++ b/foreign/go/internal/vsr/protocol_parity_test.go
@@ -87,8 +87,6 @@
"SendMessages": OperationSendMessages,
"StoreConsumerOffset": OperationStoreConsumerOffset,
"DeleteConsumerOffset": OperationDeleteConsumerOffset,
- "StoreConsumerOffset2": OperationStoreConsumerOffset2,
- "DeleteConsumerOffset2": OperationDeleteConsumerOffset2,
}
// goEvictionReasons names every eviction discriminant the codec declares.
@@ -159,7 +157,7 @@
"u32": {4, 4},
"u64": {8, 8},
"u128": {16, 16},
- "Command2": {1, 1},
+ "Command": {1, 1},
"Operation": {1, 1},
"EvictionReason": {1, 1},
}
@@ -346,15 +344,15 @@
func TestProtocolParity_FrameCommands(t *testing.T) {
sources := loadRustSources(t)
- rustValues := rustEnumValues(sources["command"], "Command2")
- require.NotEmpty(t, rustValues, "the Rust Command2 enum was not found")
+ rustValues := rustEnumValues(sources["command"], "Command")
+ require.NotEmpty(t, rustValues, "the Rust Command enum was not found")
for name, got := range goFrameCommands {
want, ok := rustValues[name]
- if !assert.True(t, ok, "Rust does not declare Command2::%s", name) {
+ if !assert.True(t, ok, "Rust does not declare Command::%s", name) {
continue
}
- assert.Equal(t, want, uint64(got), "Command2::%s", name)
+ assert.Equal(t, want, uint64(got), "Command::%s", name)
}
}
diff --git a/foreign/go/tests/e2e_helpers_test.go b/foreign/go/tests/e2e_helpers_test.go
index 8a4549b..b7d7bf6 100644
--- a/foreign/go/tests/e2e_helpers_test.go
+++ b/foreign/go/tests/e2e_helpers_test.go
@@ -52,12 +52,12 @@
rootPassword = "iggy"
// Command codes the raw-request cases drive directly.
- getSnapshotCode = 11
- storeConsumerOffset2Code = 123
- deleteConsumerOffset2Code = 124
- vendorCode = 60000
+ getSnapshotCode = 11
+ storeConsumerOffsetCode = 121
+ deleteConsumerOffsetCode = 122
+ vendorCode = 60000
- // AckLevel values of the v2 consumer-offset requests.
+ // AckLevel values of the consumer-offset requests.
ackNoAck = 0
ackQuorum = 1
)
@@ -158,9 +158,9 @@
return messages
}
-// consumerOffsetV2Payload builds the shared prefix of the v2 consumer-offset
+// consumerOffsetPayload builds the shared prefix of the consumer-offset
// requests: [consumer][stream][topic][partition flag][partition].
-func consumerOffsetV2Payload(
+func consumerOffsetPayload(
t *testing.T,
consumer iggcon.Consumer,
streamId, topicId iggcon.Identifier,
diff --git a/foreign/go/tests/e2e_test.go b/foreign/go/tests/e2e_test.go
index 48c2cfc..c6ee7d6 100644
--- a/foreign/go/tests/e2e_test.go
+++ b/foreign/go/tests/e2e_test.go
@@ -205,7 +205,7 @@
assert.Error(t, err, "a login must go through LoginUser, not the raw path")
}
-func TestE2E_ConsumerOffsetsV2OverTheRawPath(t *testing.T) {
+func TestE2E_ConsumerOffsetAckLevelsOverTheRawPath(t *testing.T) {
connected := connect(t)
ctx := context.Background()
streamId, topicId := scratchTopic(t, connected, 1)
@@ -223,11 +223,11 @@
offset = 4
}
- store := consumerOffsetV2Payload(t, consumer, streamId, topicId, partitionId)
+ store := consumerOffsetPayload(t, consumer, streamId, topicId, partitionId)
store = binary.LittleEndian.AppendUint64(store, offset)
store = append(store, ack)
- _, err := connected.SendBinaryRequest(ctx, storeConsumerOffset2Code, store)
+ _, err := connected.SendBinaryRequest(ctx, storeConsumerOffsetCode, store)
require.NoError(t, err, "ack level %d", ack)
stored, err := connected.GetConsumerOffset(ctx, consumer, streamId, topicId, &partitionId)
@@ -236,9 +236,9 @@
assert.Equal(t, offset, stored.StoredOffset, "ack level %d", ack)
}
- remove := consumerOffsetV2Payload(t, consumer, streamId, topicId, partitionId)
+ remove := consumerOffsetPayload(t, consumer, streamId, topicId, partitionId)
remove = append(remove, ackQuorum)
- _, err = connected.SendBinaryRequest(ctx, deleteConsumerOffset2Code, remove)
+ _, err = connected.SendBinaryRequest(ctx, deleteConsumerOffsetCode, remove)
require.NoError(t, err)
}
diff --git a/foreign/java/gradle/libs.versions.toml b/foreign/java/gradle/libs.versions.toml
index 6b5b54d..2cf5ce3 100644
--- a/foreign/java/gradle/libs.versions.toml
+++ b/foreign/java/gradle/libs.versions.toml
@@ -29,6 +29,9 @@
# Apache Commons
commons-lang3 = "3.20.0"
+# Hashing
+hash4j = "0.22.0"
+
# HTTP Client
httpclient5 = "5.6.3"
@@ -71,6 +74,9 @@
# Apache Commons
commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang3" }
+# Hashing
+hash4j = { module = "com.dynatrace.hash4j:hash4j", version.ref = "hash4j" }
+
# Logging
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
diff --git a/foreign/java/java-sdk/build.gradle.kts b/foreign/java/java-sdk/build.gradle.kts
index 688680e..2fa1af9 100644
--- a/foreign/java/java-sdk/build.gradle.kts
+++ b/foreign/java/java-sdk/build.gradle.kts
@@ -43,6 +43,7 @@
implementation(libs.httpclient5)
implementation(libs.jackson.databind)
implementation(libs.commons.lang3)
+ implementation(libs.hash4j)
implementation(libs.slf4j.api)
implementation(libs.spotbugs.annotations)
implementation(libs.netty.buffer)
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/ConsumerOffsetsTcpClient.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/ConsumerOffsetsTcpClient.java
index cb53a72..698f158 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/ConsumerOffsetsTcpClient.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/ConsumerOffsetsTcpClient.java
@@ -41,6 +41,8 @@
public class ConsumerOffsetsTcpClient implements ConsumerOffsetsClient {
private static final Logger log = LoggerFactory.getLogger(ConsumerOffsetsTcpClient.class);
+ private static final byte ACK_QUORUM = 1;
+
private final Supplier<AsyncTcpConnection> connectionSupplier;
public ConsumerOffsetsTcpClient(Supplier<AsyncTcpConnection> connectionSupplier) {
@@ -59,6 +61,7 @@
payload.writeBytes(BytesSerializer.toBytes(topicId));
payload.writeBytes(BytesSerializer.toBytes(partitionId));
payload.writeBytes(BytesSerializer.toBytesAsU64(offset));
+ payload.writeByte(ACK_QUORUM);
log.debug(
"Storing consumer offset - Stream: {}, Topic: {}, Partition: {}, Consumer: {}, Offset: {}",
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/MessagesTcpClient.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/MessagesTcpClient.java
index 17b8bc5..bee4cb8 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/MessagesTcpClient.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/MessagesTcpClient.java
@@ -51,6 +51,7 @@
import java.util.function.Supplier;
import static org.apache.iggy.serde.BytesSerializer.toBytes;
+import static org.apache.iggy.serde.BytesSerializer.toMessagesBatch;
/**
* Async TCP implementation of MessagesClient using Netty for non-blocking I/O.
@@ -181,35 +182,16 @@
private CompletableFuture<SendMessagesResponse> sendToPartition(
StreamId streamId, TopicId topicId, Partitioning partitioning, List<Message> messages) {
- // Build metadata section following the blocking client pattern
var metadataLength = streamId.getSize() + topicId.getSize() + partitioning.getSize() + 4;
- var payload = Unpooled.buffer(4 + metadataLength);
+ var batch = toMessagesBatch(messages);
+ var payload = Unpooled.buffer(4 + metadataLength + batch.readableBytes());
- // Write metadata length and components
payload.writeIntLE(metadataLength);
payload.writeBytes(toBytes(streamId));
payload.writeBytes(toBytes(topicId));
payload.writeBytes(toBytes(partitioning));
payload.writeIntLE(messages.size());
-
- // Write message index metadata (required by server)
- var position = 0;
- for (var message : messages) {
- // Calculate position for next message
- position += message.getSize();
-
- // offset (4 bytes)
- payload.writeIntLE(0);
- // position (4 bytes)
- payload.writeIntLE(position);
- // timestamp (8 bytes)
- payload.writeZero(8);
- }
-
- // Write actual message data
- for (var message : messages) {
- payload.writeBytes(toBytes(message));
- }
+ payload.writeBytes(batch);
return connection().send(CommandCode.Messages.SEND.getValue(), payload).thenApply(response -> {
try {
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/vsr/VsrHeaders.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/vsr/VsrHeaders.java
index 356c722..35b933e 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/vsr/VsrHeaders.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/vsr/VsrHeaders.java
@@ -56,7 +56,7 @@
static final int EVICTION_PROTOCOL_VERSION_MIN_OFFSET = 148;
static final int EVICTION_REASON_OFFSET = 255;
- // Command2 discriminants
+ // Command discriminants
static final int COMMAND_REQUEST = 5;
static final int COMMAND_REPLY = 8;
static final int COMMAND_EVICTION = 13;
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/vsr/VsrOperation.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/vsr/VsrOperation.java
index 6bd6349..794dd6f 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/vsr/VsrOperation.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/vsr/VsrOperation.java
@@ -66,8 +66,6 @@
public static final int SEND_MESSAGES = 160;
public static final int STORE_CONSUMER_OFFSET = 161;
public static final int DELETE_CONSUMER_OFFSET = 162;
- public static final int STORE_CONSUMER_OFFSET_2 = 164;
- public static final int DELETE_CONSUMER_OFFSET_2 = 165;
private static final int INTERNAL_START = 64;
private static final int METADATA_START = 128;
@@ -90,8 +88,6 @@
Map.entry(101, SEND_MESSAGES),
Map.entry(121, STORE_CONSUMER_OFFSET),
Map.entry(122, DELETE_CONSUMER_OFFSET),
- Map.entry(123, STORE_CONSUMER_OFFSET_2),
- Map.entry(124, DELETE_CONSUMER_OFFSET_2),
Map.entry(202, CREATE_STREAM),
Map.entry(203, DELETE_STREAM),
Map.entry(204, UPDATE_STREAM),
@@ -119,8 +115,6 @@
KNOWN_OPERATIONS.set(SEND_MESSAGES);
KNOWN_OPERATIONS.set(STORE_CONSUMER_OFFSET);
KNOWN_OPERATIONS.set(DELETE_CONSUMER_OFFSET);
- KNOWN_OPERATIONS.set(STORE_CONSUMER_OFFSET_2);
- KNOWN_OPERATIONS.set(DELETE_CONSUMER_OFFSET_2);
}
private VsrOperation() {}
@@ -158,11 +152,7 @@
* section ({@code [count:u32][{index,result} x count]}).
*/
static boolean isResultFramed(int operation) {
- return isMetadata(operation)
- || operation == STORE_CONSUMER_OFFSET
- || operation == DELETE_CONSUMER_OFFSET
- || operation == STORE_CONSUMER_OFFSET_2
- || operation == DELETE_CONSUMER_OFFSET_2;
+ return isMetadata(operation) || operation == STORE_CONSUMER_OFFSET || operation == DELETE_CONSUMER_OFFSET;
}
/**
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/MessageHeader.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/MessageHeader.java
index 85f5868..62fc128 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/MessageHeader.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/MessageHeader.java
@@ -30,5 +30,9 @@
Long userHeadersLength,
Long payloadLength,
BigInteger reserved) {
- public static final int SIZE = 8 + 16 + 8 + 8 + 8 + 4 + 4 + 8;
+ /**
+ * Size of a message's frame header inside a batch record. The frame carries offset and
+ * timestamp deltas as u32; this record resolves them to the absolute values.
+ */
+ public static final int SIZE = 8 + 16 + 4 + 4 + 4 + 4 + 8;
}
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesDeserializer.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesDeserializer.java
index 8a1b3c5..59b6b54 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesDeserializer.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesDeserializer.java
@@ -250,47 +250,111 @@
var messagesCount = response.readUnsignedIntLE();
var messages = new ArrayList<Message>();
while (response.isReadable()) {
- messages.add(readPolledMessage(response));
+ readBatchRecord(response, messages);
}
return new PolledMessages(partitionId, currentOffset, messagesCount, messages);
}
- public static Message readPolledMessage(ByteBuf response) {
- var checksum = readU64AsBigInteger(response);
- var id = readBytesMessageId(response);
- var offset = readU64AsBigInteger(response);
- var timestamp = readU64AsBigInteger(response);
- var originTimestamp = readU64AsBigInteger(response);
- var userHeadersLength = response.readUnsignedIntLE();
- var payloadLength = response.readUnsignedIntLE();
- var reserved = readU64AsBigInteger(response);
- var header = new MessageHeader(
- checksum, id, offset, timestamp, originTimestamp, userHeadersLength, payloadLength, reserved);
- var payload = newByteArray(payloadLength);
- response.readBytes(payload);
- Map<HeaderKey, HeaderValue> userHeaders = new HashMap<>();
- if (userHeadersLength > 0) {
- ByteBuf userHeadersBuffer = response.readSlice(toInt(userHeadersLength));
- Map<HeaderKey, HeaderValue> headers = new HashMap<>();
- while (userHeadersBuffer.isReadable()) {
- var userHeaderKeyKindCode = userHeadersBuffer.readUnsignedByte();
- var userHeaderKeyLength = userHeadersBuffer.readUnsignedIntLE();
- byte[] userHeaderKeyBytes = new byte[toInt(userHeaderKeyLength)];
- userHeadersBuffer.readBytes(userHeaderKeyBytes);
- var userHeaderKey = new HeaderKey(HeaderKind.fromCode(userHeaderKeyKindCode), userHeaderKeyBytes);
-
- var userHeaderValueKindCode = userHeadersBuffer.readUnsignedByte();
- var userHeaderValueLength = userHeadersBuffer.readUnsignedIntLE();
- byte[] userHeaderValueBytes = new byte[toInt(userHeaderValueLength)];
- userHeadersBuffer.readBytes(userHeaderValueBytes);
- headers.put(
- userHeaderKey,
- new HeaderValue(HeaderKind.fromCode(userHeaderValueKindCode), userHeaderValueBytes));
- }
- userHeaders = headers;
+ /**
+ * Reads one batch record ({@code [256B batch header][frames]}) into messages with absolute
+ * offsets and timestamps. A record may be a server-sliced view of a stored batch, so the
+ * first frame's offset delta is not necessarily zero.
+ */
+ private static void readBatchRecord(ByteBuf response, List<Message> messages) {
+ if (response.readableBytes() < BytesSerializer.BATCH_HEADER_SIZE) {
+ throw new IggyMalformedResponseException(
+ "Truncated batch header: " + response.readableBytes() + " bytes left");
}
+ var headerStart = response.readerIndex();
+ response.skipBytes(Long.BYTES); // partition_id, already carried by the poll response header
+ var baseOffset = readU64AsBigInteger(response);
+ var baseTimestamp = readU64AsBigInteger(response);
+ var batchOriginTimestamp = readU64AsBigInteger(response);
+ var batchLength = readU64AsBigInteger(response);
+ response.readerIndex(headerStart + BytesSerializer.BATCH_HEADER_SIZE);
- return new Message(header, payload, userHeaders);
+ var blobLength = batchLength.subtract(BigInteger.valueOf(BytesSerializer.BATCH_HEADER_SIZE));
+ if (blobLength.signum() < 0 || blobLength.compareTo(BigInteger.valueOf(response.readableBytes())) > 0) {
+ throw new IggyMalformedResponseException("Batch length " + batchLength + " exceeds remaining payload of "
+ + response.readableBytes() + " bytes");
+ }
+ ByteBuf blob = response.readSlice(blobLength.intValueExact());
+ while (blob.isReadable()) {
+ messages.add(readBatchMessage(blob, baseOffset, baseTimestamp, batchOriginTimestamp));
+ }
+ }
+
+ private static Message readBatchMessage(
+ ByteBuf blob, BigInteger baseOffset, BigInteger baseTimestamp, BigInteger batchOriginTimestamp) {
+ if (blob.readableBytes() < MessageHeader.SIZE) {
+ throw new IggyMalformedResponseException(
+ "Truncated message frame header: " + blob.readableBytes() + " bytes left");
+ }
+ var checksum = readU64AsBigInteger(blob);
+ var id = readBytesMessageId(blob);
+ var offsetDelta = blob.readUnsignedIntLE();
+ var timestampDelta = blob.readUnsignedIntLE();
+ var userHeadersLength = blob.readUnsignedIntLE();
+ var payloadLength = blob.readUnsignedIntLE();
+ var reserved = readU64AsBigInteger(blob);
+ if (reserved.signum() != 0) {
+ throw new IggyMalformedResponseException("Message frame reserved bytes must be zero");
+ }
+ if (payloadLength + userHeadersLength > blob.readableBytes()) {
+ throw new IggyMalformedResponseException("Message frame length " + (payloadLength + userHeadersLength)
+ + " exceeds remaining batch of " + blob.readableBytes() + " bytes");
+ }
+ var header = new MessageHeader(
+ checksum,
+ id,
+ baseOffset.add(BigInteger.valueOf(offsetDelta)),
+ baseTimestamp,
+ batchOriginTimestamp.add(BigInteger.valueOf(timestampDelta)),
+ userHeadersLength,
+ payloadLength,
+ BigInteger.ZERO);
+ var payload = newByteArray(payloadLength);
+ blob.readBytes(payload);
+ return new Message(header, payload, readUserHeaders(blob, userHeadersLength));
+ }
+
+ /**
+ * User headers ride the frame as opaque bytes, so another SDK may put non-TLV data there;
+ * such bytes decode to an empty map while {@code userHeadersLength} still reports them.
+ */
+ private static Map<HeaderKey, HeaderValue> readUserHeaders(ByteBuf frame, Long userHeadersLength) {
+ Map<HeaderKey, HeaderValue> userHeaders = new HashMap<>();
+ if (userHeadersLength == 0) {
+ return userHeaders;
+ }
+ ByteBuf slice = frame.readSlice(toInt(userHeadersLength));
+ while (slice.isReadable()) {
+ var key = readUserHeaderField(slice);
+ var value = key == null ? null : readUserHeaderField(slice);
+ if (value == null) {
+ return new HashMap<>();
+ }
+ userHeaders.put(new HeaderKey(key.kind(), key.value()), new HeaderValue(value.kind(), value.value()));
+ }
+ return userHeaders;
+ }
+
+ private static UserHeaderField readUserHeaderField(ByteBuf slice) {
+ if (slice.readableBytes() < 1 + Integer.BYTES) {
+ return null;
+ }
+ var kindCode = slice.readUnsignedByte();
+ var length = slice.readUnsignedIntLE();
+ if (length > slice.readableBytes()) {
+ return null;
+ }
+ byte[] value = newByteArray(length);
+ slice.readBytes(value);
+ try {
+ return new UserHeaderField(HeaderKind.fromCode(kindCode), value);
+ } catch (IggyInvalidArgumentException unknownKind) {
+ return null;
+ }
}
public static Stats readStats(ByteBuf response) {
@@ -695,4 +759,6 @@
private static byte[] newByteArray(Long size) {
return new byte[size.intValue()];
}
+
+ private record UserHeaderField(HeaderKind kind, byte[] value) {}
}
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesSerializer.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesSerializer.java
index 8448e50..6a67b13 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesSerializer.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesSerializer.java
@@ -19,6 +19,7 @@
package org.apache.iggy.serde;
+import com.dynatrace.hash4j.hashing.Hashing;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.apache.commons.lang3.ArrayUtils;
@@ -29,8 +30,10 @@
import org.apache.iggy.message.HeaderValue;
import org.apache.iggy.message.Message;
import org.apache.iggy.message.MessageHeader;
+import org.apache.iggy.message.MessageId;
import org.apache.iggy.message.Partitioning;
import org.apache.iggy.message.PollingStrategy;
+import org.apache.iggy.message.UuidMessageId;
import org.apache.iggy.user.GlobalPermissions;
import org.apache.iggy.user.Permissions;
import org.apache.iggy.user.StreamPermissions;
@@ -38,8 +41,11 @@
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.UUID;
/**
* Unified serializer for both blocking and async clients.
@@ -47,6 +53,9 @@
*/
public final class BytesSerializer {
+ /** Size of the batch header on the wire; bytes past the stamped fields stay zero. */
+ static final int BATCH_HEADER_SIZE = 256;
+
/**
* Key and value length bound, in encoded bytes rather than characters. Belongs to the
* header-field codec that both user headers and resource options ride, so the server refuses a
@@ -54,6 +63,12 @@
*/
private static final int MAX_HEADER_FIELD_LENGTH = 255;
+ /** The timestamp delta is a u32 microsecond offset from the batch origin timestamp. */
+ private static final BigInteger MAX_TIMESTAMP_DELTA_MICROS = BigInteger.valueOf(0xFFFF_FFFFL);
+
+ /** Batch checksum input: five u64 header fields plus the u32 message count. */
+ private static final int BATCH_CHECKSUM_FIXED_INPUT_BYTES = 5 * Long.BYTES + Integer.BYTES;
+
private BytesSerializer() {}
public static ByteBuf toBytes(Consumer consumer) {
@@ -89,27 +104,6 @@
return buffer;
}
- public static ByteBuf toBytes(Message message) {
- var buffer = Unpooled.buffer(message.getSize());
- buffer.writeBytes(toBytes(message.header()));
- buffer.writeBytes(message.payload());
- buffer.writeBytes(toBytes(message.userHeaders()));
- return buffer;
- }
-
- public static ByteBuf toBytes(MessageHeader header) {
- var buffer = Unpooled.buffer(MessageHeader.SIZE);
- buffer.writeBytes(toBytesAsU64(header.checksum()));
- buffer.writeBytes(header.id().toBytes());
- buffer.writeBytes(toBytesAsU64(header.offset()));
- buffer.writeBytes(toBytesAsU64(header.timestamp()));
- buffer.writeBytes(toBytesAsU64(header.originTimestamp()));
- buffer.writeIntLE(header.userHeadersLength().intValue());
- buffer.writeIntLE(header.payloadLength().intValue());
- buffer.writeBytes(toBytesAsU64(header.reserved()));
- return buffer;
- }
-
public static ByteBuf toBytes(PollingStrategy strategy) {
var buffer = Unpooled.buffer(9);
buffer.writeByte(strategy.kind().asCode());
@@ -259,6 +253,114 @@
}
/**
+ * Encodes messages as one batch record: a batch header followed by per-message frames.
+ * The server stamps {@code partition_id}, {@code base_offset}, and {@code base_timestamp},
+ * so they are encoded as zero here.
+ */
+ public static ByteBuf toMessagesBatch(List<Message> messages) {
+ if (messages.isEmpty()) {
+ throw new IggyInvalidArgumentException("Cannot encode an empty message batch");
+ }
+ List<RawMessage> rawMessages = new ArrayList<>(messages.size());
+ for (Message message : messages) {
+ rawMessages.add(new RawMessage(
+ encodedMessageId(message.header().id()),
+ message.header().originTimestamp(),
+ message.payload(),
+ readAllBytes(toBytes(message.userHeaders()))));
+ }
+ return encodeBatch(rawMessages);
+ }
+
+ static ByteBuf encodeBatch(List<RawMessage> messages) {
+ var batchOriginTimestamp = messages.stream()
+ .map(RawMessage::originTimestamp)
+ .min(BigInteger::compareTo)
+ .orElseThrow(() -> new IggyInvalidArgumentException("Cannot encode an empty message batch"));
+ var blobLength = 0;
+ for (RawMessage message : messages) {
+ blobLength += MessageHeader.SIZE + message.payload().length + message.userHeaders().length;
+ }
+
+ var batch = Unpooled.buffer(BATCH_HEADER_SIZE + blobLength);
+ batch.writeZero(BATCH_HEADER_SIZE);
+ for (int index = 0; index < messages.size(); index++) {
+ RawMessage message = messages.get(index);
+ var timestampDelta = message.originTimestamp().subtract(batchOriginTimestamp);
+ if (timestampDelta.compareTo(MAX_TIMESTAMP_DELTA_MICROS) > 0) {
+ throw new IggyInvalidArgumentException("Message origin timestamp exceeds the batch origin by "
+ + timestampDelta + " microseconds, more than the timestamp delta field can hold");
+ }
+ var frameStart = batch.writerIndex();
+ batch.writeLongLE(0); // checksum, backpatched below
+ batch.writeBytes(message.id());
+ batch.writeIntLE(index); // offset_delta
+ batch.writeIntLE(timestampDelta.intValue());
+ batch.writeIntLE(message.userHeaders().length);
+ batch.writeIntLE(message.payload().length);
+ batch.writeLongLE(0); // reserved
+ batch.writeBytes(message.payload());
+ batch.writeBytes(message.userHeaders());
+ batch.setLongLE(
+ frameStart, xxHash3(batch, frameStart + Long.BYTES, batch.writerIndex() - frameStart - Long.BYTES));
+ }
+
+ long batchLength = BATCH_HEADER_SIZE + blobLength;
+ batch.setBytes(24, toBytesAsU64(batchOriginTimestamp));
+ batch.setLongLE(32, batchLength);
+ batch.setLongLE(40, batchChecksum(batch, batchOriginTimestamp, batchLength, messages));
+ batch.setIntLE(48, messages.size());
+ return batch;
+ }
+
+ /**
+ * The batch checksum covers the header meta fields and each frame's checksum field, not the
+ * message bodies; bodies are bound through the per-frame checksums.
+ */
+ private static long batchChecksum(
+ ByteBuf batch, BigInteger batchOriginTimestamp, long batchLength, List<RawMessage> messages) {
+ var input = Unpooled.buffer(BATCH_CHECKSUM_FIXED_INPUT_BYTES + Long.BYTES * messages.size());
+ input.writeLongLE(0); // partition_id
+ input.writeLongLE(0); // base_offset
+ input.writeLongLE(0); // base_timestamp
+ input.writeBytes(toBytesAsU64(batchOriginTimestamp));
+ input.writeLongLE(batchLength);
+ input.writeIntLE(messages.size());
+ var frameStart = BATCH_HEADER_SIZE;
+ for (RawMessage message : messages) {
+ input.writeLongLE(batch.getLongLE(frameStart));
+ frameStart += MessageHeader.SIZE + message.payload().length + message.userHeaders().length;
+ }
+ return xxHash3(input, 0, input.readableBytes());
+ }
+
+ /**
+ * The frame checksum covers the id, so a zero id is minted client-side before encoding
+ * rather than assigned by the server.
+ */
+ private static byte[] encodedMessageId(MessageId id) {
+ if (id.toBigInteger().signum() == 0) {
+ return readAllBytes(new UuidMessageId(UUID.randomUUID()).toBytes());
+ }
+ return readAllBytes(id.toBytes());
+ }
+
+ private static long xxHash3(ByteBuf buffer, int index, int length) {
+ if (buffer.hasArray()) {
+ return Hashing.xxh3_64().hashBytesToLong(buffer.array(), buffer.arrayOffset() + index, length);
+ }
+ var bytes = new byte[length];
+ buffer.getBytes(index, bytes);
+ return Hashing.xxh3_64().hashBytesToLong(bytes);
+ }
+
+ private static byte[] readAllBytes(ByteBuf buffer) {
+ var bytes = new byte[buffer.readableBytes()];
+ buffer.readBytes(bytes);
+ return bytes;
+ }
+
+ /**
* Rejects a key or value the TLV codec cannot express.
*
* <p>The {@code HeaderKey} / {@code HeaderValue} factories bound what they build, but both are
@@ -272,4 +374,16 @@
+ " bytes, must be between 1 and " + MAX_HEADER_FIELD_LENGTH);
}
}
+
+ /**
+ * One message as it enters the batch encoder: the id already encoded to its 16 wire bytes
+ * and the user headers already encoded to their opaque bytes.
+ */
+ record RawMessage(byte[] id, BigInteger originTimestamp, byte[] payload, byte[] userHeaders) {
+ RawMessage {
+ if (id.length != 16) {
+ throw new IggyInvalidArgumentException("Message id must have 16 bytes");
+ }
+ }
+ }
}
diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/BaseIntegrationTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/BaseIntegrationTest.java
index fd24f17..7c94728 100644
--- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/BaseIntegrationTest.java
+++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/BaseIntegrationTest.java
@@ -58,11 +58,18 @@
private static final boolean USE_EXTERNAL_SERVER = System.getenv("USE_EXTERNAL_SERVER") != null;
public static int serverTcpPort() {
- return USE_EXTERNAL_SERVER ? TCP_PORT : iggyServer.getMappedPort(TCP_PORT);
+ return USE_EXTERNAL_SERVER ? externalPort("EXTERNAL_TCP_PORT", TCP_PORT) : iggyServer.getMappedPort(TCP_PORT);
}
public static int serverHttpPort() {
- return USE_EXTERNAL_SERVER ? HTTP_PORT : iggyServer.getMappedPort(HTTP_PORT);
+ return USE_EXTERNAL_SERVER
+ ? externalPort("EXTERNAL_HTTP_PORT", HTTP_PORT)
+ : iggyServer.getMappedPort(HTTP_PORT);
+ }
+
+ private static int externalPort(String variable, int defaultPort) {
+ var configured = System.getenv(variable);
+ return configured != null ? Integer.parseInt(configured) : defaultPort;
}
public static String serverHost() {
diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/BytesSerializerTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/BytesSerializerTest.java
index 13893f0..091ffc1 100644
--- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/BytesSerializerTest.java
+++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/BytesSerializerTest.java
@@ -25,11 +25,8 @@
import org.apache.iggy.exception.IggyInvalidArgumentException;
import org.apache.iggy.identifier.ConsumerId;
import org.apache.iggy.identifier.StreamId;
-import org.apache.iggy.message.BytesMessageId;
import org.apache.iggy.message.HeaderKey;
import org.apache.iggy.message.HeaderValue;
-import org.apache.iggy.message.Message;
-import org.apache.iggy.message.MessageHeader;
import org.apache.iggy.message.Partitioning;
import org.apache.iggy.message.PollingStrategy;
import org.apache.iggy.serde.BytesSerializer;
@@ -492,105 +489,6 @@
}
@Nested
- class MessageSerialization {
-
- @Test
- void shouldSerializeMessageWithoutUserHeaders() {
- // given
- var messageId = new BytesMessageId(new byte[16]);
- var header = new MessageHeader(
- BigInteger.valueOf(123), // checksum
- messageId,
- BigInteger.valueOf(0), // offset
- BigInteger.valueOf(1000), // timestamp
- BigInteger.valueOf(1000), // originTimestamp
- 0L, // userHeadersLength
- 5L, // payloadLength
- BigInteger.ZERO // reserved
- );
- byte[] payload = "hello".getBytes();
- var message = new Message(header, payload, new HashMap<>());
-
- // when
- ByteBuf result = BytesSerializer.toBytes(message);
-
- // then
- assertThat(result.readableBytes()).isEqualTo(MessageHeader.SIZE + 5); // header + payload, no user headers
- }
-
- @Test
- void shouldSerializeMessageWithUserHeaders() {
- // given
- var messageId = new BytesMessageId(new byte[16]);
- Map<HeaderKey, HeaderValue> userHeaders = new HashMap<>();
- userHeaders.put(HeaderKey.fromString("key"), HeaderValue.fromRaw("val".getBytes()));
-
- // Calculate user headers size
- ByteBuf headersBuf = BytesSerializer.toBytes(userHeaders);
- int userHeadersLength = headersBuf.readableBytes();
-
- var header = new MessageHeader(
- BigInteger.ZERO,
- messageId,
- BigInteger.ZERO,
- BigInteger.valueOf(1000),
- BigInteger.valueOf(1000),
- (long) userHeadersLength,
- 3L, // "abc".length()
- BigInteger.ZERO // reserved
- );
- byte[] payload = "abc".getBytes();
- var message = new Message(header, payload, userHeaders);
-
- // when
- ByteBuf result = BytesSerializer.toBytes(message);
-
- // then
- assertThat(result.readableBytes()).isEqualTo(MessageHeader.SIZE + 3 + userHeadersLength);
- }
- }
-
- @Nested
- class MessageHeaderSerialization {
-
- @Test
- void shouldSerializeMessageHeader() {
- // given
- var messageId = new BytesMessageId(new byte[16]);
- var header = new MessageHeader(
- BigInteger.valueOf(999), // checksum
- messageId,
- BigInteger.valueOf(42), // offset
- BigInteger.valueOf(2000), // timestamp
- BigInteger.valueOf(1999), // originTimestamp
- 10L, // userHeadersLength
- 100L, // payloadLength
- BigInteger.ZERO // reserved
- );
-
- // when
- ByteBuf result = BytesSerializer.toBytes(header);
-
- // then
- assertThat(result.readableBytes()).isEqualTo(MessageHeader.SIZE);
- // Read checksum (8 bytes)
- result.skipBytes(8);
- // Read message ID (16 bytes)
- result.skipBytes(16);
- // Read offset (8 bytes)
- result.skipBytes(8);
- // Read timestamp (8 bytes)
- result.skipBytes(8);
- // Read origin timestamp (8 bytes)
- result.skipBytes(8);
- // Read user headers length (4 bytes)
- assertThat(result.readIntLE()).isEqualTo(10);
- // Read payload length (4 bytes)
- assertThat(result.readIntLE()).isEqualTo(100);
- }
- }
-
- @Nested
class PermissionsSerialization {
@Test
diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/message/MessageTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/message/MessageTest.java
index ba6b992..48cb5a8 100644
--- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/message/MessageTest.java
+++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/message/MessageTest.java
@@ -145,14 +145,14 @@
void getSizeReturnsExpectedSizeWhenThereAreNoUserHeaders() {
var message = Message.of("foo");
- assertThat(message.getSize()).isEqualTo(67);
+ assertThat(message.getSize()).isEqualTo(51);
}
@Test
void getSizeReturnsExpectedSizeWhenThereAreUserHeaders() {
var message = Message.of("foo", Map.of(HeaderKey.fromString("k1"), HeaderValue.fromInt32(123)));
- assertThat(message.getSize()).isEqualTo(83);
+ assertThat(message.getSize()).isEqualTo(67);
}
private void assertDefaultMessageHeaderValues(MessageHeader header) {
diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java
index d36cdb5..56e05eb 100644
--- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java
+++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java
@@ -53,8 +53,6 @@
import static org.apache.iggy.serde.BytesDeserializer.readPartition;
import static org.apache.iggy.serde.BytesDeserializer.readPermissions;
import static org.apache.iggy.serde.BytesDeserializer.readPersonalAccessTokenInfo;
-import static org.apache.iggy.serde.BytesDeserializer.readPolledMessage;
-import static org.apache.iggy.serde.BytesDeserializer.readPolledMessages;
import static org.apache.iggy.serde.BytesDeserializer.readRawPersonalAccessToken;
import static org.apache.iggy.serde.BytesDeserializer.readSendMessagesResponse;
import static org.apache.iggy.serde.BytesDeserializer.readStats;
@@ -445,96 +443,6 @@
}
@Nested
- class MessageDeserialization {
-
- @Test
- void shouldDeserializePolledMessageWithoutUserHeaders() {
- // given
- ByteBuf buffer = Unpooled.buffer();
- writeU64(buffer, BigInteger.valueOf(123)); // checksum
- buffer.writeBytes(new byte[16]); // message ID
- writeU64(buffer, BigInteger.ZERO); // offset
- writeU64(buffer, BigInteger.valueOf(1000)); // timestamp
- writeU64(buffer, BigInteger.valueOf(1000)); // origin timestamp
- buffer.writeIntLE(0); // user headers length
- buffer.writeIntLE(5); // payload length
- writeU64(buffer, BigInteger.ZERO); // reserved
- buffer.writeBytes("hello".getBytes()); // payload
-
- // when
- var message = readPolledMessage(buffer);
-
- // then
- assertThat(message.header().checksum()).isEqualTo(BigInteger.valueOf(123));
- assertThat(message.header().payloadLength()).isEqualTo(5L);
- assertThat(message.payload()).isEqualTo("hello".getBytes());
- assertThat(message.userHeaders()).isEmpty();
- }
-
- @Test
- void shouldDeserializePolledMessageWithUserHeaders() {
- // given
- ByteBuf buffer = Unpooled.buffer();
- writeU64(buffer, BigInteger.ZERO);
- buffer.writeBytes(new byte[16]);
- writeU64(buffer, BigInteger.ZERO);
- writeU64(buffer, BigInteger.valueOf(1000));
- writeU64(buffer, BigInteger.valueOf(1000));
-
- // Calculate and write user headers
- ByteBuf headersBuffer = Unpooled.buffer();
- headersBuffer.writeByte(HeaderKind.String.asCode());
- headersBuffer.writeIntLE(3);
- headersBuffer.writeBytes("key".getBytes());
- headersBuffer.writeByte(HeaderKind.Raw.asCode());
- headersBuffer.writeIntLE(3);
- headersBuffer.writeBytes("val".getBytes());
-
- buffer.writeIntLE(headersBuffer.readableBytes()); // user headers length
- buffer.writeIntLE(3); // payload length
- writeU64(buffer, BigInteger.ZERO); // reserved
- buffer.writeBytes("abc".getBytes()); // payload
- buffer.writeBytes(headersBuffer); // user headers
-
- // when
- var message = readPolledMessage(buffer);
-
- // then
- assertThat(message.userHeaders()).hasSize(1);
- assertThat(message.userHeaders().get(HeaderKey.fromString("key")).asRaw())
- .isEqualTo("val".getBytes());
- }
-
- @Test
- void shouldDeserializePolledMessages() {
- // given
- ByteBuf buffer = Unpooled.buffer();
- buffer.writeIntLE(1); // partition ID
- writeU64(buffer, BigInteger.valueOf(10)); // current offset
- buffer.writeIntLE(1); // messages count
- // Write one message
- writeU64(buffer, BigInteger.ZERO);
- buffer.writeBytes(new byte[16]);
- writeU64(buffer, BigInteger.ZERO);
- writeU64(buffer, BigInteger.valueOf(1000));
- writeU64(buffer, BigInteger.valueOf(1000));
- buffer.writeIntLE(0);
- buffer.writeIntLE(2);
- writeU64(buffer, BigInteger.ZERO); // reserved
- buffer.writeBytes("hi".getBytes());
-
- // when
- var polledMessages = readPolledMessages(buffer);
-
- // then
- assertThat(polledMessages.partitionId()).isEqualTo(1L);
- assertThat(polledMessages.currentOffset()).isEqualTo(BigInteger.valueOf(10));
- assertThat(polledMessages.count()).isEqualTo(1L);
- assertThat(polledMessages.messages()).hasSize(1);
- }
- }
-
- @Nested
class SendMessagesResponseDeserialization {
private ByteBuf singleConfirmation() {
diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/MessagesBatchWireFormatTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/MessagesBatchWireFormatTest.java
new file mode 100644
index 0000000..056e563
--- /dev/null
+++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/MessagesBatchWireFormatTest.java
@@ -0,0 +1,193 @@
+/*
+ * 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.
+ */
+
+package org.apache.iggy.serde;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import org.apache.iggy.exception.IggyInvalidArgumentException;
+import org.apache.iggy.exception.IggyMalformedResponseException;
+import org.apache.iggy.message.BigIntegerMessageId;
+import org.apache.iggy.message.HeaderKey;
+import org.apache.iggy.message.HeaderValue;
+import org.apache.iggy.message.Message;
+import org.apache.iggy.message.MessageHeader;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Golden vectors generated by the Rust implementation: the batch encoded for
+ * {@code SendMessages} and the poll body serving it back.
+ */
+class MessagesBatchWireFormatTest {
+
+ private static final String PRODUCE_BATCH_ONLY =
+ "000000000000000000000000000000000000000000000000e8030000000000008c01000000000000a91f38c86307267c02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bfd2b9205a759675070000000000000000000000000000000000000000000000000000000d000000000000000000000066697273742d7061796c6f6164d66b7e1c758eb7c0080000000000000000000000000000000100000032000000110000000e00000000000000000000007365636f6e642d7061796c6f6164757365722d6865616465722d6279746573";
+
+ private static final String POLL_BODY =
+ "03000000650000000000000002000000030000000000000064000000000000008813000000000000e8030000000000008c01000000000000c96826b38a8feed202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bfd2b9205a759675070000000000000000000000000000000000000000000000000000000d000000000000000000000066697273742d7061796c6f6164d66b7e1c758eb7c0080000000000000000000000000000000100000032000000110000000e00000000000000000000007365636f6e642d7061796c6f6164757365722d6865616465722d6279746573";
+
+ @Test
+ void shouldEncodeGoldenProduceBatch() {
+ var messages = List.of(
+ new BytesSerializer.RawMessage(
+ idBytes(7),
+ BigInteger.valueOf(1000),
+ "first-payload".getBytes(StandardCharsets.UTF_8),
+ new byte[0]),
+ new BytesSerializer.RawMessage(
+ idBytes(8),
+ BigInteger.valueOf(1050),
+ "second-payload".getBytes(StandardCharsets.UTF_8),
+ "user-header-bytes".getBytes(StandardCharsets.UTF_8)));
+
+ var batch = BytesSerializer.encodeBatch(messages);
+
+ assertThat(ByteBufUtil.hexDump(batch)).isEqualTo(PRODUCE_BATCH_ONLY);
+ }
+
+ @Test
+ void shouldDecodeGoldenPollBody() {
+ var polled = BytesDeserializer.readPolledMessages(Unpooled.wrappedBuffer(ByteBufUtil.decodeHexDump(POLL_BODY)));
+
+ assertThat(polled.partitionId()).isEqualTo(3L);
+ assertThat(polled.currentOffset()).isEqualTo(BigInteger.valueOf(101));
+ assertThat(polled.count()).isEqualTo(2L);
+ assertThat(polled.messages()).hasSize(2);
+
+ var first = polled.messages().get(0);
+ assertThat(first.header().offset()).isEqualTo(BigInteger.valueOf(100));
+ assertThat(first.header().timestamp()).isEqualTo(BigInteger.valueOf(5000));
+ assertThat(first.header().originTimestamp()).isEqualTo(BigInteger.valueOf(1000));
+ assertThat(first.header().id().toBigInteger()).isEqualTo(BigInteger.valueOf(7));
+ assertThat(first.header().checksum()).isEqualTo(new BigInteger("7596755a20b9d2bf", 16));
+ assertThat(first.payload()).isEqualTo("first-payload".getBytes(StandardCharsets.UTF_8));
+ assertThat(first.userHeaders()).isEmpty();
+
+ var second = polled.messages().get(1);
+ assertThat(second.header().offset()).isEqualTo(BigInteger.valueOf(101));
+ assertThat(second.header().timestamp()).isEqualTo(BigInteger.valueOf(5000));
+ assertThat(second.header().originTimestamp()).isEqualTo(BigInteger.valueOf(1050));
+ assertThat(second.header().id().toBigInteger()).isEqualTo(BigInteger.valueOf(8));
+ assertThat(second.header().checksum()).isEqualTo(new BigInteger("c0b78e751c7e6bd6", 16));
+ assertThat(second.payload()).isEqualTo("second-payload".getBytes(StandardCharsets.UTF_8));
+ // The golden vector carries opaque non-TLV user header bytes: reported
+ // by length, decoded to no entries.
+ assertThat(second.header().userHeadersLength()).isEqualTo(17L);
+ assertThat(second.userHeaders()).isEmpty();
+ }
+
+ @Test
+ void shouldRoundTripBatchThroughPollDecoding() {
+ var userHeaders =
+ Map.of(HeaderKey.fromString("k1"), HeaderValue.fromRaw("v1".getBytes(StandardCharsets.UTF_8)));
+ var batch = BytesSerializer.toMessagesBatch(
+ List.of(message(100, 2000, "first", Map.of()), message(200, 2500, "second", userHeaders)));
+
+ var polled = BytesDeserializer.readPolledMessages(pollBody(9, 1, 2, batch));
+
+ assertThat(polled.messages()).hasSize(2);
+ var first = polled.messages().get(0);
+ assertThat(first.header().id().toBigInteger()).isEqualTo(BigInteger.valueOf(100));
+ assertThat(first.header().offset()).isEqualTo(BigInteger.ZERO);
+ assertThat(first.header().originTimestamp()).isEqualTo(BigInteger.valueOf(2000));
+ assertThat(first.payload()).isEqualTo("first".getBytes(StandardCharsets.UTF_8));
+ assertThat(first.userHeaders()).isEmpty();
+ var second = polled.messages().get(1);
+ assertThat(second.header().id().toBigInteger()).isEqualTo(BigInteger.valueOf(200));
+ assertThat(second.header().offset()).isEqualTo(BigInteger.ONE);
+ assertThat(second.header().originTimestamp()).isEqualTo(BigInteger.valueOf(2500));
+ assertThat(second.payload()).isEqualTo("second".getBytes(StandardCharsets.UTF_8));
+ assertThat(second.userHeaders().get(HeaderKey.fromString("k1")).asRaw())
+ .isEqualTo("v1".getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Test
+ void shouldMintIdsForZeroIdMessages() {
+ var batch = BytesSerializer.toMessagesBatch(List.of(Message.of("a"), Message.of("b")));
+
+ var polled = BytesDeserializer.readPolledMessages(pollBody(1, 0, 2, batch));
+
+ var firstId = polled.messages().get(0).header().id().toBigInteger();
+ var secondId = polled.messages().get(1).header().id().toBigInteger();
+ assertThat(firstId).isNotEqualTo(BigInteger.ZERO);
+ assertThat(secondId).isNotEqualTo(BigInteger.ZERO);
+ assertThat(firstId).isNotEqualTo(secondId);
+ }
+
+ @Test
+ void shouldRejectEmptyBatch() {
+ assertThatThrownBy(() -> BytesSerializer.toMessagesBatch(List.of()))
+ .isInstanceOf(IggyInvalidArgumentException.class);
+ }
+
+ @Test
+ void shouldRejectTimestampDeltaExceedingU32() {
+ var messages = List.of(message(1, 0, "a", Map.of()), message(2, 0x1_0000_0000L, "b", Map.of()));
+
+ assertThatThrownBy(() -> BytesSerializer.toMessagesBatch(messages))
+ .isInstanceOf(IggyInvalidArgumentException.class);
+ }
+
+ @Test
+ void shouldRejectNonZeroFrameReserved() {
+ var body = ByteBufUtil.decodeHexDump(POLL_BODY);
+ // Poll header (16) + batch header (256) + reserved offset inside the first frame (40).
+ body[16 + 256 + 40] = 1;
+
+ assertThatThrownBy(() -> BytesDeserializer.readPolledMessages(Unpooled.wrappedBuffer(body)))
+ .isInstanceOf(IggyMalformedResponseException.class);
+ }
+
+ private static byte[] idBytes(long id) {
+ return ByteBufUtil.getBytes(BytesSerializer.toBytesAsU128(BigInteger.valueOf(id)));
+ }
+
+ private static Message message(
+ long id, long originTimestamp, String payload, Map<HeaderKey, HeaderValue> userHeaders) {
+ var payloadBytes = payload.getBytes(StandardCharsets.UTF_8);
+ var header = new MessageHeader(
+ BigInteger.ZERO,
+ new BigIntegerMessageId(BigInteger.valueOf(id)),
+ BigInteger.ZERO,
+ BigInteger.ZERO,
+ BigInteger.valueOf(originTimestamp),
+ 0L,
+ (long) payloadBytes.length,
+ BigInteger.ZERO);
+ return new Message(header, payloadBytes, userHeaders);
+ }
+
+ private static ByteBuf pollBody(long partitionId, long currentOffset, long count, ByteBuf batch) {
+ var body = Unpooled.buffer(16 + batch.readableBytes());
+ body.writeIntLE((int) partitionId);
+ body.writeLongLE(currentOffset);
+ body.writeIntLE((int) count);
+ body.writeBytes(batch);
+ return body;
+ }
+}
diff --git a/foreign/node/package-lock.json b/foreign/node/package-lock.json
index e58a7d5..d5445bd 100644
--- a/foreign/node/package-lock.json
+++ b/foreign/node/package-lock.json
@@ -1,14 +1,15 @@
{
"name": "apache-iggy",
- "version": "0.10.0-edge.3",
+ "version": "0.10.0-edge.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "apache-iggy",
- "version": "0.10.0-edge.3",
+ "version": "0.10.0-edge.4",
"license": "Apache-2.0",
"dependencies": {
+ "@node-rs/xxhash": "1.7.6",
"debug": "4.4.3",
"generic-pool": "3.9.0",
"uuidv7": "1.2.1"
@@ -608,7 +609,6 @@
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
"integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -620,7 +620,6 @@
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -631,7 +630,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
"integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -863,7 +861,6 @@
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
"integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -876,7 +873,6 @@
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@node-rs/xxhash/-/xxhash-1.7.6.tgz",
"integrity": "sha512-XMisO+aQHsVpxRp/85EszTtOQTOlhPbd149P/Xa9F55wafA6UM3h2UhOgOs7aAzItnHU/Aw1WQ1FVTEg7WB43Q==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -909,7 +905,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -926,7 +921,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -943,7 +937,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -960,7 +953,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -977,7 +969,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -994,7 +985,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1011,7 +1001,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1028,7 +1017,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1045,7 +1033,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1062,7 +1049,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1079,7 +1065,6 @@
"cpu": [
"wasm32"
],
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1096,7 +1081,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1113,7 +1097,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1130,7 +1113,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1822,7 +1804,6 @@
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -4225,7 +4206,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
+ "devOptional": true,
"license": "0BSD"
},
"node_modules/type-check": {
diff --git a/foreign/node/package.json b/foreign/node/package.json
index c906d5c..e31cee1 100644
--- a/foreign/node/package.json
+++ b/foreign/node/package.json
@@ -1,7 +1,7 @@
{
"name": "apache-iggy",
"type": "module",
- "version": "0.10.0-edge.3",
+ "version": "0.10.0-edge.4",
"description": "Official Apache Iggy NodeJS SDK",
"keywords": [
"iggy",
@@ -50,6 +50,7 @@
"author": "github.com/T1B0",
"license": "Apache-2.0",
"dependencies": {
+ "@node-rs/xxhash": "1.7.6",
"debug": "4.4.3",
"generic-pool": "3.9.0",
"uuidv7": "1.2.1"
diff --git a/foreign/node/scripts/check-vsr-protocol.mjs b/foreign/node/scripts/check-vsr-protocol.mjs
index ebfcf66..dcbe060 100644
--- a/foreign/node/scripts/check-vsr-protocol.mjs
+++ b/foreign/node/scripts/check-vsr-protocol.mjs
@@ -150,22 +150,22 @@
'Node EvictionReason differs from the Rust consensus enum'
);
-const rustCommand2Block =
- rustCommand.match(/pub enum Command2 \{([\s\S]*?)\n\}/)?.[1] ?? '';
-const rustCommand2 = enumValues(
- rustCommand2Block,
+const rustCommandBlock =
+ rustCommand.match(/pub enum Command \{([\s\S]*?)\n\}/)?.[1] ?? '';
+const rustCommandTable = enumValues(
+ rustCommandBlock,
/^\s+([A-Za-z0-9]+)\s*=\s*([0-9]+),$/gm
);
-const nodeCommand2 = enumValues(
- nodeHeader.match(/export const Command2 = \{([\s\S]*?)\n\}/)?.[1] ?? '',
+const nodeCommandTable = enumValues(
+ nodeHeader.match(/export const Command = \{([\s\S]*?)\n\}/)?.[1] ?? '',
/^\s+([A-Za-z0-9]+):\s*([0-9]+),?$/gm
);
-assert.ok(nodeCommand2.size > 0, 'Node Command2 table was not found');
-for (const [name, value] of nodeCommand2)
+assert.ok(nodeCommandTable.size > 0, 'Node Command table was not found');
+for (const [name, value] of nodeCommandTable)
assert.equal(
- rustCommand2.get(name),
+ rustCommandTable.get(name),
value,
- `Node Command2.${name} differs from Rust`
+ `Node Command.${name} differs from Rust`
);
const rustHeaderSize = Number(
@@ -185,7 +185,7 @@
['u32', [4, 4]],
['u64', [8, 8]],
['u128', [16, 16]],
- ['Command2', [1, 1]],
+ ['Command', [1, 1]],
['Operation', [1, 1]],
['EvictionReason', [1, 1]]
]);
diff --git a/foreign/node/src/client/client.connection.test.ts b/foreign/node/src/client/client.connection.test.ts
index 5c11c54..2564cfa 100644
--- a/foreign/node/src/client/client.connection.test.ts
+++ b/foreign/node/src/client/client.connection.test.ts
@@ -28,7 +28,7 @@
import { ProtocolFrameError } from './client.frame.js';
import { IggyConnection } from './client.connection.js';
import type { ClientConfig } from './client.type.js';
-import { Command2, HEADER_SIZE, REPLY_OFFSET } from '../wire/vsr/header.js';
+import { Command, HEADER_SIZE, REPLY_OFFSET } from '../wire/vsr/header.js';
const FRAME_LIMIT = 2 * HEADER_SIZE;
@@ -53,7 +53,7 @@
const replyFrame = (body: Buffer): Buffer => {
const frame = Buffer.alloc(HEADER_SIZE + body.length);
frame.writeUInt32LE(frame.length, REPLY_OFFSET.size);
- frame.writeUInt8(Command2.Reply, REPLY_OFFSET.command);
+ frame.writeUInt8(Command.Reply, REPLY_OFFSET.command);
body.copy(frame, HEADER_SIZE);
return frame;
};
diff --git a/foreign/node/src/client/client.connection.ts b/foreign/node/src/client/client.connection.ts
index 99bcda9..10b6863 100644
--- a/foreign/node/src/client/client.connection.ts
+++ b/foreign/node/src/client/client.connection.ts
@@ -27,7 +27,7 @@
ProtocolFrameError,
ResponseFrameDecoder
} from './client.frame.js';
-import { Command2, peekCommand } from '../wire/vsr/header.js';
+import { Command, peekCommand } from '../wire/vsr/header.js';
import { evictionError } from '../wire/vsr/reply.js';
@@ -425,7 +425,7 @@
try {
for (const response of this.responseDecoder.push(data)) {
- if (peekCommand(response) === Command2.Eviction)
+ if (peekCommand(response) === Command.Eviction)
this.emit('eviction', evictionError(response));
else
this.emit('response', response);
diff --git a/foreign/node/src/client/client.frame.test.ts b/foreign/node/src/client/client.frame.test.ts
index bdeba1e..7164cf0 100644
--- a/foreign/node/src/client/client.frame.test.ts
+++ b/foreign/node/src/client/client.frame.test.ts
@@ -23,7 +23,7 @@
ResponseFrameDecoder
} from './client.frame.js';
import {
- Command2,
+ Command,
HEADER_SIZE,
REPLY_OFFSET
} from '../wire/vsr/header.js';
@@ -33,7 +33,7 @@
const vsrFrame = (body: Buffer): Buffer => {
const frame = Buffer.alloc(HEADER_SIZE + body.length);
frame.writeUInt32LE(frame.length, REPLY_OFFSET.size);
- frame.writeUInt8(Command2.Reply, REPLY_OFFSET.command);
+ frame.writeUInt8(Command.Reply, REPLY_OFFSET.command);
body.copy(frame, HEADER_SIZE);
return frame;
};
diff --git a/foreign/node/src/client/client.socket.test.ts b/foreign/node/src/client/client.socket.test.ts
index 2f72cb6..0697c5f 100644
--- a/foreign/node/src/client/client.socket.test.ts
+++ b/foreign/node/src/client/client.socket.test.ts
@@ -26,7 +26,7 @@
import { COMMAND_CODE } from '../wire/command.code.js';
import { ResponseError } from '../wire/error.utils.js';
import {
- Command2,
+ Command,
EVICTION_OFFSET,
EvictionReason,
HEADER_SIZE,
@@ -100,7 +100,7 @@
): Buffer => {
const frame = Buffer.alloc(HEADER_SIZE + body.length);
frame.writeUInt32LE(frame.length, REPLY_OFFSET.size);
- frame.writeUInt8(Command2.Reply, REPLY_OFFSET.command);
+ frame.writeUInt8(Command.Reply, REPLY_OFFSET.command);
frame.writeUInt8(operation, REPLY_OFFSET.operation);
frame.writeUInt32LE(status, REPLY_OFFSET.status);
body.copy(frame, HEADER_SIZE);
@@ -110,7 +110,7 @@
const evictionFrame = (reason: number): Buffer => {
const frame = Buffer.alloc(HEADER_SIZE);
frame.writeUInt32LE(HEADER_SIZE, REPLY_OFFSET.size);
- frame.writeUInt8(Command2.Eviction, REPLY_OFFSET.command);
+ frame.writeUInt8(Command.Eviction, REPLY_OFFSET.command);
frame.writeUInt8(reason, EVICTION_OFFSET.reason);
return frame;
};
diff --git a/foreign/node/src/wire/command.code.ts b/foreign/node/src/wire/command.code.ts
index 972f00e..fd27a48 100644
--- a/foreign/node/src/wire/command.code.ts
+++ b/foreign/node/src/wire/command.code.ts
@@ -48,8 +48,6 @@
GetOffset: 120,
StoreOffset: 121,
DeleteConsumerOffset: 122,
- StoreOffset2: 123,
- DeleteConsumerOffset2: 124,
GetStream: 200,
GetStreams: 201,
CreateStream: 202,
diff --git a/foreign/node/src/wire/message/iggy-header.utils.ts b/foreign/node/src/wire/message/iggy-header.utils.ts
index fb285b4..cd4c53b 100644
--- a/foreign/node/src/wire/message/iggy-header.utils.ts
+++ b/foreign/node/src/wire/message/iggy-header.utils.ts
@@ -16,11 +16,160 @@
// under the License.
//
-import { toDate } from "../serialize.utils.js";
+import { xxh3 } from "@node-rs/xxhash";
import { u128LEBufToBigint } from "../number.utils.js";
/**
- * Iggy message header containing metadata for each message.
+ * Size of the batch header in bytes.
+ * Layout: u64 (partitionId) + u64 (baseOffset) + u64 (baseTimestamp) +
+ * u64 (originTimestamp) + u64 (batchLength) + u64 (batchChecksum) +
+ * u32 (messageCount) + zero padding up to 256 bytes.
+ */
+export const BATCH_HEADER_SIZE = 256;
+
+/**
+ * Size of the per-message frame header in bytes.
+ * Layout: u64 (checksum) + u128 (id) + u32 (offsetDelta) +
+ * u32 (timestampDelta) + u32 (userHeadersLength) + u32 (payloadLength) +
+ * u64 (reserved).
+ */
+export const FRAME_HEADER_SIZE = 48;
+
+/** Size of the frame checksum field prefixing the frame header. */
+const FRAME_CHECKSUM_SIZE = 8;
+
+/**
+ * Batch header describing a run of message frames.
+ */
+export type BatchHeader = {
+ /** Partition the batch belongs to (zero when sent by a client) */
+ partitionId: bigint;
+ /** Offset of the first message in the batch (zero when sent by a client) */
+ baseOffset: bigint;
+ /** Server timestamp of the batch in microseconds (zero when sent by a client) */
+ baseTimestamp: bigint;
+ /** Smallest origin timestamp of the batched messages in microseconds */
+ originTimestamp: bigint;
+ /** Total batch size in bytes, header included */
+ batchLength: bigint;
+ /** XXH3-64 checksum of the batch header fields and frame checksums */
+ batchChecksum: bigint;
+ /** Number of message frames in the batch */
+ messageCount: number;
+};
+
+/**
+ * Per-message frame header.
+ */
+export type FrameHeader = {
+ /** XXH3-64 checksum of the frame past this field, payload and user headers included */
+ checksum: bigint;
+ /** Unique message identifier */
+ id: bigint;
+ /** Index of the message within the batch */
+ offsetDelta: number;
+ /** Message origin timestamp minus batch origin timestamp in microseconds */
+ timestampDelta: number;
+ /** Length of user-defined headers in bytes */
+ userHeadersLength: number;
+ /** Length of message payload in bytes */
+ payloadLength: number;
+ /** Reserved for future use, must be zero */
+ reserved: bigint;
+};
+
+/**
+ * Serializes a batch header to its 256-byte wire format.
+ *
+ * @param header - Batch header to serialize
+ * @returns Serialized batch header buffer
+ */
+export const serializeBatchHeader = (header: BatchHeader): Buffer => {
+ const b = Buffer.alloc(BATCH_HEADER_SIZE);
+ b.writeBigUInt64LE(header.partitionId, 0);
+ b.writeBigUInt64LE(header.baseOffset, 8);
+ b.writeBigUInt64LE(header.baseTimestamp, 16);
+ b.writeBigUInt64LE(header.originTimestamp, 24);
+ b.writeBigUInt64LE(header.batchLength, 32);
+ b.writeBigUInt64LE(header.batchChecksum, 40);
+ b.writeUInt32LE(header.messageCount, 48);
+ return b;
+};
+
+/**
+ * Deserializes a batch header from a buffer.
+ *
+ * @param b - Buffer containing the serialized batch header
+ * @param pos - Starting position in the buffer
+ * @returns Parsed BatchHeader object
+ */
+export const deserializeBatchHeader = (b: Buffer, pos = 0): BatchHeader => ({
+ partitionId: b.readBigUInt64LE(pos),
+ baseOffset: b.readBigUInt64LE(pos + 8),
+ baseTimestamp: b.readBigUInt64LE(pos + 16),
+ originTimestamp: b.readBigUInt64LE(pos + 24),
+ batchLength: b.readBigUInt64LE(pos + 32),
+ batchChecksum: b.readBigUInt64LE(pos + 40),
+ messageCount: b.readUInt32LE(pos + 48),
+});
+
+/**
+ * Deserializes a frame header from a buffer.
+ *
+ * @param b - Buffer containing the serialized frame header
+ * @param pos - Starting position in the buffer
+ * @returns Parsed FrameHeader object
+ */
+export const deserializeFrameHeader = (b: Buffer, pos = 0): FrameHeader => ({
+ checksum: b.readBigUInt64LE(pos),
+ id:
+ b.readBigUInt64LE(pos + 8) | (b.readBigUInt64LE(pos + 16) << 64n),
+ offsetDelta: b.readUInt32LE(pos + 24),
+ timestampDelta: b.readUInt32LE(pos + 28),
+ userHeadersLength: b.readUInt32LE(pos + 32),
+ payloadLength: b.readUInt32LE(pos + 36),
+ reserved: b.readBigUInt64LE(pos + 40),
+});
+
+/**
+ * Computes the XXH3-64 checksum of a complete frame.
+ * Covers everything past the checksum field: the remaining frame header,
+ * the payload, and the user headers.
+ *
+ * @param frame - Complete frame buffer [header][payload][user headers]
+ * @returns Frame checksum
+ */
+export const frameChecksum = (frame: Buffer): bigint =>
+ xxh3.xxh64(frame.subarray(FRAME_CHECKSUM_SIZE));
+
+/**
+ * Computes the XXH3-64 checksum of a batch.
+ * Covers the batch header fields up to the checksum, the message count,
+ * and each frame checksum in message order.
+ *
+ * @param header - Batch header fields, batchChecksum ignored
+ * @param frameChecksums - Frame checksums in message order
+ * @returns Batch checksum
+ */
+export const batchChecksum = (
+ header: Omit<BatchHeader, "batchChecksum" | "messageCount">,
+ frameChecksums: bigint[],
+): bigint => {
+ const b = Buffer.allocUnsafe(44 + frameChecksums.length * 8);
+ b.writeBigUInt64LE(header.partitionId, 0);
+ b.writeBigUInt64LE(header.baseOffset, 8);
+ b.writeBigUInt64LE(header.baseTimestamp, 16);
+ b.writeBigUInt64LE(header.originTimestamp, 24);
+ b.writeBigUInt64LE(header.batchLength, 32);
+ b.writeUInt32LE(frameChecksums.length, 40);
+ frameChecksums.forEach((checksum, index) => {
+ b.writeBigUInt64LE(checksum, 44 + index * 8);
+ });
+ return xxh3.xxh64(b);
+};
+
+/**
+ * Iggy message header containing metadata for each polled message.
*/
export type IggyMessageHeader = {
/** Message checksum for integrity verification */
@@ -42,67 +191,9 @@
};
/**
- * Size of the Iggy message header in bytes.
- * Layout: u64 (checksum) + u128 (id) + u64 (offset) + u64 (timestamp) + u64 (originTimestamp) + u32 (userHeadersLength) + u32 (payloadLength) + u64 (reserved)
- */
-export const IGGY_MESSAGE_HEADER_SIZE = 8 + 16 + 8 + 8 + 8 + 4 + 4 + 8;
-
-/**
- * Serializes an Iggy message header to wire format.
- * Sets checksum, offset, and timestamp to zero (filled by server).
- *
- * @param id - Message ID as 16-byte buffer
- * @param payload - Message payload
- * @param userHeaders - Serialized user headers
- * @returns Serialized header buffer
- */
-export const serializeIggyMessageHeader = (
- id: Buffer,
- payload: Buffer,
- userHeaders: Buffer,
-) => {
- const b = Buffer.allocUnsafe(IGGY_MESSAGE_HEADER_SIZE);
- b.writeBigUInt64LE(0n, 0); // checksum u64
- b.fill(id, 8, 24); // id u128
- b.writeBigUInt64LE(0n, 24); // offset u64
- b.writeBigUInt64LE(0n, 32); // timestamp u64
- b.writeBigUint64LE(BigInt(new Date().getTime()), 40); // originTimestamp u64
- b.writeUInt32LE(userHeaders.length, 48); // userHeaders len u32
- b.writeUInt32LE(payload.length, 52); // payload len u32
- b.writeBigUInt64LE(0n, 56); // reserved u64
- return b;
-};
-
-/**
* Deserializes a message ID from a 16-byte buffer to BigInt.
*
* @param b - 16-byte buffer containing the message ID
* @returns Message ID as BigInt
*/
export const deserialiseMessageId = (b: Buffer) => u128LEBufToBigint(b);
-
-/**
- * Deserializes Iggy message headers from a buffer.
- *
- * @param b - Buffer containing the serialized header
- * @returns Parsed IggyMessageHeader object
- * @throws Error if buffer length doesn't match expected header size
- */
-export const deserializeIggyMessageHeaders = (b: Buffer) => {
- if (b.length !== IGGY_MESSAGE_HEADER_SIZE)
- throw new Error(
- `deserialize message headers error, length = ${b.length} ` +
- `expected ${IGGY_MESSAGE_HEADER_SIZE}`,
- );
- const headers: IggyMessageHeader = {
- checksum: b.readBigUInt64LE(0),
- id: deserialiseMessageId(b.subarray(8, 24)),
- offset: b.readBigUInt64LE(24),
- timestamp: toDate(b.readBigUInt64LE(32)),
- originTimestamp: toDate(b.readBigUInt64LE(40)),
- userHeadersLength: b.readUInt32LE(48),
- payloadLength: b.readUInt32LE(52),
- reserved: b.readBigUInt64LE(56),
- };
- return headers;
-};
diff --git a/foreign/node/src/wire/message/message-batch.test.ts b/foreign/node/src/wire/message/message-batch.test.ts
new file mode 100644
index 0000000..4cb14bb
--- /dev/null
+++ b/foreign/node/src/wire/message/message-batch.test.ts
@@ -0,0 +1,218 @@
+// 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");
+ });
+});
diff --git a/foreign/node/src/wire/message/message.utils.ts b/foreign/node/src/wire/message/message.utils.ts
index ae7e91e..a350f7b 100644
--- a/foreign/node/src/wire/message/message.utils.ts
+++ b/foreign/node/src/wire/message/message.utils.ts
@@ -16,18 +16,25 @@
// under the License.
//
-import Debug from 'debug';
+import { uuidv4 } from 'uuidv7';
import { uint32ToBuf, u128ToBuf, uint8ToBuf } from '../number.utils.js';
import { serializeHeaders, type Headers } from './header.utils.js';
import { serializeIdentifier, type Id } from '../identifier.utils.js';
import { serializePartitioning, type Partitioning } from './partitioning.utils.js';
import { parse as parseUUID } from '../uuid.utils.js';
-import { serializeIggyMessageHeader } from './iggy-header.utils.js';
+import {
+ BATCH_HEADER_SIZE,
+ FRAME_HEADER_SIZE,
+ batchChecksum,
+ frameChecksum,
+ serializeBatchHeader,
+} from './iggy-header.utils.js';
-const debug = Debug('iggy:client');
+/** Size of the message ID in bytes (u128) */
+const MESSAGE_ID_SIZE = 16;
-/** Size of the message index entry in bytes (16 bytes per message) */
-const INDEX_SIZE = 16;
+/** Largest representable frame timestamp delta (u32, microseconds) */
+const MAX_TIMESTAMP_DELTA = 0xFFFF_FFFFn;
/** Valid types for message ID: numeric, bigint, or UUID string */
export type MessageIdKind = number | bigint | string;
@@ -41,7 +48,23 @@
/** Optional user-defined headers */
headers?: Headers,
/** Message payload as string or Buffer */
- payload: string | Buffer
+ payload: string | Buffer,
+ /** Optional origin timestamp in microseconds (defaults to send time) */
+ originTimestamp?: bigint
+};
+
+/**
+ * A message prepared for batch encoding.
+ */
+export type MessageToEncode = {
+ /** Message ID as a 16-byte little-endian buffer */
+ id: Buffer,
+ /** Message payload */
+ payload: Buffer,
+ /** Serialized user headers */
+ userHeaders: Buffer,
+ /** Origin timestamp in microseconds */
+ originTimestamp: bigint
};
/**
@@ -57,7 +80,7 @@
'number' === typeof x;
/**
- * Serializes a message ID to a 16-byte buffer.
+ * Serializes a message ID to a 16-byte little-endian buffer.
* Supports undefined (zero), numeric, bigint, and UUID string formats.
*
* @param id - Message ID to serialize
@@ -70,7 +93,7 @@
throw new Error(`invalid message id: '${id}' (use uuid string | number | bigint >= 0)`)
if(id === undefined)
- return Buffer.alloc(16, 0); // 0u128
+ return Buffer.alloc(MESSAGE_ID_SIZE, 0); // 0u128
if ('bigint' === typeof id || 'number' === typeof id) {
if (id < 0)
@@ -82,7 +105,7 @@
try {
const uuid = parseUUID(id);
- return Buffer.from(uuid.toHex(), 'hex');
+ return u128ToBuf(BigInt(`0x${uuid.toHex()}`));
} catch (err) {
throw new Error(
`invalid message id: '${id}' (use uuid string | number | bigint >= 0)`,
@@ -93,76 +116,112 @@
}
/**
- * Serializes a single message to wire format.
- * Format: [iggy_header][payload][user_headers]
+ * Serializes a message ID, minting a random UUID when the ID is
+ * absent or zero.
*
- * @param msg - Message to serialize
- * @returns Serialized message buffer
+ * @param id - Optional message ID
+ * @returns 16-byte little-endian buffer containing a non-zero ID
*/
-export const serializeMessage = (msg: CreateMessage) => {
- const { id, headers, payload } = msg;
-
+const resolveMessageId = (id?: MessageIdKind): Buffer => {
const bId = serializeMessageId(id);
- const bUserHeaders = serializeHeaders(headers);
- const bPayload = 'string' === typeof payload ? Buffer.from(payload) : payload
- const bIggyMessageHeader = serializeIggyMessageHeader(bId, bPayload, bUserHeaders);
-
- const r = Buffer.concat([
- bIggyMessageHeader,
- bPayload,
- bUserHeaders
- ]);
-
- debug(
- 'id', bId.length, bId.toString('hex'),
- 'iggyHeaders', bIggyMessageHeader.length, bIggyMessageHeader.toString('hex'),
- 'userHeaders', bUserHeaders.length, bUserHeaders.toString('hex'),
- 'payload', bPayload.length, bPayload.toString('hex'),
- 'full len', r.length //, r.toString('hex')
- );
-
- return r;
+ return bId.every((byte) => byte === 0)
+ ? u128ToBuf(BigInt(`0x${uuidv4().replaceAll('-', '')}`))
+ : bId;
};
/**
- * Serializes multiple messages to an array of buffers.
+ * Serializes a single message frame.
+ * Format: [frame header][payload][user headers]
*
- * @param messages - Array of messages to serialize
- * @returns Array of serialized message buffers
+ * @param message - Message to serialize
+ * @param index - Index of the message within the batch
+ * @param batchOriginTimestamp - Origin timestamp of the batch in microseconds
+ * @returns Serialized frame buffer
+ * @throws Error if the timestamp delta exceeds u32
*/
-export const serializeMessages = (messages: CreateMessage[]) =>
- messages.map(c => serializeMessage(c));
+const serializeMessageFrame = (
+ { id, payload, userHeaders, originTimestamp }: MessageToEncode,
+ index: number,
+ batchOriginTimestamp: bigint
+): Buffer => {
+ if (id.length !== MESSAGE_ID_SIZE)
+ throw new Error(
+ `invalid message id length: ${id.length}, expected ${MESSAGE_ID_SIZE}`
+ );
+ const timestampDelta = originTimestamp - batchOriginTimestamp;
+ if (timestampDelta > MAX_TIMESTAMP_DELTA)
+ throw new Error(
+ `message timestamp delta ${timestampDelta} exceeds u32 range`
+ );
+
+ const frame = Buffer.alloc(
+ FRAME_HEADER_SIZE + payload.length + userHeaders.length
+ );
+ id.copy(frame, 8);
+ frame.writeUInt32LE(index, 24);
+ frame.writeUInt32LE(Number(timestampDelta), 28);
+ frame.writeUInt32LE(userHeaders.length, 32);
+ frame.writeUInt32LE(payload.length, 36);
+ payload.copy(frame, FRAME_HEADER_SIZE);
+ userHeaders.copy(frame, FRAME_HEADER_SIZE + payload.length);
+ frame.writeBigUInt64LE(frameChecksum(frame), 0);
+ return frame;
+};
/**
- * Creates an index buffer for a batch of messages.
- * Each index entry is 16 bytes tracking message positions.
+ * Encodes messages into the canonical batch format.
+ * Format: [batch header][frames], one frame per message.
*
- * @param messages - Array of serialized message buffers
- * @returns Index buffer
+ * @param messages - Messages to encode
+ * @returns Serialized batch buffer
+ * @throws Error if the batch is empty
*/
-export const createMessagesIndex = (messages: Buffer[]) => {
- const bIndex = Buffer.allocUnsafe(messages.length * INDEX_SIZE);
- let currentIndex = 0;
- let msgsSize = 0;
- messages.forEach(msg => {
- msgsSize += msg.length;
- bIndex.writeBigUInt64LE(0n, currentIndex)
- bIndex.writeUInt32LE(msgsSize, currentIndex + 4)
- bIndex.writeBigUInt64LE(0n, currentIndex + 8)
- currentIndex += INDEX_SIZE;
- });
- return bIndex;
-}
+export const encodeMessagesBatch = (messages: MessageToEncode[]): Buffer => {
+ if (messages.length === 0)
+ throw new Error('cannot encode an empty message batch');
+
+ const originTimestamp = messages.reduce(
+ (min, message) =>
+ message.originTimestamp < min ? message.originTimestamp : min,
+ messages[0].originTimestamp
+ );
+ const frames = messages.map((message, index) =>
+ serializeMessageFrame(message, index, originTimestamp)
+ );
+ const framesLength = frames.reduce((sum, frame) => sum + frame.length, 0);
+ const batchLength = BigInt(BATCH_HEADER_SIZE + framesLength);
+ const header = {
+ partitionId: 0n,
+ baseOffset: 0n,
+ baseTimestamp: 0n,
+ originTimestamp,
+ batchLength,
+ };
+
+ return Buffer.concat([
+ serializeBatchHeader({
+ ...header,
+ batchChecksum: batchChecksum(
+ header,
+ frames.map((frame) => frame.readBigUInt64LE(0))
+ ),
+ messageCount: messages.length,
+ }),
+ ...frames,
+ ]);
+};
/**
* Serializes a send messages command payload.
- * Includes stream/topic identifiers, partitioning, and all messages with index.
+ * Format: [metadata length][stream id][topic id][partitioning]
+ * [messages count][batch].
*
* @param streamId - Stream identifier
* @param topicId - Topic identifier
* @param messages - Array of messages to send
* @param partitioning - Optional partitioning strategy
* @returns Serialized command payload
+ * @throws Error if the message array is empty
*/
export const serializeSendMessages = (
streamId: Id,
@@ -170,6 +229,9 @@
messages: CreateMessage[],
partitioning?: Partitioning,
) => {
+ if (messages.length === 0)
+ throw new Error('cannot send an empty message batch');
+
const streamIdentifier = serializeIdentifier(streamId);
const topicIdentifier = serializeIdentifier(topicId);
const bPartitioning = serializePartitioning(partitioning);
@@ -179,8 +241,15 @@
bPartitioning.length + bMessagesCount.length
);
- const bMessagesArray = serializeMessages(messages);
- const bMessageIndex = createMessagesIndex(bMessagesArray);
+ const sendTimestamp = BigInt(Date.now()) * 1000n;
+ const bBatch = encodeMessagesBatch(messages.map(
+ ({ id, headers, payload, originTimestamp }) => ({
+ id: resolveMessageId(id),
+ payload: 'string' === typeof payload ? Buffer.from(payload) : payload,
+ userHeaders: serializeHeaders(headers),
+ originTimestamp: originTimestamp ?? sendTimestamp
+ })
+ ));
return Buffer.concat([
bMetadataLen,
@@ -188,8 +257,7 @@
topicIdentifier,
bPartitioning,
bMessagesCount,
- bMessageIndex,
- ...bMessagesArray
+ bBatch
]);
};
diff --git a/foreign/node/src/wire/message/poll.utils.ts b/foreign/node/src/wire/message/poll.utils.ts
index a425eaa..a92e06a 100644
--- a/foreign/node/src/wire/message/poll.utils.ts
+++ b/foreign/node/src/wire/message/poll.utils.ts
@@ -18,12 +18,15 @@
import { type Id } from "../identifier.utils.js";
import { type ValueOf, reverseRecord } from "../../type.utils.js";
+import { toDate } from "../serialize.utils.js";
import { serializeGetOffset, type Consumer } from "../offset/offset.utils.js";
import { deserializeHeaders, type ParsedHeaderEntry } from "./header.utils.js";
import { Transform, type TransformCallback } from "node:stream";
import {
- deserializeIggyMessageHeaders,
- IGGY_MESSAGE_HEADER_SIZE,
+ BATCH_HEADER_SIZE,
+ FRAME_HEADER_SIZE,
+ deserializeBatchHeader,
+ deserializeFrameHeader,
type IggyMessageHeader,
} from "./iggy-header.utils.js";
@@ -237,44 +240,108 @@
};
/**
- * Deserializes an array of messages from a buffer.
- *
- * @param b - Buffer containing serialized messages
- * @returns Array of deserialized messages
+ * A message frame decoded from a batch record, with absolute values
+ * resolved against the record header.
*/
-export const deserializeMessages = (b: Buffer) => {
- const messages: Message[] = [];
- let pos = 0;
+export type BatchMessage = {
+ /** Frame checksum, passed through unverified */
+ checksum: bigint;
+ /** Unique message identifier */
+ id: bigint;
+ /** Absolute message offset within the partition */
+ offset: bigint;
+ /** Server timestamp of the record in microseconds */
+ timestamp: bigint;
+ /** Message origin timestamp in microseconds */
+ originTimestamp: bigint;
+ /** Message payload data */
+ payload: Buffer;
+ /** Raw user header bytes */
+ userHeaders: Buffer;
+};
+
+/**
+ * Deserializes batch records into message frames with absolute values.
+ * Each record is [batch header][frames], walked by the record's batch
+ * length. Records may be server-sliced, so the first frame of a record
+ * can carry a non-zero offset delta.
+ *
+ * @param b - Buffer containing serialized batch records
+ * @param pos - Starting position in the buffer
+ * @returns Array of decoded message frames
+ * @throws Error if a record or frame is malformed
+ */
+export const deserializeBatchMessages = (
+ b: Buffer,
+ pos = 0,
+): BatchMessage[] => {
+ const messages: BatchMessage[] = [];
const len = b.length;
while (pos < len) {
- if (pos + IGGY_MESSAGE_HEADER_SIZE > len) break;
- const bHead = b.subarray(pos, pos + IGGY_MESSAGE_HEADER_SIZE);
- const headers = deserializeIggyMessageHeaders(bHead);
- pos += IGGY_MESSAGE_HEADER_SIZE;
- const plEnd = pos + headers.payloadLength;
- if (plEnd > len) break;
- const payload = b.subarray(pos, plEnd);
- pos += headers.payloadLength;
- let userHeaders: ParsedHeaderEntry[] = [];
- if (
- headers.userHeadersLength > 0 &&
- plEnd + headers.userHeadersLength <= len
- ) {
- userHeaders = deserializeHeaders(
- b.subarray(plEnd, plEnd + headers.userHeadersLength),
+ if (pos + BATCH_HEADER_SIZE > len)
+ throw new Error("truncated batch header in poll response");
+ const batch = deserializeBatchHeader(b, pos);
+ const recordEnd = pos + Number(batch.batchLength);
+ if (Number(batch.batchLength) < BATCH_HEADER_SIZE || recordEnd > len)
+ throw new Error(
+ `invalid batch length ${batch.batchLength} in poll response`,
);
- pos += headers.userHeadersLength;
+ pos += BATCH_HEADER_SIZE;
+ while (pos < recordEnd) {
+ if (pos + FRAME_HEADER_SIZE > recordEnd)
+ throw new Error("truncated message frame in poll response");
+ const frame = deserializeFrameHeader(b, pos);
+ if (frame.reserved !== 0n)
+ throw new Error(
+ `non-zero reserved field ${frame.reserved} in message frame`,
+ );
+ const payloadEnd = pos + FRAME_HEADER_SIZE + frame.payloadLength;
+ const frameEnd = payloadEnd + frame.userHeadersLength;
+ if (frameEnd > recordEnd)
+ throw new Error("truncated message frame in poll response");
+ messages.push({
+ checksum: frame.checksum,
+ id: frame.id,
+ offset: batch.baseOffset + BigInt(frame.offsetDelta),
+ timestamp: batch.baseTimestamp,
+ originTimestamp:
+ batch.originTimestamp + BigInt(frame.timestampDelta),
+ payload: b.subarray(pos + FRAME_HEADER_SIZE, payloadEnd),
+ userHeaders: b.subarray(payloadEnd, frameEnd),
+ });
+ pos = frameEnd;
}
- messages.push({
- headers,
- payload,
- userHeaders,
- });
}
return messages;
};
/**
+ * Deserializes an array of messages from a buffer of batch records.
+ *
+ * @param b - Buffer containing serialized batch records
+ * @param pos - Starting position in the buffer
+ * @returns Array of deserialized messages
+ */
+export const deserializeMessages = (b: Buffer, pos = 0): Message[] =>
+ deserializeBatchMessages(b, pos).map((message) => ({
+ headers: {
+ checksum: message.checksum,
+ id: message.id,
+ offset: message.offset,
+ timestamp: toDate(message.timestamp),
+ originTimestamp: toDate(message.originTimestamp),
+ userHeadersLength: message.userHeaders.length,
+ payloadLength: message.payload.length,
+ reserved: 0n,
+ },
+ payload: message.payload,
+ userHeaders:
+ message.userHeaders.length > 0
+ ? deserializeHeaders(message.userHeaders)
+ : ([] as ParsedHeaderEntry[]),
+ }));
+
+/**
* Deserializes a poll messages response from a buffer.
*
* @param r - Response buffer
@@ -285,7 +352,7 @@
const partitionId = r.readUInt32LE(pos);
const currentOffset = r.readBigUInt64LE(pos + 4);
const count = r.readUInt32LE(pos + 12);
- const messages = deserializeMessages(r.subarray(16));
+ const messages = deserializeMessages(r, pos + 16);
return {
partitionId,
diff --git a/foreign/node/src/wire/message/send-messages.command.test.ts b/foreign/node/src/wire/message/send-messages.command.test.ts
index ac35b9c..01fa75e 100644
--- a/foreign/node/src/wire/message/send-messages.command.test.ts
+++ b/foreign/node/src/wire/message/send-messages.command.test.ts
@@ -76,7 +76,9 @@
};
it("serialize SendMessages into a buffer", () => {
- assert.deepEqual(SEND_MESSAGES.serialize(t1).length, 589);
+ // metadata length prefix (4) + metadata (18) + batch header (256) +
+ // 7 frames of 48-byte header + 1-byte payload (343)
+ assert.deepEqual(SEND_MESSAGES.serialize(t1).length, 621);
});
it("serialize all kinds of messageId", () => {
diff --git a/foreign/node/src/wire/offset/delete-offset.command.ts b/foreign/node/src/wire/offset/delete-offset.command.ts
index 9cefe98..6d7f37f 100644
--- a/foreign/node/src/wire/offset/delete-offset.command.ts
+++ b/foreign/node/src/wire/offset/delete-offset.command.ts
@@ -19,7 +19,7 @@
import { deserializeVoidResponse } from '../../client/client.utils.js';
import type { GetOffset } from './get-offset.command.js';
import { wrapCommand } from '../command.utils.js';
-import { serializeGetOffset } from './offset.utils.js';
+import { serializeDeleteOffset } from './offset.utils.js';
import { COMMAND_CODE } from '../command.code.js';
@@ -37,7 +37,7 @@
code: COMMAND_CODE.DeleteConsumerOffset,
serialize: ({ streamId, topicId, consumer, partitionId = 0 }: DeleteOffset) => {
- return serializeGetOffset(streamId, topicId, consumer, partitionId);
+ return serializeDeleteOffset(streamId, topicId, consumer, partitionId);
},
deserialize: deserializeVoidResponse
diff --git a/foreign/node/src/wire/offset/offset.utils.ts b/foreign/node/src/wire/offset/offset.utils.ts
index 84c9f2f..4a7c859 100644
--- a/foreign/node/src/wire/offset/offset.utils.ts
+++ b/foreign/node/src/wire/offset/offset.utils.ts
@@ -20,6 +20,9 @@
import { serializeIdentifier, type Id } from '../identifier.utils.js';
import { uint8ToBuf } from '../number.utils.js';
+/** Offset mutations always request quorum acknowledgement. */
+const ACK_QUORUM = 1;
+
/**
* Consumer kind options for offset operations.
*/
@@ -145,11 +148,32 @@
partitionId: number | null,
offset: bigint
) => {
- const b = Buffer.allocUnsafe(8);
+ const b = Buffer.allocUnsafe(9);
b.writeBigUInt64LE(offset, 0);
+ b.writeUInt8(ACK_QUORUM, 8);
return Buffer.concat([
serializeGetOffset(streamId, topicId, consumer, partitionId),
b
]);
}
+
+/**
+ * Serializes parameters for delete offset operation.
+ *
+ * @param streamId - Stream identifier (ID or name)
+ * @param topicId - Topic identifier (ID or name)
+ * @param consumer - Consumer identifier (single or group)
+ * @param partitionId - Partition ID. VSR requires an explicit partition.
+ * @returns Buffer containing serialized delete offset request
+ */
+export const serializeDeleteOffset = (
+ streamId: Id,
+ topicId: Id,
+ consumer: Consumer,
+ partitionId: number | null
+) =>
+ Buffer.concat([
+ serializeGetOffset(streamId, topicId, consumer, partitionId),
+ uint8ToBuf(ACK_QUORUM)
+ ]);
diff --git a/foreign/node/src/wire/vsr/header.test.ts b/foreign/node/src/wire/vsr/header.test.ts
index a8095c7..639e2a3 100644
--- a/foreign/node/src/wire/vsr/header.test.ts
+++ b/foreign/node/src/wire/vsr/header.test.ts
@@ -18,7 +18,7 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
- Command2,
+ Command,
encodeRequestHeader,
HEADER_SIZE,
REQUEST_OFFSET
@@ -38,7 +38,7 @@
assert.equal(header.length, HEADER_SIZE);
assert.equal(header.readUInt32LE(REQUEST_OFFSET.size), HEADER_SIZE + 3);
- assert.equal(header.readUInt8(REQUEST_OFFSET.command), Command2.Request);
+ assert.equal(header.readUInt8(REQUEST_OFFSET.command), Command.Request);
assert.equal(
header.readBigUInt64LE(REQUEST_OFFSET.client),
0x99AABBCCDDEEFF00n
@@ -70,7 +70,7 @@
});
const expected = Buffer.alloc(HEADER_SIZE);
expected.writeUInt32LE(HEADER_SIZE, REQUEST_OFFSET.size);
- expected.writeUInt8(Command2.Request, REQUEST_OFFSET.command);
+ expected.writeUInt8(Command.Request, REQUEST_OFFSET.command);
expected.writeBigUInt64LE(1n, REQUEST_OFFSET.client);
expected.writeUInt8(1, REQUEST_OFFSET.operation);
assert.deepEqual(header, expected);
diff --git a/foreign/node/src/wire/vsr/header.ts b/foreign/node/src/wire/vsr/header.ts
index 9c51217..4b00e60 100644
--- a/foreign/node/src/wire/vsr/header.ts
+++ b/foreign/node/src/wire/vsr/header.ts
@@ -64,8 +64,8 @@
reason: 255
} as const;
-/** `Command2` frame discriminants a client encounters. */
-export const Command2 = {
+/** `Command` frame discriminants a client encounters. */
+export const Command = {
Request: 5,
Reply: 8,
Eviction: 13
@@ -120,7 +120,7 @@
export const encodeRequestHeader = (fields: RequestHeaderFields): Buffer => {
const header = Buffer.alloc(HEADER_SIZE);
header.writeUInt32LE(fields.size, REQUEST_OFFSET.size);
- header.writeUInt8(Command2.Request, REQUEST_OFFSET.command);
+ header.writeUInt8(Command.Request, REQUEST_OFFSET.command);
// u128 client id: two little-endian u64 halves, low half first.
header.writeBigUInt64LE(fields.client & U64_MASK, REQUEST_OFFSET.client);
header.writeBigUInt64LE(fields.client >> 64n, REQUEST_OFFSET.client + 8);
diff --git a/foreign/node/src/wire/vsr/operation.test.ts b/foreign/node/src/wire/vsr/operation.test.ts
index 2602373..daac920 100644
--- a/foreign/node/src/wire/vsr/operation.test.ts
+++ b/foreign/node/src/wire/vsr/operation.test.ts
@@ -53,9 +53,7 @@
[COMMAND_CODE.LeaveGroup, Operation.LeaveConsumerGroup],
[COMMAND_CODE.SendMessages, Operation.SendMessages],
[COMMAND_CODE.StoreOffset, Operation.StoreConsumerOffset],
- [COMMAND_CODE.DeleteConsumerOffset, Operation.DeleteConsumerOffset],
- [COMMAND_CODE.StoreOffset2, Operation.StoreConsumerOffset2],
- [COMMAND_CODE.DeleteConsumerOffset2, Operation.DeleteConsumerOffset2]
+ [COMMAND_CODE.DeleteConsumerOffset, Operation.DeleteConsumerOffset]
]);
describe('VSR operation classification', () => {
@@ -92,16 +90,14 @@
assert.equal(isPartition(Operation.SendMessages), true);
assert.equal(isPartition(159), false);
assert.equal(isResultFramed(Operation.StoreConsumerOffset), true);
- assert.equal(isResultFramed(Operation.StoreConsumerOffset2), true);
assert.equal(isResultFramed(Operation.DeleteConsumerOffset), true);
- assert.equal(isResultFramed(Operation.DeleteConsumerOffset2), true);
assert.equal(isResultFramed(Operation.SendMessages), false);
});
it('recognizes only declared operation discriminants', () => {
assert.equal(isKnownOperation(Operation.Register), true);
assert.equal(isKnownOperation(Operation.SendMessages), true);
- for (const undeclared of [69, 127, 150, 159, 163, 166, 255])
+ for (const undeclared of [69, 127, 150, 159, 163, 164, 165, 166, 255])
assert.equal(isKnownOperation(undeclared), false);
});
});
diff --git a/foreign/node/src/wire/vsr/operation.ts b/foreign/node/src/wire/vsr/operation.ts
index 4765e53..6c7520d 100644
--- a/foreign/node/src/wire/vsr/operation.ts
+++ b/foreign/node/src/wire/vsr/operation.ts
@@ -59,9 +59,7 @@
LeaveConsumerGroup: 149,
SendMessages: 160,
StoreConsumerOffset: 161,
- DeleteConsumerOffset: 162,
- StoreConsumerOffset2: 164,
- DeleteConsumerOffset2: 165
+ DeleteConsumerOffset: 162
} as const;
const INTERNAL_START = 64;
@@ -84,8 +82,6 @@
[COMMAND_CODE.SendMessages, Operation.SendMessages],
[COMMAND_CODE.StoreOffset, Operation.StoreConsumerOffset],
[COMMAND_CODE.DeleteConsumerOffset, Operation.DeleteConsumerOffset],
- [COMMAND_CODE.StoreOffset2, Operation.StoreConsumerOffset2],
- [COMMAND_CODE.DeleteConsumerOffset2, Operation.DeleteConsumerOffset2],
[COMMAND_CODE.CreateStream, Operation.CreateStream],
[COMMAND_CODE.DeleteStream, Operation.DeleteStream],
[COMMAND_CODE.UpdateStream, Operation.UpdateStream],
@@ -139,9 +135,7 @@
export const isResultFramed = (operation: number): boolean =>
isMetadata(operation) ||
operation === Operation.StoreConsumerOffset ||
- operation === Operation.StoreConsumerOffset2 ||
- operation === Operation.DeleteConsumerOffset ||
- operation === Operation.DeleteConsumerOffset2;
+ operation === Operation.DeleteConsumerOffset;
/**
* Picks the header operation for a command code. Unknown extension codes are
diff --git a/foreign/node/src/wire/vsr/reply.test.ts b/foreign/node/src/wire/vsr/reply.test.ts
index ea4a1a6..33c8099 100644
--- a/foreign/node/src/wire/vsr/reply.test.ts
+++ b/foreign/node/src/wire/vsr/reply.test.ts
@@ -20,7 +20,7 @@
import { COMMAND_CODE } from '../command.code.js';
import { ResponseError } from '../error.utils.js';
import {
- Command2,
+ Command,
EVICTION_OFFSET,
EvictionReason,
HEADER_SIZE,
@@ -40,7 +40,7 @@
): Buffer => {
const frame = Buffer.alloc(HEADER_SIZE + body.length);
frame.writeUInt32LE(frame.length, REPLY_OFFSET.size);
- frame.writeUInt8(Command2.Reply, REPLY_OFFSET.command);
+ frame.writeUInt8(Command.Reply, REPLY_OFFSET.command);
frame.writeUInt8(operation, REPLY_OFFSET.operation);
frame.writeUInt32LE(status, REPLY_OFFSET.status);
body.copy(frame, HEADER_SIZE);
@@ -50,7 +50,7 @@
const eviction = (reason: number): Buffer => {
const frame = Buffer.alloc(HEADER_SIZE);
frame.writeUInt32LE(HEADER_SIZE, REPLY_OFFSET.size);
- frame.writeUInt8(Command2.Eviction, REPLY_OFFSET.command);
+ frame.writeUInt8(Command.Eviction, REPLY_OFFSET.command);
frame.writeUInt8(reason, EVICTION_OFFSET.reason);
return frame;
};
@@ -64,7 +64,7 @@
assert.throws(() => decodeResponse(undersized), ResponseError);
const unsupported = reply(Operation.NonReplicated);
- unsupported.writeUInt8(Command2.Request, REPLY_OFFSET.command);
+ unsupported.writeUInt8(Command.Request, REPLY_OFFSET.command);
assert.throws(() => decodeResponse(unsupported), ResponseError);
});
diff --git a/foreign/node/src/wire/vsr/reply.ts b/foreign/node/src/wire/vsr/reply.ts
index 9309f9a..b0871ea 100644
--- a/foreign/node/src/wire/vsr/reply.ts
+++ b/foreign/node/src/wire/vsr/reply.ts
@@ -26,7 +26,7 @@
import { ResponseError, responseError } from '../error.utils.js';
import {
- Command2, EvictionReason, HEADER_SIZE,
+ Command, EvictionReason, HEADER_SIZE,
peekCommand, readEviction, readReplyOperation, readSize, readStatus
} from './header.js';
import { Operation, isKnownOperation, isResultFramed } from './operation.js';
@@ -65,9 +65,9 @@
throw responseError(commandCode, EMPTY_RESPONSE);
switch (peekCommand(frame)) {
- case Command2.Eviction:
+ case Command.Eviction:
throw evictionError(frame);
- case Command2.Reply:
+ case Command.Reply:
break;
default:
throw responseError(commandCode, INVALID_COMMAND);
diff --git a/foreign/node/src/wire/vsr/vsr.test.ts b/foreign/node/src/wire/vsr/vsr.test.ts
index 9149938..61f8564 100644
--- a/foreign/node/src/wire/vsr/vsr.test.ts
+++ b/foreign/node/src/wire/vsr/vsr.test.ts
@@ -94,7 +94,7 @@
serializeSendMessages(
1,
2,
- [],
+ [{ payload: 'x' }],
Partitioning.PartitionId(3)
)
);
diff --git a/foreign/python/Cargo.toml b/foreign/python/Cargo.toml
index cb6b328..9446a1c 100644
--- a/foreign/python/Cargo.toml
+++ b/foreign/python/Cargo.toml
@@ -17,7 +17,7 @@
[package]
name = "apache-iggy"
-version = "0.9.0-dev3"
+version = "0.9.0-dev4"
edition = "2024"
authors = ["Iggy Committers <dev@iggy.apache.org>"]
license = "Apache-2.0"
diff --git a/foreign/python/pyproject.toml b/foreign/python/pyproject.toml
index 7f29630..00903f9 100644
--- a/foreign/python/pyproject.toml
+++ b/foreign/python/pyproject.toml
@@ -22,7 +22,7 @@
[project]
name = "apache-iggy"
requires-python = ">=3.10"
-version = "0.9.0.dev3"
+version = "0.9.0.dev4"
description = "Apache Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
readme = "README.md"
license = { file = "LICENSE" }
diff --git a/foreign/python/uv.lock b/foreign/python/uv.lock
index d263c59..739fc1c 100644
--- a/foreign/python/uv.lock
+++ b/foreign/python/uv.lock
@@ -12,7 +12,7 @@
[[package]]
name = "apache-iggy"
-version = "0.9.0.dev3"
+version = "0.9.0.dev4"
source = { editable = "." }
[package.optional-dependencies]
diff --git a/web/package.json b/web/package.json
index eb2e63f..7dd852f 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,6 +1,6 @@
{
"name": "iggy-web-ui",
- "version": "0.4.0-edge.2",
+ "version": "0.4.0-edge.3",
"private": true,
"scripts": {
"dev": "vite dev --port 3050",