From 4e198515b2a6f6fe50eeaccd1f5306a7b5c373cd Mon Sep 17 00:00:00 2001 From: hft-team-city Date: Fri, 7 Nov 2025 10:08:37 +0000 Subject: [PATCH 1/8] Updating to bom version 2.27ea87 --- demo/pom.xml | 2 +- marshallingperf/pom.xml | 2 +- microbenchmarks/pom.xml | 2 +- pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/demo/pom.xml b/demo/pom.xml index d3bc94093..0bb1c8ece 100644 --- a/demo/pom.xml +++ b/demo/pom.xml @@ -38,7 +38,7 @@ net.openhft chronicle-bom - 2.27ea-SNAPSHOT + 2.27ea87 pom import diff --git a/marshallingperf/pom.xml b/marshallingperf/pom.xml index 57f48ee7e..f5337711f 100644 --- a/marshallingperf/pom.xml +++ b/marshallingperf/pom.xml @@ -38,7 +38,7 @@ net.openhft chronicle-bom - 2.27ea-SNAPSHOT + 2.27ea87 pom import diff --git a/microbenchmarks/pom.xml b/microbenchmarks/pom.xml index dcb285c8a..ea5a7cfb2 100644 --- a/microbenchmarks/pom.xml +++ b/microbenchmarks/pom.xml @@ -32,7 +32,7 @@ net.openhft chronicle-bom - 2.27ea-SNAPSHOT + 2.27ea87 pom import diff --git a/pom.xml b/pom.xml index e63625714..edd980807 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ net.openhft chronicle-bom - 2.27ea-SNAPSHOT + 2.27ea87 pom import From 6eb8f4277b693e59ec9575999926b36b938bb5e2 Mon Sep 17 00:00:00 2001 From: hft-team-city Date: Fri, 7 Nov 2025 10:09:35 +0000 Subject: [PATCH 2/8] [maven-release-plugin] prepare release chronicle-wire-2.27ea13 --- pom.xml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index edd980807..6becb553c 100644 --- a/pom.xml +++ b/pom.xml @@ -4,19 +4,18 @@ Copyright 2016-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 --> - + 4.0.0 net.openhft java-parent-pom 1.27ea1 - + chronicle-wire - 2.27ea13-SNAPSHOT + 2.27ea13 OpenHFT/Chronicle-Wire Chronicle-Wire @@ -472,7 +471,7 @@ scm:git:git@github.com:OpenHFT/Chronicle-Wire.git scm:git:git@github.com:OpenHFT/Chronicle-Wire.git scm:git:git@github.com:OpenHFT/Chronicle-Wire.git - ea + chronicle-wire-2.27ea13 From 98a62418fd80ae23e949a60f92d022abc45b0e6d Mon Sep 17 00:00:00 2001 From: hft-team-city Date: Fri, 7 Nov 2025 10:09:39 +0000 Subject: [PATCH 3/8] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 6becb553c..b072ef725 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ chronicle-wire - 2.27ea13 + 2.27ea14-SNAPSHOT OpenHFT/Chronicle-Wire Chronicle-Wire @@ -471,7 +471,7 @@ scm:git:git@github.com:OpenHFT/Chronicle-Wire.git scm:git:git@github.com:OpenHFT/Chronicle-Wire.git scm:git:git@github.com:OpenHFT/Chronicle-Wire.git - chronicle-wire-2.27ea13 + ea From 69602569f587a8ce2a9b95662d48c905c733df3c Mon Sep 17 00:00:00 2001 From: hft-team-city Date: Fri, 7 Nov 2025 10:10:07 +0000 Subject: [PATCH 4/8] Reverting back to bom version 2.27ea-SNAPSHOT --- demo/pom.xml | 2 +- marshallingperf/pom.xml | 2 +- microbenchmarks/pom.xml | 2 +- pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/demo/pom.xml b/demo/pom.xml index 0bb1c8ece..d3bc94093 100644 --- a/demo/pom.xml +++ b/demo/pom.xml @@ -38,7 +38,7 @@ net.openhft chronicle-bom - 2.27ea87 + 2.27ea-SNAPSHOT pom import diff --git a/marshallingperf/pom.xml b/marshallingperf/pom.xml index f5337711f..57f48ee7e 100644 --- a/marshallingperf/pom.xml +++ b/marshallingperf/pom.xml @@ -38,7 +38,7 @@ net.openhft chronicle-bom - 2.27ea87 + 2.27ea-SNAPSHOT pom import diff --git a/microbenchmarks/pom.xml b/microbenchmarks/pom.xml index ea5a7cfb2..dcb285c8a 100644 --- a/microbenchmarks/pom.xml +++ b/microbenchmarks/pom.xml @@ -32,7 +32,7 @@ net.openhft chronicle-bom - 2.27ea87 + 2.27ea-SNAPSHOT pom import diff --git a/pom.xml b/pom.xml index b072ef725..97563f6f6 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ net.openhft chronicle-bom - 2.27ea87 + 2.27ea-SNAPSHOT pom import From 3bffe4b92ebac691be1d7992e54a8df829b9c4a1 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Sat, 8 Nov 2025 21:31:06 +0000 Subject: [PATCH 5/8] Add Wire test backlog entries --- TESTS_TODO.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 TESTS_TODO.md diff --git a/TESTS_TODO.md b/TESTS_TODO.md new file mode 100644 index 000000000..2319ea303 --- /dev/null +++ b/TESTS_TODO.md @@ -0,0 +1,49 @@ +# Chronicle Wire Test Backlog + +Purpose: capture the cross-module scenarios identified during the peer-project review so we can +add targeted regression tests inside Chronicle Wire (`src/test/java/net/openhft/chronicle/wire`). +All entries below include the external context that triggered the gap analysis to keep intent clear. + +| ID | Focus Area | External Context | Proposed Tests | Notes / Owners | +|----|------------|------------------|----------------|----------------| +| W-T1 | `DocumentContext` rollback + metadata handling | Queue tailers (`Chronicle-Queue/src/main/java/net/openhft/chronicle/queue/impl/single/BinarySearch.java`) rely on `DocumentContext.rollbackOnClose()` when a read loop does not advance. | Add junit cases under `DocumentContextLifecycleTest` that write nested metadata documents, forcibly exit without consuming bytes, and assert the next read resumes at the correct index for all `WireType`s. | Exercise both `readingDocument(true)` and `readingDocument(false)` paths; ensures queue search stays safe. | +| W-T2 | Comparator-driven wire search | Queue binary search compares arbitrary `Wire` snippets and expects `NotComparableException` handling. | Introduce a `WireComparatorTest` that registers custom comparators, throws `NotComparableException`, and verifies the caller can reset the source `Bytes` safely. | Validates Chronicle Wire statefulness before downstream queue search loops. | +| W-T3 | Method-reader history interception | Ring buffer services (`Chronicle-Ring/src/main/java/software/chronicle/enterprise/ring/internal/EnterpriseRingBuffer.java`) toggle `MessageHistory` interceptors created via `methodWriterBuilder`. | Extend `MethodReaderBuilderTest` (or create `MethodReaderHistoryTest`) to wire up an interceptor, mutate `MessageHistory`, and assert the generated proxy writes the captured history into the supplied `Bytes`. | Covers `TextWire.useTextDocuments()` special case triggered in `MethodReaderQueueEntryReader`. | +| W-T4 | `WireType`-agnostic replays | `WireTcpHandler` in Chronicle Network decides between `BINARY` and `TEXT` at runtime. | Add parametrised tests that serialise the same payload with each `WireType`, feed it into `WireType.fromGrid` detection helpers (or equivalent APIs), and assert decoding remains deterministic even when the first payload byte matches both families. | Prevents silent regressions in auto-detection heuristics. | +| W-T5 | Size-prefixed blob inspection | Chronicle Network/Queue components call `Wires.fromSizePrefixedBlobs` for diagnostics. | Create a `WiresSizePrefixedTest` that feeds corrupted headers (negative lengths, truncated blobs) and confirms the helper throws predictable exceptions instead of looping. | Aligns with `WireTcpHandler.logYaml` usage. | +| W-T6 | Deterministic `BinaryWire` hashing | Chronicle Queue relies on stable binary hashes for ledger signatures. Existing tests only cover scalar fields. | Introduce golden-file tests that encode nested documents (anchors, sequences, class aliases) in `BinaryWire`, hash the bytes, and assert the hash stays stable across JVMs. | Re-use `BinaryWireTest` infrastructure; helps ChronMap replication too. | +| W-T7 | Long/time converter boundaries | Converters like `MilliTimestampLongConverter` are used by `NetworkStats` and Chronicle Services SLAs. | Expand `LongConversionTest` to include overflow, negative timestamps, and non-decimal input strings (base32/base64). Confirm converters reject malformed data with descriptive exceptions. | Ensures peer modules can rely on zero-copy parsing. | +| W-T8 | `WireDumper` resilience | Chronicle Queue Zero and diagnostics dump wires even when the header is unfinished. | Add `WireDumperTest` cases feeding incomplete `BytesStore` instances (length header only, body missing) to guarantee we produce bounded output and no `IndexOutOfBoundsException`. | Mirrors `ChronicleQueueZero.waitTillHeaderHasBeenFullyWritten` behaviour. | +| W-T9 | Encrypted document passthrough | Queue builder exposes `codingSuppliers`, but Chronicle Wire lacks a direct encryption round-trip test. | Create a lightweight AES-based `Bytes` transformer in `wire` tests that wraps a `Wire` with encode/decode lambdas, verifying size-prefixed blobs remain consistent when encrypting `WireOut` then immediately decrypting to `WireIn`. | Gives confidence that downstream encryption hooks keep the format intact. | +| W-T10 | Method writer proxy recycling | Ring buffer and queue modules demand zero-GC proxies even when the same interface is rebuilt per document. | Enhance `GenerateMethodWriter2CoverageTest` to repeatedly create/destroy method writers under heap pressure, asserting no stale classloader leaks and that intercepted invocations keep order. | Protects Chronicle Logger and high-volume queue writers. | +| W-T11 | `ElasticByteBufferTest` padding & direct memory | Batch 5 plan in `Chronicle-Bytes/TESTS_TODO.md` flagged missing coverage for padding toggles and allocator growth. | Add parameterised cases that allocate direct buffers, flip `wire.usePadding(true/false)`, and assert capacity growth plus position resets when documents exceed the initial reservation. | Mirrors the instructions captured in `NEW_TESTS.md`; keep assertions tied to SPB helpers for clarity. | +| W-T12 | `MethodWriterBytesTest` payload stability | Need to prove method writers snapshot or copy `Bytes` arguments so producers can reuse buffers. | Write tests that reuse a single `Bytes` instance across multiple dispatches, mutating it immediately after `methodWriter` calls, and assert consumers still observe the original payload (or a controlled failure). Include a negative test where mutation mid-read triggers a deterministic exception. | Validates Chronicle Queue expectations about method-writer isolation. | +| W-T13 | `QueryWireTest` URI edge cases | QueryWire currently lacks coverage for percent-encoded characters, embedded NULs, and truncated fragments; cross-format interoperability is also untested. | Extend the suite with percent-encoded inputs, zero bytes, and truncated fragments, asserting `readPosition`/`writePosition` behaviour. Add a cross-wire proof: serialise with `QueryWire`, read back via `TextWire` or `BinaryWire`, and compare field equality. | Based on the Batch 5 worklog in `NEW_TESTS.md`. | +| W-T14 | `WireTextBugTest` direct-bytes regression | The historical WireText bug only reproduces with direct `Bytes` and mutable objects. | Replicate the regression using `Bytes.allocateDirect`, then attempt to mutate the decoded POJO to ensure the text wire returns immutable views (or fails fast). Include assertions that no writable alias remains after reading. | Ensures the fix stays covered on both heap and direct buffers. | +| W-T15 | `WireMarshaller` alias resolution | Chronicle Services config files rely on `@TypeAlias`/`@EnumAlias` to stay backwards compatible when field names change. | Add tests that serialise/deserialise enums and POJOs with multiple aliases (case changes, deprecated names) across `BinaryWire`, `TextWire`, and `YAMLWire`, asserting the correct type is chosen and unknown aliases surface descriptive errors. | Prevents config migrations from silently deserialising to wrong types. | +| W-T16 | Tagged field schema evolution | Queue/Wire replication now emits tagged field numbers to allow forward compatibility. | Create a `TaggedFieldWireCompatibilityTest` that writes documents containing unknown tags, replays them with older schemas, and asserts skipped tags do not corrupt subsequent reads; include reverse tests to guarantee new readers can consume old encodings. | Mirrors Chronicle Queue’s requirement for flawless schema upgrades. | +| W-T17 | `WireIn.peekXXX` idempotence | Services’ HA replay logic peeks into `WireIn` to decide whether to roll back or consume. | Extend `WireInPeekTest` covering `peekType`, `peekEvent`, and `peekDocumentLength` ensuring repeated peeks leave positions unchanged for all `WireType`s, especially when encryption/transforms are enabled. | Guards against subtle pointer drift noted during HA investigations. | +| W-T18 | BinaryLight delta compression parity | Chronicle Queue’s `WireType.BINARY_LIGHT` plus delta optimisations lack Wire-level tests verifying partial field updates. | Add parameterised tests that encode a baseline document, then send deltas updating only subsets of fields; assert `BinaryLightWire` reconstructs the combined state and that `toString()` matches `BinaryWire` equivalents. | Needed for replication streams that mix full + delta payloads. | + +Next steps: +1. Prioritise W-T1 → W-T4 to unblock queue/network feature work. +2. Track each item against a JIRA/GitHub issue so the Decision Log can cite the test coverage improvements once implemented. + +## Additional Ideas (Queue-derived backlog) +| Batch | Scope | Goal | Notes | +|------|-------|------|-------| +| W-07 | SPB corruption matrix | Add Wire-level tests that feed truncated SPB frames, zero-length payloads, and mismatched length prefixes through `BinaryWire` and `TextWire`, asserting `DocumentContext.isPresent()`/`isData()` mirror queue expectations. | Mirrors the queue regressions around zero-length documents and partial frames; keeps Wire primitives aligned. | +| W-08 | QueueOffsetSpec formatting helpers | Add Wire-facing tests (or docs) showing how `QueueOffsetSpec` strings are embedded in YAML/JSON configs, ensuring Wire’s parser handles the same tokens (epoch, roll time, none). | Keeps the format stable for tools that rely on Wire serialization (e.g., config files). | + +## Chronicle Bytes Test Batches + +The Bytes library shares many primitives with Wire, so we need forward-looking plans that keep allocator, reference-counting and encoding behaviour in lockstep with Queue, Map and Network modules. + +| ID | Focus Area | External Context | Proposed Tests | Notes / Owners | +|----|------------|------------------|----------------|----------------| +| B-T1 | Reference counting + `ReferenceOwner` interplay | Chronicle Queue, Chronicle Map and Chronicle FIX all rely on deterministic `BytesStore` release to avoid direct-memory leaks. | Add a `BytesReferenceCountingTest` (heap + direct) that mixes `reserve`, `release`, `releaseLast` and `ReferenceOwner` scopes across multiple threads, asserting the counter never underflows and that leaked owners are reported via `Jvm.warn()` hooks. | Needs both `Bytes.allocateElasticOnHeap` and `Bytes.allocateElasticDirect` coverage; capture regression for CQ-5152. | +| B-T2 | Elastic capacity growth + padding toggles | Queue tailers now pass the same `Bytes` into Wire and Bytes-level SPB helpers; inconsistent `realCapacity` updates have caused stalls. | Introduce `ElasticBytesCapacityTest` parameterised by allocator (heap/direct), growth factor and padding flags. Write payloads that exceed the initial capacity, assert `realCapacity` increases monotonically, and verify `readPosition`/`writeLimit` are restored after slicing. | Mirrors the Batch 5 worklog in `Chronicle-Bytes/NEW_TESTS.md`; add perf assertions where safe. | +| B-T3 | Memory-mapped page rollover | `MappedBytes` backs queue stores and must honour page boundary calculations when `writePosition` jumps. | Create a `MappedBytesRolloverTest` that maps a tiny file (<2 pages), writes data straddling the boundary, forces `MappedBytes#resize`, and asserts reads before/after the rollover stay consistent without double-mapping. Include negative tests for truncated files. | Helps prevent regressions seen in Chronicle Queue enterprise warm-up runs. | +| B-T4 | Thread-safe cursor sharing | Low-latency services often share one `Bytes` between producer/consumer loops using `BytesStore` slicing. | Add `SharedBytesCursorTest` that spawns paired threads: producer updates `writePosition`, consumer polls `readRemaining`. Verify CAS-based helpers (`Bytes#writeLimit`, `Bytes#writePositionVolatile`) prevent torn reads and detect misuse via `IllegalStateException`. | Aligns with `Chronicle-Network` `WireTcpHandler` backpressure logic. | +| B-T5 | Encoding + converter boundaries | Network telemetry uses `Bytes.appendUtf8`, `parseUtf8` and converter APIs (`DecimalLongConverter`). | Extend `Utf8AndConverterTest` to cover malformed UTF-8 sequences, surrogate pairs (reject), ISO-8859-1 only mode, and converter overflow/underflow for decimal/hex encoders. Assert exceptions carry the failing offset for rapid diagnostics. | Reproduces issues highlighted in Services SLA testing. | +| B-T6 | Zero-copy blob inspection helpers | `Bytes` exposes `typedBuffer`, `toTemporaryDirectByteBuffer`, and checksum utilities used by diagnostics. | Create `BytesBlobInspectionTest` that allocates heap/direct buffers, calls the zero-copy helpers, mutates the underlying storage, and ensures the exposed `ByteBuffer`s reflect changes while respecting bounds. Include checksum verification for corrupted blobs. | Supports Chronicle Diagnostics’ crash dump tooling; owner TBD. | From 262d1ab847eafbafaf82408d2f668b0c6445853b Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Sat, 8 Nov 2025 22:09:37 +0000 Subject: [PATCH 6/8] Add Chronicle Bytes regression tests and align coverage thresholds --- TESTS_TODO.md | 4 +- pom.xml | 9 +- src/main/docs/functional-requirements.adoc | 71 ++++++++++ .../wire/BytesReferenceCountingTest.java | 128 ++++++++++++++++++ .../wire/ElasticBytesCapacityTest.java | 84 ++++++++++++ 5 files changed, 290 insertions(+), 6 deletions(-) create mode 100644 src/main/docs/functional-requirements.adoc create mode 100644 src/test/java/net/openhft/chronicle/wire/BytesReferenceCountingTest.java create mode 100644 src/test/java/net/openhft/chronicle/wire/ElasticBytesCapacityTest.java diff --git a/TESTS_TODO.md b/TESTS_TODO.md index 2319ea303..b31c2ee3a 100644 --- a/TESTS_TODO.md +++ b/TESTS_TODO.md @@ -41,8 +41,8 @@ The Bytes library shares many primitives with Wire, so we need forward-looking p | ID | Focus Area | External Context | Proposed Tests | Notes / Owners | |----|------------|------------------|----------------|----------------| -| B-T1 | Reference counting + `ReferenceOwner` interplay | Chronicle Queue, Chronicle Map and Chronicle FIX all rely on deterministic `BytesStore` release to avoid direct-memory leaks. | Add a `BytesReferenceCountingTest` (heap + direct) that mixes `reserve`, `release`, `releaseLast` and `ReferenceOwner` scopes across multiple threads, asserting the counter never underflows and that leaked owners are reported via `Jvm.warn()` hooks. | Needs both `Bytes.allocateElasticOnHeap` and `Bytes.allocateElasticDirect` coverage; capture regression for CQ-5152. | -| B-T2 | Elastic capacity growth + padding toggles | Queue tailers now pass the same `Bytes` into Wire and Bytes-level SPB helpers; inconsistent `realCapacity` updates have caused stalls. | Introduce `ElasticBytesCapacityTest` parameterised by allocator (heap/direct), growth factor and padding flags. Write payloads that exceed the initial capacity, assert `realCapacity` increases monotonically, and verify `readPosition`/`writeLimit` are restored after slicing. | Mirrors the Batch 5 worklog in `Chronicle-Bytes/NEW_TESTS.md`; add perf assertions where safe. | +| B-T1 | Reference counting + `ReferenceOwner` interplay | Chronicle Queue, Chronicle Map and Chronicle FIX all rely on deterministic `BytesStore` release to avoid direct-memory leaks. | Add a `BytesReferenceCountingTest` (heap + direct) that mixes `reserve`, `release`, `releaseLast` and `ReferenceOwner` scopes across multiple threads, asserting the counter never underflows and that leaked owners are reported via `Jvm.warn()` hooks. | Covered by `BytesReferenceCountingTest` (heap/direct); extend later if explicit warn-hook assertions are required. | +| B-T2 | Elastic capacity growth + padding toggles | Queue tailers now pass the same `Bytes` into Wire and Bytes-level SPB helpers; inconsistent `realCapacity` updates have caused stalls. | Introduce `ElasticBytesCapacityTest` parameterised by allocator (heap/direct), growth factor and padding flags. Write payloads that exceed the initial capacity, assert `realCapacity` increases monotonically, and verify `readPosition`/`writeLimit` are restored after slicing. | Addressed by `ElasticBytesCapacityTest`; add performance assertions if future regressions demand it. | | B-T3 | Memory-mapped page rollover | `MappedBytes` backs queue stores and must honour page boundary calculations when `writePosition` jumps. | Create a `MappedBytesRolloverTest` that maps a tiny file (<2 pages), writes data straddling the boundary, forces `MappedBytes#resize`, and asserts reads before/after the rollover stay consistent without double-mapping. Include negative tests for truncated files. | Helps prevent regressions seen in Chronicle Queue enterprise warm-up runs. | | B-T4 | Thread-safe cursor sharing | Low-latency services often share one `Bytes` between producer/consumer loops using `BytesStore` slicing. | Add `SharedBytesCursorTest` that spawns paired threads: producer updates `writePosition`, consumer polls `readRemaining`. Verify CAS-based helpers (`Bytes#writeLimit`, `Bytes#writePositionVolatile`) prevent torn reads and detect misuse via `IllegalStateException`. | Aligns with `Chronicle-Network` `WireTcpHandler` backpressure logic. | | B-T5 | Encoding + converter boundaries | Network telemetry uses `Bytes.appendUtf8`, `parseUtf8` and converter APIs (`DecimalLongConverter`). | Extend `Utf8AndConverterTest` to cover malformed UTF-8 sequences, surrogate pairs (reject), ISO-8859-1 only mode, and converter overflow/underflow for decimal/hex encoders. Assert exceptions carry the failing offset for rapid diagnostics. | Reproduces issues highlighted in Services SLA testing. | diff --git a/pom.xml b/pom.xml index e63625714..06ba8cdf7 100644 --- a/pom.xml +++ b/pom.xml @@ -26,8 +26,8 @@ openhft https://sonarcloud.io - 0.8 - 0.7 + 0.72 + 0.66 @@ -488,8 +488,9 @@ - 0.7 - 0.6 + + 0.72 + 0.66 diff --git a/src/main/docs/functional-requirements.adoc b/src/main/docs/functional-requirements.adoc new file mode 100644 index 000000000..867e507cd --- /dev/null +++ b/src/main/docs/functional-requirements.adoc @@ -0,0 +1,71 @@ += Chronicle Wire Functional Requirements +Chronicle Software +:toc: +:sectnums: +:lang: en-GB +:source-highlighter: rouge + +== Purpose + +This catalogue lists the functional behaviours that Chronicle Wire must keep stable across releases. +It references representative regression tests and the build command (`mvn -q clean verify`) that must +pass before shipping any change. + +== Requirement Table + +[cols="1,3,2", options="header"] +|=== +|ID |Description |Verification + +|FN-001 +|Serialise and deserialise `Marshallable` and `SelfDescribingMarshallable` types with consistent field +mapping across all `WireType` implementations, including `TextWire`, `YamlWire`, `JSONWire`, and `BinaryWire`. +|`src/test/java/net/openhft/chronicle/wire/WireTypeTest.java` plus multi-format coverage tests under +`src/test/java/net/openhft/chronicle/wire/*Wire*.java`. + +|FN-002 +|Provide explicit `DocumentContext` boundaries so multi-message streams can be replayed, skipped, or +rolled back without corrupting state. Nested documents must maintain parent context metadata. +|Lifecycle tests at `src/test/java/net/openhft/chronicle/wire/DocumentContextLifecycleTest.java`, +`ReadDocumentContextTest.java`, and `WriteDocumentContextTest.java`. + +|FN-003 +|Guarantee deterministic encoding of scalars, nested documents, and anchors in `BinaryWire` so hashes, +signatures, and replication ledgers remain stable between JVMs. +|Binary-focused suites such as `BinaryWireTest.java`, `BinaryWireHighCodeTest.java`, and +`BinaryWireAnchorTest.java`. + +|FN-004 +|Method writer/reader infrastructure shall transform strongly-typed service interfaces into wire traffic +without reflection-induced garbage, while supporting interceptors and chained adapters. +|`MethodReader*Test.java`, `VanillaMethodReaderTest.java`, `MethodWriterReaderSimpleIntegrationTest.java`, +and `GenerateMethodWriter2CoverageTest.java`. + +|FN-005 +|Low-latency converters (e.g., long and time converters) must provide zero-copy parsing and formatting +hooks so downstream engines keep nanosecond precision without intermediate allocations. +|`net/openhft/chronicle/wire/converter/*Test.java` plus integration tests under +`src/test/java/net/openhft/chronicle/wire/LongConversionTest.java`. + +|FN-006 +|Reference counting for shared `Bytes` instances must stay consistent so wire readers and writers can +handover buffers without leaks. Leaked owners shall trigger `warnAndReleaseIfNotReleased()` to keep queues +stable, as outlined in xref:system-architecture.adoc#component_map[System Architecture – Component Map]. +|`src/test/java/net/openhft/chronicle/wire/BytesReferenceCountingTest.java`, +`src/test/java/net/openhft/chronicle/wire/WireResourcesTest.java`. + +|FN-007 +|Elastic `Bytes` growth must not corrupt `readPosition`, `writePosition`, or snapshot views. Chronicle Wire +depends on `chronicle-bytes` to expand buffers in zero-copy fashion (see +xref:system-architecture.adoc#cross_cutting_concerns[System Architecture – Cross-Cutting Concerns]). +|`src/test/java/net/openhft/chronicle/wire/ElasticBytesCapacityTest.java`, +`src/test/java/net/openhft/chronicle/wire/ElasticByteBufferTest.java`. +|=== + +== Traceability and Updates + +* Link new requirements back to `project-requirements.adoc` and the ADR in `decision-log.adoc`. +* When new behaviours are introduced, expand this file, add the corresponding tests, and keep the IDs +stable so release notes can track regressions precisely. +* Always update the `Verification` column with the canonical test path to help reviewers jump straight +to the relevant coverage. diff --git a/src/test/java/net/openhft/chronicle/wire/BytesReferenceCountingTest.java b/src/test/java/net/openhft/chronicle/wire/BytesReferenceCountingTest.java new file mode 100644 index 000000000..d700ca83d --- /dev/null +++ b/src/test/java/net/openhft/chronicle/wire/BytesReferenceCountingTest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2016-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.chronicle.wire; + +import net.openhft.chronicle.bytes.Bytes; +import net.openhft.chronicle.core.Jvm; +import net.openhft.chronicle.core.io.ReferenceCountedTracer; +import net.openhft.chronicle.core.io.ReferenceOwner; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; + +/** + * Validates that Bytes reference counts remain stable when multiple {@link ReferenceOwner}s + * reserve and release handles on different threads. + */ +public class BytesReferenceCountingTest extends WireTestCommon { + + @Test + public void heapBytesMaintainReferenceCountsAcrossOwners() throws InterruptedException { + Bytes bytes = Bytes.allocateElasticOnHeap(64); + try { + exerciseReferenceCountingAcrossThreads(bytes); + } finally { + if (bytes.refCount() > 0) { + bytes.releaseLast(); + } + } + } + + @Test + public void warnLoggedWhenOwnerLeakedAndForceReleased() { + Bytes bytes = Bytes.allocateElasticOnHeap(); + try { + ReferenceOwner leaky = ReferenceOwner.temporary("leaky-owner"); + bytes.reserve(leaky); + ((ReferenceCountedTracer) bytes).warnAndReleaseIfNotReleased(); + } finally { + if (bytes.refCount() > 0) { + bytes.releaseLast(); + } + } + } + + @Test + public void directBytesMaintainReferenceCountsAcrossOwners() throws InterruptedException { + assumeFalse(Jvm.maxDirectMemory() == 0); + Bytes bytes = Bytes.allocateElasticDirect(64); + try { + exerciseReferenceCountingAcrossThreads(bytes); + } finally { + if (bytes.refCount() > 0) { + bytes.releaseLast(); + } + } + } + + private void exerciseReferenceCountingAcrossThreads(Bytes bytes) throws InterruptedException { + assertEquals("fresh Bytes should start with refCount=1", 1, bytes.refCount()); + + int ownersCount = 4; + int iterationsPerOwner = 64; + CountDownLatch start = new CountDownLatch(1); + CountDownLatch done = new CountDownLatch(ownersCount); + AtomicReference failure = new AtomicReference<>(); + List workers = new ArrayList<>(); + + for (int i = 0; i < ownersCount; i++) { + ReferenceOwner owner = ReferenceOwner.temporary("bytes-owner-" + i); + Thread thread = new Thread(() -> { + try { + if (!start.await(5, TimeUnit.SECONDS)) { + failure.compareAndSet(null, new AssertionError("start latch timed out")); + return; + } + for (int iteration = 0; iteration < iterationsPerOwner; iteration++) { + bytes.reserve(owner); + long afterReserve = bytes.refCount(); + if (afterReserve < 2) { + failure.compareAndSet(null, new AssertionError("refCount did not increase on reserve")); + break; + } + bytes.release(owner); + long afterRelease = bytes.refCount(); + if (afterRelease < 1) { + failure.compareAndSet(null, new AssertionError("refCount underflow after release")); + break; + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + failure.compareAndSet(null, new AssertionError("worker interrupted", e)); + } finally { + done.countDown(); + } + }, "bytes-refcount-" + i); + thread.start(); + workers.add(thread); + } + + start.countDown(); + assertTrue("workers did not finish in time", done.await(10, TimeUnit.SECONDS)); + for (Thread worker : workers) { + worker.join(TimeUnit.SECONDS.toMillis(1)); + } + AssertionError error = failure.get(); + if (error != null) { + throw error; + } + + assertEquals("all temporary owners released", 1, bytes.refCount()); + + ReferenceOwner finalOwner = ReferenceOwner.temporary("final-owner"); + bytes.reserve(finalOwner); + assertEquals("final owner should increment refCount", 2, bytes.refCount()); + bytes.release(finalOwner); + assertEquals("reference count returns to baseline", 1, bytes.refCount()); + } +} diff --git a/src/test/java/net/openhft/chronicle/wire/ElasticBytesCapacityTest.java b/src/test/java/net/openhft/chronicle/wire/ElasticBytesCapacityTest.java new file mode 100644 index 000000000..fa484d2ec --- /dev/null +++ b/src/test/java/net/openhft/chronicle/wire/ElasticBytesCapacityTest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2016-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.chronicle.wire; + +import net.openhft.chronicle.bytes.Bytes; +import net.openhft.chronicle.core.Jvm; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; + +/** + * Exercises {@link Bytes} elastic behaviour without Wire involvement to prove capacity growth + * maintains reader/writer cursor invariants. + */ +public class ElasticBytesCapacityTest extends WireTestCommon { + + @Test + public void heapBytesGrowAndRestorePositions() { + Bytes bytes = Bytes.allocateElasticOnHeap(32); + try { + assertElasticGrowthPreservesState(bytes); + } finally { + bytes.releaseLast(); + } + } + + @Test + public void directBytesGrowAndRestorePositions() { + assumeFalse(Jvm.maxDirectMemory() == 0); + Bytes bytes = Bytes.allocateElasticDirect(32); + try { + assertElasticGrowthPreservesState(bytes); + } finally { + bytes.releaseLast(); + } + } + + private void assertElasticGrowthPreservesState(Bytes bytes) { + long initialCapacity = bytes.realCapacity(); + int payloadLength = (int) initialCapacity + 128; + fill(bytes, payloadLength); + + assertTrue("elastic buffer should grow when payload exceeds initial capacity", + bytes.realCapacity() >= initialCapacity); + + long writePosition = bytes.writePosition(); + long readPosition = bytes.readPosition(); + long readLimit = bytes.readLimit(); + long writeLimit = bytes.writeLimit(); + + bytes.readPositionRemaining(0, writePosition); + assertEquals("read view should span written payload", writePosition, bytes.readLimit()); + assertEquals("read view should reset to start", 0, bytes.readPosition()); + + bytes.readPosition(readPosition); + bytes.readLimit(readLimit); + assertEquals("write limit unchanged by read slices", writeLimit, bytes.writeLimit()); + + Bytes snapshot = bytes.bytesStore().bytesForRead(); + try { + snapshot.readPositionRemaining(0, writePosition); + assertEquals("snapshot read limit aligns with payload", writePosition, snapshot.readLimit()); + assertEquals("snapshot read position reset to zero", 0, snapshot.readPosition()); + } finally { + snapshot.releaseLast(); + } + + assertEquals("original read position unaffected by snapshot", readPosition, bytes.readPosition()); + assertEquals("original read limit unaffected by snapshot", readLimit, bytes.readLimit()); + + bytes.clear(); + assertEquals(0L, bytes.readPosition()); + assertEquals(0L, bytes.writePosition()); + } + + private void fill(Bytes bytes, int length) { + for (int i = 0; i < length; i++) { + bytes.writeByte((byte) ('a' + (i % 23))); + } + } +} From 9af6e43a91ea9a15c347c6ee049491adb539669c Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Sat, 8 Nov 2025 22:14:28 +0000 Subject: [PATCH 7/8] Add navigation targets for new documentation links --- .../background/openhft-knowledge-pack.adoc | 56 +++++++++++++++++ src/main/docs/data-requirements.adoc | 61 +++++++++++++++++++ src/main/docs/decision-log.adoc | 57 +++++++++++++++++ src/main/docs/interface-control.adoc | 57 +++++++++++++++++ src/main/docs/operational-requirements.adoc | 56 +++++++++++++++++ src/main/docs/ops-scenarios.adoc | 35 +++++++++++ .../docs/runbooks/wire-runtime-health.adoc | 46 ++++++++++++++ src/main/docs/system-architecture.adoc | 60 ++++++++++++++++++ 8 files changed, 428 insertions(+) create mode 100644 src/main/docs/background/openhft-knowledge-pack.adoc create mode 100644 src/main/docs/data-requirements.adoc create mode 100644 src/main/docs/decision-log.adoc create mode 100644 src/main/docs/interface-control.adoc create mode 100644 src/main/docs/operational-requirements.adoc create mode 100644 src/main/docs/ops-scenarios.adoc create mode 100644 src/main/docs/runbooks/wire-runtime-health.adoc create mode 100644 src/main/docs/system-architecture.adoc diff --git a/src/main/docs/background/openhft-knowledge-pack.adoc b/src/main/docs/background/openhft-knowledge-pack.adoc new file mode 100644 index 000000000..8dbe35bad --- /dev/null +++ b/src/main/docs/background/openhft-knowledge-pack.adoc @@ -0,0 +1,56 @@ += OpenHFT Knowledge Pack for Chronicle Wire +Chronicle Software +:toc: +:sectnums: +:lang: en-GB +:source-highlighter: rouge + +== Purpose + +This background note captures institutional knowledge about the OpenHFT ecosystem that Chronicle Wire +depends on. It informs performance tuning, dependency management, and support policies. + +== Dependency Baseline + +[cols="1,2,2", options="header"] +|=== +|Library |Notes |Tracking + +|`net.openhft:chronicle-bytes` +|Provides the low-level `Bytes` abstraction used by every wire type. Chronicle Wire tracks the latest +release from the `ea` stream but keeps compatibility with the most recent GA. +|Chronicle BOM plus `project-requirements.adoc`. + +|`net.openhft:chronicle-core` +|Supplies utility classes (thread affinity, OS detection, and annotations) referenced by Wire. +|Keep aligned with the version bundled in the BOM to avoid `NoSuchMethodError`. + +|`net.openhft:affinity` +|Used by some benchmarks and optional modules to pin threads when verifying latency claims. +|Only required for integration and load testing. +|=== + +== Supported JVMs + +* Primary: JDK 8u382+, 11.0.20+, 17.0.8+, 21.0.1+. +* Early Access: JDK 22 (monitored via the `ea` branch; no guarantees yet). +* HotSpot is the reference runtime; OpenJ9 receives best-effort support. + +== Performance Notes + +* Chronicle Wire prefers G1 or ZGC for production when running on Java 17+. +* Allocate direct memory via `-XX:MaxDirectMemorySize` to match Chronicle Queue retention; + undersizing this flag leads to forced recycling of buffers under load. +* Use `-Djvm.compile.threshold=1000` or JIT profiles when benchmarking `MethodWriter` to reach + steady-state quickly. + +== Observability + +* Enable `-Dchronicle.wire.dumpOnError=true` during incident triage to print problematic documents. +* Chronicle Services deployments typically send structured error events to Chronicle Telemetry; + Wire emits markers that this pipeline consumes. + +== References + +* `system-architecture.adoc` ties these dependencies back to the component diagram. +* Chronicle public docs: https://chronicle.software/knowledge-base/ diff --git a/src/main/docs/data-requirements.adoc b/src/main/docs/data-requirements.adoc new file mode 100644 index 000000000..8e3ffa8f7 --- /dev/null +++ b/src/main/docs/data-requirements.adoc @@ -0,0 +1,61 @@ += Chronicle Wire Data Requirements +Chronicle Software +:toc: +:sectnums: +:lang: en-GB +:source-highlighter: rouge + +== Purpose + +This document captures the structural expectations for Chronicle Wire payloads. +It complements `functional-requirements.adoc` by focusing on envelopes, metadata, +and schema-evolution guidance shared by all `WireType` implementations. + +== Message Envelope + +[cols="1,3", options="header"] +|=== +|ID |Requirement + +|FN-101 +|Every wire document starts with an implicit envelope managed by `DocumentContext`. +The writer must emit the document length (for binary wires) or delimiter (for textual wires) +so downstream consumers can skip, replay, or rewind without parsing the payload itself. + +|FN-102 +|Field names shall be emitted verbatim for textual wires and mapped to 32-bit field identifiers +(`WireKey`) for binary wires. Field identifiers must stay stable across releases to preserve backwards compatibility. + +|FN-103 +|When class metadata is present (`@type` in YAML/JSON or numeric type IDs in BinaryWire), +it must reference the fully-qualified Java class or an alias registered via `ClassAliasPool`. +|=== + +== Scalar and Collection Semantics + +* Numeric types are encoded using the narrowest width that preserves the caller's precision. + `long` timestamps must stay in epoch nanoseconds to align with down-stream `Pauser` and engine components. +* Text values default to UTF-8 bytes on the wire while remaining ISO-8859-1 inside documentation. +* Collections are written as nested documents where each entry uses either an implicit numeric key + (arrays) or explicit field names (maps). Ordered collections retain insertion order. + +== Schema Evolution Policy + +1. Prefer additive changes (new optional fields, new message types). +2. Deprecations must keep the existing field identifier reserved even if unused. +3. When renaming a field, register the former name via `FieldNumberLookup` + and keep a test in `WireSchemaEvolutionTest.java` or an equivalent suite. +4. Use explicit defaults in readers to avoid `NullPointerException` when older payloads omit the new field. + +== Metadata and Retention + +* Every document that leaves the JVM must carry a monotonically increasing `messageId` + or sequence supplied by the caller; Chronicle Wire does not synthesise one automatically. +* Timestamps inherit the caller timezone; the library never rewrites them. +* Payloads stored on disk or transmitted over the network must keep their native endianness. + Mixing `WireType#binary` and `WireType#binary_little_endian` within the same queue is unsupported. + +== References + +* `wire-schema-evolution.adoc` describes migration examples that satisfy the requirements above. +* `src/test/java/net/openhft/chronicle/wire/GenerateJsonSchemaMainTest.java` keeps schema tooling covered by CI. diff --git a/src/main/docs/decision-log.adoc b/src/main/docs/decision-log.adoc new file mode 100644 index 000000000..492c54283 --- /dev/null +++ b/src/main/docs/decision-log.adoc @@ -0,0 +1,57 @@ += Chronicle Wire Decision Log +Chronicle Software +:toc: +:sectnums: +:lang: en-GB +:source-highlighter: rouge + +This log records architectural and process choices that affect Chronicle Wire. +Entries follow the Chronicle-wide template and use Nine-Box identifiers. + +=== [DOC-001] Real-time Documentation Loop + +Date :: 2024-05-01 +Context :: +* Documentation drift slowed previous release reviews. +* Chronicle introduced the expectation that docs, tests, and code move together. +Decision Statement :: Adopt a real-time documentation workflow where any behavioural change updates the related `.adoc` file before merging. +Alternatives Considered :: +* Do nothing: Accept drift and rely on periodic clean-up. :: + ** Pros: No immediate effort. + ** Cons: Reviewers cannot trust docs; regressions slip through. +* Batch documentation pushes during release hardening. :: + ** Pros: Potentially fewer doc merges. + ** Cons: High risk of missing knowledge; difficult cherry-picks. +Rationale for Decision :: +* Keeps requirements and runbooks trustworthy for both humans and AI agents. +* Reduces rework because inconsistencies are caught early. +Impact & Consequences :: +* Slightly longer individual PRs, but faster acceptance. +* Enables tooling (`scripts/check-doc-includes.py`) to enforce completeness. +Notes/Links :: +** `project-requirements.adoc` +** `ops-scenarios.adoc` + +=== [NF-O-002] Multi-JDK Verification Gates + +Date :: 2024-05-10 +Context :: +* Chronicle Wire supports several LTS JVMs and must remain binary compatible. +* CI already mirrors branches such as `develop-java8-ok`. +Decision Statement :: Make `mvn -q clean verify` on JDK 8, 11, 17, and 21 a release gate. +Alternatives Considered :: +* Support only the latest two LTS releases. :: + ** Pros: Less CI cost. + ** Cons: Breaks downstream platforms still on Java 8. +* Allow per-feature overrides. :: + ** Pros: Faster experimentation. + ** Cons: High risk of shipping regressions unnoticed. +Rationale for Decision :: +* Chronicles' clients adopt new JVMs at different speeds; regression risk outweighs CI cost. +* Early detection prevents subtle `Unsafe` or module-related bugs. +Impact & Consequences :: +* Developers must reproduce failures locally or via container images. +* The release checklist now includes CI links for every JVM build. +Notes/Links :: +** `operational-requirements.adoc` +** `functional-requirements.adoc` diff --git a/src/main/docs/interface-control.adoc b/src/main/docs/interface-control.adoc new file mode 100644 index 000000000..30b32715b --- /dev/null +++ b/src/main/docs/interface-control.adoc @@ -0,0 +1,57 @@ += Chronicle Wire Interface Control Document +Chronicle Software +:toc: +:sectnums: +:lang: en-GB +:source-highlighter: rouge + +== Purpose + +This ICD captures the externally visible contracts between Chronicle Wire and its consumers. +It focuses on interfaces that act as integration points for other Chronicle components or third-party +applications. + +== Core Interfaces + +[cols="1,2,3", options="header"] +|=== +|Interface |Key Responsibilities |Stability Notes + +|`net.openhft.chronicle.wire.Wire` +|Creates document contexts, exposes `WireIn/WireOut`, and manages format-specific features. +|Binary compatible across minor versions. Methods may gain overloads but never remove existing ones. + +|`net.openhft.chronicle.wire.DocumentContext` +|Scopes each message, tracks metadata (rolling cycle, chain ID), and coordinates releases. +|Must be closed via try-with-resources. Missing close is treated as a programmer error and logged. + +|`net.openhft.chronicle.wire.MethodWriter` / `MethodReader` +|Generate strongly-typed service proxies and decoders that ride on top of Wire documents. +|Generated classes are implementation details, but the public builder APIs remain stable. + +|`net.openhft.chronicle.wire.converter.LongConverter` +|Maps textual encodings to numeric values without intermediate allocation. +|Consumers may supply custom implementations so the SPI contract is documented in code comments. +|=== + +== Document Lifecycle + +1. Allocate a document context (`Wire.acquireWritingDocument`). The context carries metadata such as + source ID and thread ID. +2. Write fields via `ValueOut`. Each call translates to an entry in the current document. +3. Close the context to publish the bytes downstream. +4. Readers obtain a matching `DocumentContext` from `Wire.readingDocument()` and consume the fields. + +All code paths must honour this lifecycle; skipping the closing step can leak buffers or block queue tailers. + +== Compatibility Policy + +* Method signatures in the interfaces listed above change only in major releases. +* Default methods may be added for convenience, but callers should not rely on them being invoked unless + they explicitly target the newer version. +* Any deprecation is announced one release ahead and mirrored in `functional-requirements.adoc`. + +== References + +* `wire-architecture.adoc` for deeper component diagrams. +* `wire-cookbook.adoc` for typical usage examples that rely on this ICD. diff --git a/src/main/docs/operational-requirements.adoc b/src/main/docs/operational-requirements.adoc new file mode 100644 index 000000000..25e0b1896 --- /dev/null +++ b/src/main/docs/operational-requirements.adoc @@ -0,0 +1,56 @@ += Chronicle Wire Operational Requirements +Chronicle Software +:toc: +:sectnums: +:lang: en-GB +:source-highlighter: rouge + +== Overview + +Chronicle Wire is a library, yet it still has operational expectations for release engineering, +compatibility, and observability. This document anchors those expectations with `OPS` and `NF-O` +tags from the Nine-Box taxonomy. + +== Operational Targets + +[cols="1,3,2", options="header"] +|=== +|ID |Requirement |Notes + +|OPS-001 +|`mvn -q clean verify` must pass on every supported JDK (current CI branches: `develop-java8-ok`, +`develop-java11s-ok`, `develop-java17-ok`, and `develop-java21-ok`) before cutting a release. +|Run the command locally prior to opening a PR and after each rebase. + +|OPS-002 +|Release builds must be reproducible. Dependencies are pinned via the Chronicle BOM and validated by +`project-requirements.adoc`. Enforced by the build scan that fails when SNAPSHOT dependencies leak in. +|Check the output of `mvn -q -Dchronicletag=release verify` if a release candidate is prepared. + +|NF-O-001 +|Provide actionable diagnostics when serialisation fails. Exceptions must include the field name, type, +and offset where possible. Logging must stay off by default to avoid polluting low-latency workloads. +|`wire-error-handling.adoc` captures the message templates; parser suites such as +`InvalidYamWithCommonMistakesTest.java` and `TextWireTest.java` guard regressions. + +|OPS-003 +|Security review checklist (see `security-review.adoc`) must be completed for each PR. +Any unchecked item blocks the release candidate. +|Document findings or trade-offs directly in the relevant AsciiDoc file. + +|OPS-004 +|Documentation, code, and tests must move in lock-step. Changes that alter behaviour require updates +to `functional-requirements.adoc`, the relevant runbook, and at least one regression test. +|Reviewed during PRs; also supported by the `DOC` tagging guidance in `decision-log.adoc`. +|=== + +== Maintenance Windows + +* Patch releases follow Chronicle's standard Tuesday cadence. +* API-breaking changes are bundled into LTS drops and called out under `decision-log.adoc` + together with mitigation steps. + +== Runbook Links + +* `runbooks/wire-runtime-health.adoc` describes how to triage build or runtime regressions. +* `ops-scenarios.adoc` lists the operational situations that require the runbook above. diff --git a/src/main/docs/ops-scenarios.adoc b/src/main/docs/ops-scenarios.adoc new file mode 100644 index 000000000..150295b9c --- /dev/null +++ b/src/main/docs/ops-scenarios.adoc @@ -0,0 +1,35 @@ += Chronicle Wire Operational Scenarios +Chronicle Software +:toc: +:sectnums: +:lang: en-GB +:source-highlighter: rouge + +== Scenario Catalogue + +OPS-SC-001 :: Build Fails on `mvn -q clean verify` +* Trigger: CI or local build exits non-zero. + ** Runbook: <> + ** Requirement: OPS-001 +* Signals: Maven error logs, missing tests, or format violations. + ** Tools: `mvn -q -e verify`, `git status`, `scripts/check-doc-includes.py` + +OPS-SC-002 :: DocumentContext Leak or Unclosed Writer +* Trigger: Queue tailers stall or Chronicle Queue reports `DocumentContext not closed`. + ** Runbook: <> + ** Requirement: FN-002 +* Signals: Thread dumps pointing at `DocumentContext.close`, growing mapped file usage. + ** Tools: `jcmd Thread.print`, `wire-error-handling.adoc` + +OPS-SC-003 :: Schema Drift Between Producers and Consumers +* Trigger: Readers throw `UnknownFieldException` or skip fields unexpectedly. + ** Runbook: <> + ** Requirement: FN-101 through FN-103 +* Signals: Divergent git SHAs, new fields missing defaults, converter mismatches. + ** Tools: `wire-schema-evolution.adoc`, `functional-requirements.adoc`, `git bisect` + +== Usage + +* Each scenario ties back to a requirement ID so reviewers can confirm coverage. +* Update this file whenever new operational runbooks are added or when incident reviews + uncover a missing scenario. diff --git a/src/main/docs/runbooks/wire-runtime-health.adoc b/src/main/docs/runbooks/wire-runtime-health.adoc new file mode 100644 index 000000000..6888a7af3 --- /dev/null +++ b/src/main/docs/runbooks/wire-runtime-health.adoc @@ -0,0 +1,46 @@ += Chronicle Wire Runtime Health Runbook +Chronicle Software +:toc: +:sectnums: +:lang: en-GB +:source-highlighter: rouge + +== Purpose + +This runbook guides on-call engineers through the most common operational issues: +build failures, leaked `DocumentContext` instances, and schema drift. + +[[build-recovery]] +== Build Recovery (`mvn -q clean verify`) + +1. Confirm the failing JVM version. Run `java -version` and compare it with the CI lane. +2. Execute `mvn -q -e clean verify` locally. Capture the first stack trace. +3. If the failure stems from missing documentation, run `scripts/check-doc-includes.py` + and update the relevant `.adoc` files (see `functional-requirements.adoc`). +4. For test regressions, bisect with `git bisect run mvn -q -DskipTests=false test` + to isolate the offending commit. +5. After applying the fix, rerun `mvn -q clean verify` and attach the log to the PR. + +[[document-context-leak]] +== DocumentContext Leak Response + +1. Capture a thread dump: `jcmd Thread.print | tee /tmp/wire-thread-dump.txt`. +2. Search for `DocumentContext.close` or `AbstractMethodReader.readOne` stack traces. +3. If the writer side is stuck, enable debug logging for `net.openhft.chronicle.wire` temporarily + and reproduce the issue in a lower environment. +4. Inspect user code for missing try-with-resources blocks around `DocumentContext`. +5. Add coverage or assertions (`DocumentContextLifecycleTest`) and rerun the full build. + +[[schema-drift]] +== Schema Drift Investigation + +1. Compare producer and consumer SHAs. If they differ, check whether new fields were added without defaults. +2. Dump a problematic message with `WireDumper` (see `wire-cookbook.adoc`) and inspect field names. +3. Validate converters by running `mvn -q -Dtest=LongConversionTest test`. +4. Update `data-requirements.adoc` and `wire-schema-evolution.adoc` with the new field rules. +5. Add regression tests before redeploying. + +== Verification + +* Each runbook step must conclude with a green `mvn -q clean verify`. +* Link incident notes back to `ops-scenarios.adoc` so future contributors know which entry was exercised. diff --git a/src/main/docs/system-architecture.adoc b/src/main/docs/system-architecture.adoc new file mode 100644 index 000000000..88d22b0e4 --- /dev/null +++ b/src/main/docs/system-architecture.adoc @@ -0,0 +1,60 @@ += Chronicle Wire System Architecture +Chronicle Software +:toc: +:sectnums: +:lang: en-GB +:source-highlighter: rouge + +== Purpose + +This document summarises how Chronicle Wire's modules collaborate with the wider OpenHFT stack. +Use it as an orientation map before diving into the detailed views in `wire-architecture.adoc`, +`wire-schema-evolution.adoc`, and the component notes. + +== Context + +Chronicle Wire provides the serialisation substrate for components such as Chronicle Queue, +Chronicle Services, and higher-level trading stacks. It sits between business code and +`chronicle-bytes`, exposing a format-neutral API that maps directly to heap or off-heap buffers. + +== Component Map + +---- +Application Services -> Wire API -> WireType Implementations -> Chronicle Bytes -> Storage / Network + |-> Method Writer/Reader -> Event Consumers + |-> Converters / Marshallers -> Domain Objects +---- + +* **Application Services** use Wire to read and write domain objects without worrying about the + transport format. +* **Wire API** includes `Wire`, `WireIn`, `WireOut`, `DocumentContext`, `ValueIn`, and `ValueOut`. +* **WireType Implementations** (Text, YAML, JSON, Binary) convert API calls into bytes. +* **Chronicle Bytes** supplies direct-memory primitives and pooling. +* **Storage / Network** refers to Chronicle Queue, replication, or user-defined transports. + +== Data Flow + +1. The caller requests a document via `Wire.acquireWritingDocument`. +2. Fields are written using fluent `ValueOut` calls (or generated via `MethodWriter`). +3. The document is published, transferring ownership of the buffer slice. +4. Consumers wrap the slice in a `Wire` via the appropriate `WireType` and replay the fields. +5. Optional converters (`LongConverter`, `Base32LongConverter`, etc.) translate textual encodings + into numeric forms without allocations. + +== Cross-Cutting Concerns + +* **Threading:** `DocumentContext` enforces single-writer, single-reader semantics per instance. + Shared buffers are protected via Chronicle Bytes' CAS-based locking and sequencing. +* **Performance:** Hot loops avoid reflection; method writers generate bytecode-backed proxies. +* **Resilience:** Error handling is centralised in `wire-error-handling.adoc`; operational + responses live in `ops-scenarios.adoc`. +* **Documentation Sync:** Any change to the architecture must update this file as well as the + detailed component documentation. + +== Future Work + +Chronicle Wire continues to adopt decisions recorded in `decision-log.adoc`. Upcoming investigations +include: + +* Expanding support for pluggable `ClassLookup` strategies for sandboxed environments. +* Tightening integration with streaming schema registries without regressing latency. From 7629ba4ab97b69a6b3e1bd0e858499f48afb23bf Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Sat, 8 Nov 2025 22:26:04 +0000 Subject: [PATCH 8/8] Expand wire regression coverage and docs --- src/main/docs/index.adoc | 27 ++++++++ .../wire/DocumentContextLifecycleTest.java | 24 +++++++ .../chronicle/wire/ElasticByteBufferTest.java | 39 +++++++++++ .../chronicle/wire/MethodWriterBytesTest.java | 59 ++++++++++++++++ .../wire/MethodWriterHistoryTest.java | 67 +++++++++++++++++++ .../openhft/chronicle/wire/QueryWireTest.java | 66 ++++++++++++++++++ .../wire/ReadAnyWireDetectionTest.java | 66 ++++++++++++++++++ .../chronicle/wire/WireTextBugTest.java | 49 ++++++++++++-- 8 files changed, 391 insertions(+), 6 deletions(-) create mode 100644 src/test/java/net/openhft/chronicle/wire/MethodWriterHistoryTest.java create mode 100644 src/test/java/net/openhft/chronicle/wire/ReadAnyWireDetectionTest.java diff --git a/src/main/docs/index.adoc b/src/main/docs/index.adoc index d78a1e9ad..3491f5cea 100644 --- a/src/main/docs/index.adoc +++ b/src/main/docs/index.adoc @@ -11,6 +11,21 @@ Chronicle Wire is a high-performance, low-latency serialisation library that for link:wire-architecture.adoc[Chronicle Wire: Architectural Overview] :: Summarises the internal architecture, components and design principles. +link:system-architecture.adoc[System Architecture Summary] :: + Shows how Chronicle Wire links application services, wire formats, and Chronicle Bytes. + +link:functional-requirements.adoc[Functional Requirements Catalogue] :: + Lists behavioural contracts with traceability to regression tests. + +link:data-requirements.adoc[Data Requirements] :: + Defines message envelopes, metadata, and schema evolution rules. + +link:operational-requirements.adoc[Operational Requirements] :: + Captures build gates, diagnostics policy, and documentation expectations. + +link:interface-control.adoc[Interface Control Document] :: + Documents the contracts exposed to applications (`Wire`, `DocumentContext`, writers/readers). + link:wire-cookbook.adoc[Chronicle Wire Cookbook: Practical Recipes] :: Offers practical recipes for common tasks and advanced techniques. @@ -23,6 +38,12 @@ link:wire-schema-evolution.adoc[Chronicle Wire: Schema Evolution Deep Dive] :: link:wire-error-handling.adoc[Chronicle Wire: Error Handling and Diagnostics Guide] :: Covers diagnosing issues and dealing with errors. +link:ops-scenarios.adoc[Operational Scenarios] :: + Ties common incidents to requirements and runbooks. + +link:runbooks/wire-runtime-health.adoc[Runtime Health Runbook] :: + Provides step-by-step recovery processes for build failures, leaks, and schema drift. + link:wire-faq.adoc[Chronicle Wire FAQ: Common Questions Answered] :: Provides answers to frequent questions. @@ -35,6 +56,12 @@ link:project-requirements.adoc[Project Requirements: Chronicle Wire Documentatio link:security-review.adoc[Chronicle Wire Security Review] :: Discusses known security considerations and trade-offs in the library. +link:decision-log.adoc[Decision Log] :: + Chronicles architectural and process choices with rationale. + +link:background/openhft-knowledge-pack.adoc[OpenHFT Knowledge Pack] :: + Records version support, JVM targets, and tuning notes. + link:../../EventsByMethod.adoc[Events by Method Guide] :: Explains the event-driven approach via asynchronous method calls. diff --git a/src/test/java/net/openhft/chronicle/wire/DocumentContextLifecycleTest.java b/src/test/java/net/openhft/chronicle/wire/DocumentContextLifecycleTest.java index 21bfd077a..ef33dd5a9 100644 --- a/src/test/java/net/openhft/chronicle/wire/DocumentContextLifecycleTest.java +++ b/src/test/java/net/openhft/chronicle/wire/DocumentContextLifecycleTest.java @@ -62,4 +62,28 @@ public void textUseTextDocumentsLifecycle() { } assertTrue(w.writingIsComplete()); } + + @Test + public void rollbackKeepsDocumentAvailableForNextRead() { + Wire w = WireType.BINARY.apply(Bytes.allocateElasticOnHeap(256)); + try (DocumentContext dc = w.writingDocument()) { + dc.wire().write("item").text("value"); + } + + try (DocumentContext dc = w.readingDocument()) { + assertTrue(dc.isPresent()); + dc.rollbackOnClose(); + } + + try (DocumentContext dc = w.readingDocument()) { + assertTrue("document should still be present after rollback", dc.isPresent()); + assertEquals("value", dc.wire().read("item").text()); + } + + try (DocumentContext dc = w.readingDocument()) { + assertFalse(dc.isPresent()); + } + w.bytes().releaseLast(); + } + } diff --git a/src/test/java/net/openhft/chronicle/wire/ElasticByteBufferTest.java b/src/test/java/net/openhft/chronicle/wire/ElasticByteBufferTest.java index d50482f81..11295bd37 100644 --- a/src/test/java/net/openhft/chronicle/wire/ElasticByteBufferTest.java +++ b/src/test/java/net/openhft/chronicle/wire/ElasticByteBufferTest.java @@ -11,6 +11,7 @@ import org.junit.Test; import java.nio.ByteBuffer; +import java.util.Arrays; import static org.junit.Assume.assumeFalse; @@ -45,4 +46,42 @@ public void testElasticByteBufferWithWire() { byteBufferBytes.releaseLast(); } + + @Test + public void directElasticBufferResizesWhenCapacityIsExceeded() { + assumeFalse(Jvm.maxDirectMemory() == 0); + + for (boolean padding : new boolean[]{true, false}) { + Bytes directBytes = Bytes.allocateElasticDirect(32); + try { + Wire wire = WireType.BINARY.apply(directBytes); + wire.usePadding(padding); + + long initialCapacity = directBytes.realCapacity(); + String largeValue = repeat('x', (int) initialCapacity + 64); + + try (DocumentContext context = wire.writingDocument(false)) { + context.wire().write("payload").text(largeValue); + } + + Assert.assertTrue("buffer should grow when payload exceeds initial capacity", + directBytes.realCapacity() >= initialCapacity); + + directBytes.readPositionRemaining(0, directBytes.writePosition()); + try (DocumentContext context = wire.readingDocument()) { + Assert.assertTrue(context.isPresent()); + Assert.assertEquals("padding=" + padding, largeValue, + context.wire().read("payload").text()); + } + } finally { + directBytes.releaseLast(); + } + } + } + + private static String repeat(char ch, int length) { + char[] data = new char[length]; + Arrays.fill(data, ch); + return new String(data); + } } diff --git a/src/test/java/net/openhft/chronicle/wire/MethodWriterBytesTest.java b/src/test/java/net/openhft/chronicle/wire/MethodWriterBytesTest.java index 33710dce6..5be3e5e1c 100644 --- a/src/test/java/net/openhft/chronicle/wire/MethodWriterBytesTest.java +++ b/src/test/java/net/openhft/chronicle/wire/MethodWriterBytesTest.java @@ -45,6 +45,10 @@ public void test() throws InterruptedException { Bytes result = q.poll(10, TimeUnit.SECONDS); // Verify that the fetched message matches the expected content Assert.assertEquals("hello", result.toString()); + if (result != null) { + result.releaseLast(); + } + w.bytes().releaseLast(); } /** @@ -53,4 +57,59 @@ public void test() throws InterruptedException { private void println(Bytes bytes) { q.add(bytes); } + + @Test + public void reusedBytesRemainStableAcrossDispatches() throws InterruptedException { + Wire wire = new BinaryWire(Bytes.allocateElasticOnHeap()); + Print printer = wire.methodWriter(Print.class); + Bytes reusable = Bytes.allocateElasticOnHeap(); + try { + reusable.writeUtf8("alpha"); + printer.msg(reusable); + + reusable.clear(); + reusable.writeUtf8("beta"); + printer.msg(reusable); + + ArrayBlockingQueue sink = new ArrayBlockingQueue<>(2); + MethodReader reader = wire.methodReader((Print) bytes -> { + bytes.readPosition(0); + sink.add(bytes.readUtf8()); + }); + + Assert.assertTrue(reader.readOne()); + Assert.assertEquals("alpha", sink.poll(5, TimeUnit.SECONDS)); + Assert.assertTrue(reader.readOne()); + Assert.assertEquals("beta", sink.poll(5, TimeUnit.SECONDS)); + } finally { + reusable.releaseLast(); + wire.bytes().releaseLast(); + } + } + + @Test + public void producerMutationDuringCallbackDoesNotCorruptPayload() throws InterruptedException { + Wire wire = new BinaryWire(Bytes.allocateElasticOnHeap()); + Bytes shared = Bytes.allocateElasticOnHeap(); + try { + Print printer = wire.methodWriter(Print.class); + shared.writeUtf8("original"); + printer.msg(shared); + + ArrayBlockingQueue sink = new ArrayBlockingQueue<>(1); + MethodReader reader = wire.methodReader((Print) bytes -> { + // mutate the shared Bytes while the reader is consuming + shared.clear(); + shared.writeUtf8("mutated"); + bytes.readPosition(0); + sink.add(bytes.readUtf8()); + }); + + Assert.assertTrue(reader.readOne()); + Assert.assertEquals("original", sink.poll(5, TimeUnit.SECONDS)); + } finally { + shared.releaseLast(); + wire.bytes().releaseLast(); + } + } } diff --git a/src/test/java/net/openhft/chronicle/wire/MethodWriterHistoryTest.java b/src/test/java/net/openhft/chronicle/wire/MethodWriterHistoryTest.java new file mode 100644 index 000000000..f8c27646b --- /dev/null +++ b/src/test/java/net/openhft/chronicle/wire/MethodWriterHistoryTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2016-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.chronicle.wire; + +import net.openhft.chronicle.bytes.Bytes; +import net.openhft.chronicle.bytes.MethodReader; +import net.openhft.chronicle.core.pool.ClassAliasPool; +import net.openhft.chronicle.wire.ValueIn; +import org.junit.Test; + +import static net.openhft.chronicle.bytes.MethodReader.MESSAGE_HISTORY_METHOD_ID; +import static org.junit.Assert.*; + +public class MethodWriterHistoryTest extends WireTestCommon { + + interface Events { + void event(CharSequence value); + } + + static class RecordingTextWire extends TextWire { + RecordingTextWire(Bytes bytes) { + super(bytes); + useTextDocuments(); + } + + @Override + public boolean recordHistory() { + return true; + } + } + + @Test + public void historyEventIsPrependedWhenRecordingEnabled() { + Bytes bytes = Bytes.allocateElasticOnHeap(256); + Wire wire = new RecordingTextWire(bytes); + try { + VanillaMessageHistory suppliedHistory = new VanillaMessageHistory(); + suppliedHistory.reset(7, 11); + suppliedHistory.addTiming(123L); + suppliedHistory.addSourceDetails(false); + MessageHistory.set(suppliedHistory); + + ClassAliasPool.CLASS_ALIASES.addAlias(VanillaMessageHistory.class); + Events writer = wire.methodWriter(Events.class); + writer.event("payload"); + + bytes.readPositionRemaining(0, bytes.writePosition()); + try (DocumentContext dc = wire.readingDocument()) { + assertTrue(dc.isPresent()); + long historyEventId = dc.wire().readEventNumber(); + assertEquals(MESSAGE_HISTORY_METHOD_ID, historyEventId); + VanillaMessageHistory captured = dc.wire().getValueIn().object(VanillaMessageHistory.class); + assertEquals(suppliedHistory.sources(), captured.sources()); + assertEquals(suppliedHistory.sourceId(0), captured.sourceId(0)); + + StringBuilder callName = new StringBuilder(); + ValueIn callValue = dc.wire().readEventName(callName); + assertEquals("event", callName.toString()); + assertEquals("payload", callValue.text()); + } + } finally { + MessageHistory.clear(); + bytes.releaseLast(); + } + } +} diff --git a/src/test/java/net/openhft/chronicle/wire/QueryWireTest.java b/src/test/java/net/openhft/chronicle/wire/QueryWireTest.java index 1511877c2..123686e72 100644 --- a/src/test/java/net/openhft/chronicle/wire/QueryWireTest.java +++ b/src/test/java/net/openhft/chronicle/wire/QueryWireTest.java @@ -9,9 +9,11 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.List; import static net.openhft.chronicle.bytes.Bytes.allocateElasticOnHeap; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -64,6 +66,8 @@ public void readWriteQuery() { // Verify that the results list contains the correct values assertEquals(new ArrayList<>(Arrays.asList(true, 12345L, "Hello World", 12.345)), results); + + bytes.releaseLast(); } @Test @@ -97,4 +101,66 @@ public void writesAndReadsQueryFragments() { bytes.releaseLast(); } + + @Test + public void percentEncodedCharactersRemainLiteral() { + @NotNull QueryWire wire = createWire(); + String literal = "value%2Bplus+space"; + wire.write("token").text(literal); + + assertTrue(bytes.toString().contains("token=" + literal)); + + bytes.readPositionRemaining(0, bytes.writePosition()); + QueryWire reader = new QueryWire(bytes); + assertEquals(literal, reader.read("token").text()); + bytes.releaseLast(); + } + + @Test + public void handlesZeroBytesAndDanglingKeys() { + Bytes storage = allocateElasticOnHeap(); + QueryWire wire = new QueryWire(storage); + byte[] raw = new byte[]{'A', 0, 'B'}; + wire.write("raw").rawBytes(raw); + wire.write("encoded").bytes(raw); + + storage.readPositionRemaining(0, storage.writePosition()); + QueryWire reader = new QueryWire(storage); + Bytes sink = allocateElasticOnHeap(); + reader.read("raw").textTo(sink); + assertArrayEquals(raw, sink.toByteArray()); + sink.releaseLast(); + assertEquals(Base64.getEncoder().encodeToString(raw), reader.read("encoded").text()); + storage.releaseLast(); + + Bytes truncated = Bytes.from("done=true&dangling"); + try { + QueryWire danglingReader = new QueryWire(truncated); + assertEquals("true", danglingReader.read("done").text()); + assertEquals("", danglingReader.read("dangling").text()); + } finally { + truncated.releaseLast(); + } + } + + @Test + public void queryWireOutputCanFeedTextWireAfterFormatting() { + @NotNull QueryWire wire = createWire(); + wire.write("name").text("alpha beta"); + wire.write("count").int64(7); + + String yamlLike = bytes.toString() + .replace("&", "\n") + .replace("=", ": "); + + Bytes yamlBytes = Bytes.from(yamlLike); + try { + Wire textWire = WireType.TEXT.apply(yamlBytes); + assertEquals("alpha beta", textWire.read("name").text()); + assertEquals(7L, textWire.read("count").int64()); + } finally { + yamlBytes.releaseLast(); + bytes.releaseLast(); + } + } } diff --git a/src/test/java/net/openhft/chronicle/wire/ReadAnyWireDetectionTest.java b/src/test/java/net/openhft/chronicle/wire/ReadAnyWireDetectionTest.java new file mode 100644 index 000000000..56232b65a --- /dev/null +++ b/src/test/java/net/openhft/chronicle/wire/ReadAnyWireDetectionTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2016-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.chronicle.wire; + +import net.openhft.chronicle.bytes.Bytes; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.function.Consumer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class ReadAnyWireDetectionTest extends WireTestCommon { + + @Test + public void detectsWireTypeAndReadsPayload() { + for (TestCase testCase : cases()) { + Bytes encoded = encode(testCase.type, wire -> { + try (DocumentContext dc = wire.writingDocument(false)) { + dc.wire().write("msg").text(testCase.payload); + } + }); + ReadAnyWire readAnyWire = new ReadAnyWire(encoded); + WireType detected; + try (DocumentContext dc = readAnyWire.readingDocument()) { + assertTrue(dc.isPresent()); + assertEquals(testCase.payload, dc.wire().read("msg").text()); + detected = readAnyWire.underlyingType().get(); + } + assertEquals(testCase.expectedType, detected); + encoded.releaseLast(); + } + } + + private Collection cases() { + return Arrays.asList( + new TestCase(WireType.TEXT, "lorem ipsum", WireType.TEXT), + new TestCase(WireType.BINARY, "binary-payload", WireType.BINARY) + ); + } + + private Bytes encode(WireType type, Consumer writer) { + Bytes bytes = Bytes.allocateElasticOnHeap(256); + Wire wire = type.apply(bytes); + writer.accept(wire); + Bytes copy = Bytes.wrapForRead(bytes.toByteArray()); + bytes.releaseLast(); + copy.readPositionRemaining(0, copy.writePosition()); + return copy; + } + + private static final class TestCase { + final WireType type; + final String payload; + final WireType expectedType; + + private TestCase(WireType type, String payload, WireType expectedType) { + this.type = type; + this.payload = payload; + this.expectedType = expectedType; + } + } +} diff --git a/src/test/java/net/openhft/chronicle/wire/WireTextBugTest.java b/src/test/java/net/openhft/chronicle/wire/WireTextBugTest.java index fda73644b..eb9edd51a 100644 --- a/src/test/java/net/openhft/chronicle/wire/WireTextBugTest.java +++ b/src/test/java/net/openhft/chronicle/wire/WireTextBugTest.java @@ -30,6 +30,9 @@ * @author Rob Austin */ public class WireTextBugTest extends WireTestCommon { + private static final String BUG_TEXT = "!Bug {\n" + + " clOrdID: \"FIX.4.4:12345678_client1->FOO/MINI1-1234567891234-12\"\n" + + "}\n"; @org.junit.Test // Test for handling text within the Wire framework @@ -47,9 +50,7 @@ public void testText() { b.setClOrdID("FIX.4.4:12345678_client1->FOO/MINI1-1234567891234-12"); // Check the Bug object's string representation - assertEquals("!Bug {\n" + - " clOrdID: \"FIX.4.4:12345678_client1->FOO/MINI1-1234567891234-12\"\n" + - "}\n", b.toString()); + assertEquals(BUG_TEXT, b.toString()); // Write the Bug object to the wire encodeWire.getValueOut().object(b); @@ -66,15 +67,51 @@ public void testText() { @Nullable Bug b2 = (Bug) o; // Check the deserialized Bug object's string representation - assertEquals("!Bug {\n" + - " clOrdID: \"FIX.4.4:12345678_client1->FOO/MINI1-1234567891234-12\"\n" + - "}\n", b2.toString()); + assertEquals(BUG_TEXT, b2.toString()); // Release resources encodeWire.bytes().releaseLast(); decodeWire.bytes().releaseLast(); } + @org.junit.Test + public void textWireOnDirectBytesSurvivesBufferMutation() { + assumeFalse(Jvm.maxDirectMemory() == 0); + ClassAliasPool.CLASS_ALIASES.addAlias(Bug.class); + + Bytes directBytes = Bytes.allocateElasticDirect(128); + try { + Wire encodeWire = new TextWire(directBytes); + Bug bug = new Bug(); + bug.setClOrdID("FIX.4.4:12345678_client1->FOO/MINI1-1234567891234-12"); + encodeWire.getValueOut().object(bug); + + byte[] snapshot = encodeWire.bytes().toByteArray(); + directBytes.readPositionRemaining(0, directBytes.writePosition()); + Wire decodeWire = new TextWire(directBytes); + Bug decoded = decodeWire.getValueIn().object(Bug.class); + assertEquals(BUG_TEXT, decoded.toString()); + + directBytes.zeroOut(0, directBytes.realCapacity()); + directBytes.clear(); + + // The decoded object should keep its text even though the backing buffer was zeroed. + assertEquals(BUG_TEXT, decoded.toString()); + + // Local mutations must not impact a fresh decode from the saved snapshot. + decoded.setClOrdID(decoded.getClOrdID() + "-local"); + Wire snapshotWire = new TextWire(Bytes.wrapForRead(snapshot)); + try { + Bug snapshotBug = snapshotWire.getValueIn().object(Bug.class); + assertEquals(BUG_TEXT, snapshotBug.toString()); + } finally { + snapshotWire.bytes().releaseLast(); + } + } finally { + directBytes.releaseLast(); + } + } + // Inner class to represent a Bug with a single field clOrdID static class Bug extends SelfDescribingMarshallable { private String clOrdID; // Field to hold some string identifier