Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
63e7f0b
Chore(deps): bump rust-toolchain
dependabot[bot] Oct 22, 2026
809fa9e
Use try_update for atomic counters
leynos Oct 22, 2026
4e4a2cf
Specify the version that deprecated the compatibility handlers
leynos Oct 22, 2026
1c3b768
Use map_or_else for default protocol hooks
leynos Oct 22, 2026
6d086fc
Drop must_use on functions returning must_use types
leynos Oct 22, 2026
5a0e98c
Remove unfulfilled integer_division_remainder_used expectations
leynos Oct 22, 2026
b5bc0dc
Format protocol hooks default
leynos Oct 22, 2026
2a02d0a
Fix lints raised by the new toolchain in wireframe_testing
leynos Oct 22, 2026
6dd2cb5
Fix lints raised by the new toolchain in tests and examples
leynos Oct 22, 2026
d99f909
Avoid unstable map_or_default in the tracing fixture
leynos Oct 22, 2026
65dd918
Refresh trybuild snapshots for the new compiler diagnostics
leynos Oct 22, 2026
014208f
Allow the compiler-quoted server impl header in spelling checks
leynos Oct 22, 2026
336404e
Merge main into the toolchain bump
leynos Oct 22, 2026
9ddf5e8
Lower the Rust coverage baseline for the toolchain bump
leynos Oct 22, 2026
873faa3
Revert "Lower the Rust coverage baseline for the toolchain bump"
leynos Oct 22, 2026
0bab054
Start a new coverage baseline for the toolchain bump
leynos Oct 22, 2026
a5ed0ae
Expect the new baseline path in the coverage contract test
leynos Oct 22, 2026
fc31549
Count negative integer digits by magnitude in the RESP example
leynos Oct 22, 2026
67dd286
Test that message identifiers stay distinct under contention
leynos Oct 22, 2026
e74b872
Add a property test for signed digit counting
leynos Oct 23, 2026
ea5ce8c
Point the Cranelift provisioning and guide at the new pin
leynos Oct 23, 2026
da8cecb
Test RESP integer encoding at signed boundaries and the frame limit
leynos Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coverage-baseline-v2.rust
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
87.8
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ jobs:
# Both runner choices are Ubuntu. Confirm the manifest's Cranelift
# component and install the Linux linker before debug Make targets.
run: |
rustup component add rustc-codegen-cranelift-preview --toolchain nightly-2026-03-26
rustup component add rustc-codegen-cranelift-preview --toolchain nightly-2026-10-22
sudo apt-get update
sudo apt-get install -y mold
mold --version
Expand Down Expand Up @@ -147,6 +147,10 @@ jobs:
output-path: lcov.info
format: lcov
with-ratchet: 'true'
# A new path starts a new baseline: the cache entry's version hashes
# this path, so the restore misses the 89.0% measured on the previous
# toolchain and the committed file sets the baseline.
baseline-rust-file: .coverage-baseline-v2.rust
# The ratchet reads this report; nothing else does, so it is not
# published as an artefact.
publish-artefact: 'false'
4 changes: 4 additions & 0 deletions .github/workflows/coverage-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ jobs:
output-path: lcov.info
format: lcov
with-ratchet: 'true'
# A new path starts a new baseline: the cache entry's version hashes
# this path, so the restore misses the 89.0% measured on the previous
# toolchain and the committed file sets the baseline.
baseline-rust-file: .coverage-baseline-v2.rust
# Reports whether the token is configured without binding it anywhere:
# the expression is evaluated before the shell runs, so this step
# writes `available=true` or `available=false` and the token is in no
Expand Down
15 changes: 8 additions & 7 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -963,7 +963,7 @@ as a test assertion on the SHA string.

## Development builds

The repository pins `nightly-2026-03-26` with `rustfmt`, `clippy`, and
The repository pins `nightly-2026-10-22` with `rustfmt`, `clippy`, and
`rust-analyzer`. The toolchain also needs `rustc-codegen-cranelift-preview` for
the optional development backend. Native Linux development builds use `mold` as
the linker; CI provisions the component and linker before running the standard
Expand Down Expand Up @@ -1025,9 +1025,10 @@ making the faster backend an explicit choice for routine debug work.
## Cranelift

The Make test targets still select the development fragment, but the test
profile overrides Cranelift with LLVM on the pinned `nightly-2026-03-26`
toolchain. This exception follows reproduced test failures: with Cranelift,
`make test` aborted in
profile overrides Cranelift with LLVM. The failures behind this exception were
reproduced on the previously pinned `nightly-2026-03-26` and have not been
re-measured on `nightly-2026-10-22`. This exception follows reproduced test
failures: with Cranelift, `make test` aborted in
`client::pool::sync::tests::lock_or_recover_reads_poisoned_mutex` with
`SIGABRT` and `fatal runtime error: failed to initiate panic, error 5`. The
test passed with the LLVM test-profile override. Under Cranelift, a metrics
Expand All @@ -1041,9 +1042,9 @@ source fragment; reproduce the recorded failures and reassess the override when
the toolchain pin changes. The failures above are the first measurement; the
re-measurement below supersedes their details.

Re-measured on 2026-09-28 on the pinned `nightly-2026-03-26`, with Cranelift
for both the development and test profiles, as part of adopting the build
standard. The exception stands. Three test binaries fail:
Re-measured on 2026-09-28 on the then-pinned `nightly-2026-03-26`, with
Cranelift for both the development and test profiles, as part of adopting the
build standard. The exception stands. Three test binaries fail:

- `wireframe` library tests abort with `SIGABRT` and
`failed to initiate panic, error 5` at
Expand Down
23 changes: 23 additions & 0 deletions examples/resp_codec_impl/codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,26 @@ impl FrameCodec for RespFrameCodec {

fn max_frame_length(&self) -> usize { self.max_frame_length }
}

#[cfg(test)]
mod tests {
use bytes::BytesMut;
use tokio_util::codec::Encoder;

use super::RespAdapter;
use crate::resp_codec_impl::frame::RespFrame;

/// `:-99\r\n` is six bytes: it fits a six-byte limit exactly and is
/// rejected by a five-byte one.
#[test]
fn negative_integer_frame_is_accepted_at_its_exact_size_limit() {
let frame = RespFrame::Integer(-99);
let mut dst = BytesMut::new();
assert!(RespAdapter::new(6).encode(frame.clone(), &mut dst).is_ok());
assert_eq!(dst.as_ref(), b":-99\r\n");

let mut rejected = BytesMut::new();
assert!(RespAdapter::new(5).encode(frame, &mut rejected).is_err());
assert!(rejected.is_empty());
}
}
67 changes: 63 additions & 4 deletions examples/resp_codec_impl/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ fn encode_frame_at(frame: &RespFrame, dst: &mut BytesMut, depth: usize) -> Resul
fn digits_len_usize(mut value: usize) -> usize {
let mut digits = 1;
while value >= 10 {
value /= 10;
value = value.div_euclid(10);
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Resolved
digits += 1;
}
digits
Expand All @@ -123,14 +123,73 @@ fn digits_len_usize(mut value: usize) -> usize {
/// Count digits in a signed integer, including its possible minus sign.
fn digits_len_i64(value: i64) -> usize {
let mut digits = 1;
let mut cursor = value;
while cursor <= -10 || cursor >= 10 {
cursor /= 10;
let mut cursor = value.unsigned_abs();
while cursor >= 10 {
cursor = cursor.div_euclid(10);
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Resolved
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Resolved
Comment thread
leynos marked this conversation as resolved.
Resolved
digits += 1;
}
if value < 0 { digits + 1 } else { digits }
}

#[cfg(test)]
mod tests {
use super::digits_len_i64;

/// Negative values must count their magnitude, not round toward negative
/// infinity: `-99` is three characters and `-10` is three.
#[test]
fn digits_len_i64_matches_the_rendered_length() {
for value in [i64::MIN, -100, -99, -10, -9, -1, 0, 1, 9, 10, 99, i64::MAX] {
assert_eq!(digits_len_i64(value), value.to_string().len(), "{value}");
}
}

/// Exact length and wire bytes for the signed boundaries.
#[test]
fn integer_frames_encode_to_their_measured_length() -> Result<(), std::io::Error> {
use bytes::BytesMut;

use super::{encode_frame, encoded_len};
use crate::resp_codec_impl::frame::RespFrame;

for (value, wire) in [
(-99_i64, ":-99\r\n".to_owned()),
(i64::MIN, format!(":{}\r\n", i64::MIN)),
] {
let frame = RespFrame::Integer(value);
let mut dst = BytesMut::new();
encode_frame(&frame, &mut dst)?;
assert_eq!(dst.as_ref(), wire.as_bytes());
assert_eq!(encoded_len(&frame)?, wire.len());
}
Ok(())
}

proptest::proptest! {
/// The measured length equals the emitted bytes for any integer frame.
#[test]
fn encoded_len_matches_emitted_bytes(value in proptest::prelude::any::<i64>()) {
use bytes::BytesMut;

use super::{encode_frame, encoded_len};
use crate::resp_codec_impl::frame::RespFrame;

let frame = RespFrame::Integer(value);
let mut dst = BytesMut::new();
proptest::prop_assert!(encode_frame(&frame, &mut dst).is_ok());
proptest::prop_assert_eq!(encoded_len(&frame).ok(), Some(dst.len()));
}
}

proptest::proptest! {
/// Any `i64` renders with exactly the length the helper reports.
#[test]
fn digits_len_i64_matches_for_any_value(value in proptest::prelude::any::<i64>()) {
proptest::prop_assert_eq!(digits_len_i64(value), value.to_string().len());
}
}
}

/// Add encoded-size components and report overflow as a frame-size error.
fn checked_add(lhs: usize, rhs: usize) -> Result<usize, io::Error> {
lhs.checked_add(rhs)
Expand Down
2 changes: 1 addition & 1 deletion examples/resp_codec_impl/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const MAX_RECURSION_DEPTH: usize = 64;
fn decimal_digits(mut value: usize) -> usize {
let mut digits = 1;
while value >= 10 {
value /= 10;
value = value.div_euclid(10);
digits += 1;
}
digits
Expand Down
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[toolchain]
channel = "nightly-2026-03-26"
channel = "nightly-2026-10-22"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
components = ["rustfmt", "clippy", "rust-analyzer", "rustc-codegen-cranelift-preview"]
3 changes: 1 addition & 2 deletions src/app/builder/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,7 @@ where
pub fn protocol_hooks(&self) -> ProtocolHooks<F::Frame, ()> {
self.protocol
.as_ref()
.map(ProtocolHooks::from_protocol)
.unwrap_or_default()
.map_or_else(ProtocolHooks::default, ProtocolHooks::from_protocol)
}

/// Get the configured message assembler, if any.
Expand Down
2 changes: 1 addition & 1 deletion src/app/codec_driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ mod tests {
assert_eq!(first.len(), 1);

let second = pipeline.drain_output();
assert!(second.is_empty());
assert_eq!(second, [] as [crate::app::Envelope; 0]);
}

#[rstest]
Expand Down
10 changes: 8 additions & 2 deletions src/app/inbound_handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,10 @@ where
/// # Errors
///
/// Returns an [`io::Error`] if stream processing or handler execution fails.
#[deprecated(note = "prepare the app once, then call PreparedApp::handle_connection_result")]
#[deprecated(
since = "0.3.0",
note = "prepare the app once, then call PreparedApp::handle_connection_result"
)]
pub async fn handle_connection_result<W>(&self, stream: W) -> io::Result<()>
where
W: AsyncRead + AsyncWrite + Send + Unpin + 'static,
Expand All @@ -187,7 +190,10 @@ where
}

/// Handle a connection through the compatibility preparation path.
#[deprecated(note = "prepare the app once, then call PreparedApp::handle_connection")]
#[deprecated(
since = "0.3.0",
note = "prepare the app once, then call PreparedApp::handle_connection"
)]
pub async fn handle_connection<W>(&self, stream: W)
where
W: AsyncRead + AsyncWrite + Send + Unpin + 'static,
Expand Down
8 changes: 4 additions & 4 deletions src/app/prepared_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,10 +327,10 @@ where
/// ```
#[must_use]
pub fn protocol_hooks(&self) -> crate::hooks::ProtocolHooks<F::Frame, ()> {
self.protocol
.as_ref()
.map(crate::hooks::ProtocolHooks::from_protocol)
.unwrap_or_default()
self.protocol.as_ref().map_or_else(
crate::hooks::ProtocolHooks::default,
crate::hooks::ProtocolHooks::from_protocol,
)
}

/// Get the configured message assembler, if any.
Expand Down
2 changes: 1 addition & 1 deletion src/client/pool/lease.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ where
self.pending_responses.fetch_add(1, Ordering::AcqRel);
}
PendingResponseChange::Remove => {
let _ = self.pending_responses.fetch_update(
let _ = self.pending_responses.try_update(
Comment thread
leynos marked this conversation as resolved.
Resolved
Ordering::AcqRel,
Ordering::Acquire,
|pending| Some(pending.saturating_sub(1)),
Expand Down
4 changes: 0 additions & 4 deletions src/client/pool/scheduler/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@ where
}

/// Race slot capacity against shutdown or cancellation for one waiter.
#[expect(
clippy::integer_division_remainder_used,
reason = "tokio::select! macro internally uses % for random branch selection"
)]
async fn service_one_waiter(
&self,
mut sender: WaiterSender<S, P, C>,
Expand Down
4 changes: 0 additions & 4 deletions src/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,10 +337,6 @@ where
///
/// The `strict_priority_order` and `shutdown_signal_precedence` tests
/// assert that this ordering is preserved across refactors.
#[expect(
clippy::integer_division_remainder_used,
reason = "tokio::select! expands to modulus operations internally"
)]
async fn next_event(&mut self, state: &ActorState) -> Event<F, E> {
let avail = self.compute_availability(state);
let can_poll_multi_packet = avail.output == AvailableOutput::MultiPacket;
Expand Down
1 change: 0 additions & 1 deletion src/extractor/streaming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ impl StreamingBody {
///
/// Use this when you need direct access to the stream for custom
/// processing with [`futures::StreamExt`] methods.
#[must_use]
pub fn into_stream(self) -> crate::request::RequestBodyStream { self.stream }

/// Convert to an [`AsyncRead`] adapter.
Expand Down
2 changes: 1 addition & 1 deletion src/fragment/fragmenter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ impl Fragmenter {
pub fn next_message_id(&self) -> MessageId {
match self
.next_message_id
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
.try_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
Comment thread
leynos marked this conversation as resolved.
Resolved
current.checked_add(1)
}) {
Ok(previous) => MessageId::new(previous),
Expand Down
39 changes: 38 additions & 1 deletion src/fragment/tests/fragmenter_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ fn fragmenter_handles_empty_payload() {
.fragments()
.first()
.expect("batch should contain at least one fragment");
assert!(fragment.payload().is_empty());
assert_eq!(fragment.payload(), []);
assert!(fragment.header().is_last_fragment());
assert_eq!(fragment.header().fragment_index(), FragmentIndex::zero());
}
Expand Down Expand Up @@ -153,3 +153,40 @@ fn fragmenter_returns_error_for_out_of_bounds_slice() {
other => panic!("expected SliceBounds, got {other:?}"),
}
}

/// Concurrent callers each receive a distinct identifier and none observes the
/// exhaustion panic, so the compare-exchange behind `try_update` retries on
/// contention rather than failing.
#[test]
fn fragmenter_allocates_distinct_ids_under_contention() -> TestResult {
const THREADS: usize = 8;
const IDS_PER_THREAD: usize = 2_000;

let fragmenter = std::sync::Arc::new(Fragmenter::new(
NonZeroUsize::new(8).ok_or("fragment size must be non-zero")?,
));
let handles: Vec<_> = (0..THREADS)
.map(|_| {
let fragmenter = std::sync::Arc::clone(&fragmenter);
std::thread::spawn(move || {
(0..IDS_PER_THREAD)
.map(|_| fragmenter.next_message_id())
.collect::<Vec<_>>()
})
})
.collect();
let mut ids = std::collections::HashSet::new();
for handle in handles {
let batch = handle.join().map_err(|_| "id allocation thread panicked")?;
ids.extend(batch);
}
if ids.len() != THREADS * IDS_PER_THREAD {
return Err(format!(
"expected {} distinct identifiers, observed {}",
THREADS * IDS_PER_THREAD,
ids.len()
)
.into());
}
Ok(())
}
2 changes: 1 addition & 1 deletion src/preamble/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ async fn bounded_preamble_preserves_valid_containers_and_following_bytes(#[case]
.await
.expect("preamble within the budget must decode");
assert_eq!(decoded, value);
assert!(leftover.is_empty());
assert_eq!(leftover, [] as [u8; 0]);
assert_eq!(reader.position(), encoded_len as u64);
assert!(encoded_len <= MAX_PREAMBLE_LEN);
assert_eq!(
Expand Down
4 changes: 0 additions & 4 deletions src/push/queues/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,6 @@ impl<F: FrameLike> PushQueues<F> {
/// assert_eq!(frame, 2);
/// }
/// ```
#[expect(
clippy::integer_division_remainder_used,
reason = "tokio::select! expands to modulus internally"
)]
pub async fn recv(&mut self) -> Option<(PushPriority, F)> {
let mut high_closed = false;
let mut low_closed = false;
Expand Down
2 changes: 0 additions & 2 deletions src/request/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ pub const DEFAULT_BODY_CHANNEL_CAPACITY: usize = 16;
/// // tx: send chunks from connection
/// // rx: consume in handler
/// ```
#[must_use]
pub fn body_channel(
capacity: usize,
) -> (mpsc::Sender<Result<Bytes, io::Error>>, RequestBodyStream) {
Expand Down Expand Up @@ -178,7 +177,6 @@ impl RequestBodyReader {
/// **Safe to call when:** the reader has been fully drained (all reads
/// returned zero bytes or an error), or when deliberately abandoning
/// any remaining buffered content.
#[must_use]
pub fn into_inner(self) -> RequestBodyStream { self.inner.into_inner() }
}
impl AsyncRead for RequestBodyReader {
Expand Down
2 changes: 1 addition & 1 deletion src/request/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,5 @@ fn inherit_correlation_variants(
#[rstest]
fn empty_metadata_is_valid() {
let parts = RequestParts::new(1, None, vec![]);
assert!(parts.metadata().is_empty());
assert_eq!(parts.metadata(), []);
}
Loading
Loading