From 1ff22167bcb5e00af14f79e6ea1056e04056ce37 Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Fri, 17 Apr 2026 18:27:03 +0500 Subject: [PATCH 1/4] add new ring index --- aether/all.h | 2 +- aether/types/ring_buffer.h | 183 ------------------------ aether/types/ring_index.h | 193 ++++++++++++++++++++++++++ tests/test-types/CMakeLists.txt | 2 +- tests/test-types/main.cpp | 4 +- tests/test-types/test-ring-buffer.cpp | 105 -------------- tests/test-types/test-ring-index.cpp | 124 +++++++++++++++++ 7 files changed, 321 insertions(+), 292 deletions(-) delete mode 100644 aether/types/ring_buffer.h create mode 100644 aether/types/ring_index.h delete mode 100644 tests/test-types/test-ring-buffer.cpp create mode 100644 tests/test-types/test-ring-index.cpp diff --git a/aether/all.h b/aether/all.h index 5c4a7362..9a90227d 100644 --- a/aether/all.h +++ b/aether/all.h @@ -61,8 +61,8 @@ #include "aether/types/static_map.h" #include "aether/types/server_id.h" #include "aether/types/client_id.h" +#include "aether/types/ring_index.h" #include "aether/types/data_buffer.h" -#include "aether/types/ring_buffer.h" #include "aether/types/client_config.h" #include "aether/types/server_config.h" #include "aether/types/state_machine.h" diff --git a/aether/types/ring_buffer.h b/aether/types/ring_buffer.h deleted file mode 100644 index 120dd6fe..00000000 --- a/aether/types/ring_buffer.h +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2024 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AETHER_TYPES_RING_BUFFER_H_ -#define AETHER_TYPES_RING_BUFFER_H_ - -#include -#include -#include - -#include "aether/format/format.h" - -namespace ae { - -/** - * \brief Struct to apply addition and subtraction operations to ring buffer - * index. - * Value is always in range [0, Max] and value overflow leads to - * start from zero - * The window size is set as Max/2 - 1. - */ -template ::max(), - AE_REQUIRERS((std::is_integral))> -struct RingIndex { - using type = T; - static constexpr T max = Max; - static constexpr T window_size = (Max / 2) - 1; - - constexpr RingIndex() : value_{Mod(0)} {} - explicit constexpr RingIndex(T val) : value_{Mod(val)} {} - - constexpr void Clockwise(T val) { - val = Mod(val); - if ((Max - val + 1) < value_) { - // -1 for zero value - value_ = static_cast(val - (Max - value_)); - } else { - value_ += val; - } - } - constexpr void CounterClockwise(T val) { - val = Mod(val); - if (value_ < val) { - value_ = static_cast(Max - (val - value_) + 1); - } else { - value_ -= val; - } - } - - constexpr T Distance(RingIndex other) const { - auto a = Max - value_; - auto b = Max - other.value_; - if (a >= b) { - return static_cast(a - b); - } - return static_cast(a + other.value_); - } - - constexpr bool IsBefore(RingIndex other) const { - return (value_ != other.value_) && (Distance(other) < window_size); - } - - constexpr bool IsAfter(RingIndex other) const { - return (value_ != other.value_) && (Distance(other) > window_size); - } - - constexpr bool operator==(RingIndex other) const { - return value_ == other.value_; - } - - constexpr bool operator!=(RingIndex other) const { - return value_ != other.value_; - } - - constexpr RingIndex& operator+=(T val) { - if constexpr (std::is_signed_v) { - if (val < 0) { - CounterClockwise(std::abs(val)); - return *this; - } - } - if (val > 0) { - Clockwise(val); - } - return *this; - } - - constexpr RingIndex& operator-=(T val) { - if constexpr (std::is_signed_v) { - if (val < 0) { - Clockwise(std::abs(val)); - return *this; - } - } - if (val > 0) { - CounterClockwise(val); - } - return *this; - } - - friend constexpr RingIndex operator+(RingIndex index, T val) { - if constexpr (std::is_signed_v) { - if (val < 0) { - index.CounterClockwise(std::abs(val)); - return index; - } - } - if (val > 0) { - index.Clockwise(val); - } - return index; - } - - friend constexpr RingIndex operator-(RingIndex index, T val) { - if constexpr (std::is_signed_v) { - if (val < 0) { - index.Clockwise(std::abs(val)); - return index; - } - } - if (val > 0) { - index.CounterClockwise(val); - } - return index; - } - - constexpr RingIndex& operator++() { - Clockwise(1); - return *this; - } - constexpr RingIndex operator++(int) { - RingIndex tmp = *this; - Clockwise(1); - return tmp; - } - constexpr RingIndex& operator--() { - CounterClockwise(1); - return *this; - } - constexpr RingIndex operator--(int) { - RingIndex tmp = *this; - CounterClockwise(1); - return tmp; - } - - explicit constexpr operator T() const { return value_; } - - private: - static constexpr T Mod(T val) { - if constexpr (Max != std::numeric_limits::max()) { - return val % (Max + 1); - } else { - return val; - } - } - - T value_; -}; - -template -struct Formatter> : Formatter { - template - void Format(RingIndex const& index, FormatContext& ctx) const { - Formatter::Format(static_cast(index), ctx); - } -}; - -} // namespace ae - -#endif // AETHER_TYPES_RING_BUFFER_H_ diff --git a/aether/types/ring_index.h b/aether/types/ring_index.h new file mode 100644 index 00000000..d1d8bb2b --- /dev/null +++ b/aether/types/ring_index.h @@ -0,0 +1,193 @@ +/* + * Copyright 2024 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_TYPES_RING_INDEX_H_ +#define AETHER_TYPES_RING_INDEX_H_ + +#include +#include + +#include "aether/format/format.h" + +namespace ae { +template +struct IndexComparable; + +/** + * \brief Struct to apply addition and subtraction operations to ring buffer + * index. + * Value is always in range [0, Max] and value overflow leads to + * start from zero + */ +template ::max() / 2) - 1> + requires(Max < std::numeric_limits::max() / 2) +struct RingIndex { + friend struct IndexComparable; + + using type = std::size_t; + static constexpr type max = Max; + + constexpr RingIndex() noexcept : value_{} {} + explicit constexpr RingIndex(std::size_t val) noexcept : value_{val % max} {} + + constexpr std::size_t Distance(RingIndex other) const noexcept { + auto a = max - value_; + auto b = max - other.value_; + if (a >= b) { + return a - b; + } + return a + other.value_; + } + + friend constexpr std::size_t Distance(RingIndex one, + RingIndex another) noexcept { + return one.Distance(another); + } + + constexpr bool operator==(RingIndex other) const noexcept { + return value_ == other.value_; + } + constexpr bool operator!=(RingIndex other) const noexcept { + return value_ != other.value_; + } + + constexpr RingIndex& operator+=(std::size_t val) noexcept { + value_ = (value_ + val) % max; + return *this; + } + + constexpr RingIndex& operator-=(std::size_t val) noexcept { + value_ = (val > value_) ? (max - val + value_) : (value_ - val); + value_ = value_ % max; + return *this; + } + + friend constexpr RingIndex operator+(RingIndex index, + std::size_t val) noexcept { + index += val; + return index; + } + + friend constexpr RingIndex operator-(RingIndex index, + std::size_t val) noexcept { + index -= val; + return index; + } + + constexpr RingIndex& operator++() noexcept { + value_ = (value_ + 1) % max; + return *this; + } + constexpr RingIndex operator++(int) noexcept { + RingIndex tmp = *this; + value_ = (value_ + 1) % max; + return tmp; + } + constexpr RingIndex& operator--() noexcept { + value_ = (value_ - 1) % max; + return *this; + } + constexpr RingIndex operator--(int) noexcept { + RingIndex tmp = *this; + value_ = (value_ - 1) % max; + return tmp; + } + + explicit constexpr operator std::size_t() const { return value_; } + + template + requires(std::is_integral_v) + explicit constexpr operator T() const { + return static_cast(value_); + } + + private: + std::size_t value_; +}; + +template +struct Formatter> : Formatter { + template + void Format(RingIndex const& index, FormatContext& ctx) const { + Formatter::Format(static_cast(index), ctx); + } +}; + +template +struct IndexComparable { + constexpr bool operator>(Index other) const noexcept { + return this->IsAfter(other); + } + constexpr bool operator>=(Index other) const noexcept { + return (value == other) || this->IsAfter(other); + } + constexpr bool operator<(Index other) const noexcept { + return this->IsBefore(other); + } + constexpr bool operator<=(Index other) const noexcept { + return (value == other) || this->IsBefore(other); + } + + constexpr bool IsBefore(Index other) const noexcept { + return (value != other) && (begin.Distance(value) < begin.Distance(other)); + } + + constexpr bool IsAfter(Index other) const noexcept { + return (value != other) && (begin.Distance(value) > begin.Distance(other)); + } + + Index value; + Index begin; +}; + +template +struct RingIndexRange { + // index is in [left:right] range + constexpr bool InRange(Index index, Index begin) const noexcept { + return (IndexComparable{left, begin} <= index) && + (IndexComparable{right, begin} >= index); + } + // index range is before index ( right < index) + constexpr bool IsBefore(Index index, Index begin) const { + return IndexComparable{right, begin} < index; + } + // index range is after index ( left > index) + constexpr bool IsAfter(Index index, Index begin) const { + return IndexComparable{left, begin} > index; + } + // index range is flipped ( left > right) + constexpr bool IsFlipped(Index begin) const { + return IndexComparable{left, begin} > right; + } + // index range is empty ( left == right) + constexpr bool IsEmpty() const { return left == right; } + + constexpr auto distance() const { return Distance(left, right); } + + constexpr bool operator==(RingIndexRange const& other) const { + return (left == other.left) && (right == other.right); + } + constexpr bool operator!=(RingIndexRange const& other) const { + return !(*this == other); + } + + Index left; + Index right; +}; + +} // namespace ae + +#endif // AETHER_TYPES_RING_INDEX_H_ diff --git a/tests/test-types/CMakeLists.txt b/tests/test-types/CMakeLists.txt index ca2f707f..7026a831 100644 --- a/tests/test-types/CMakeLists.txt +++ b/tests/test-types/CMakeLists.txt @@ -17,7 +17,7 @@ cmake_minimum_required( VERSION 3.16 ) list(APPEND test_srcs main.cpp test-literal-array.cpp - test-ring-buffer.cpp + test-ring-index.cpp test-concat-arrays.cpp test-span.cpp test-static-map.cpp diff --git a/tests/test-types/main.cpp b/tests/test-types/main.cpp index 9cc884fb..f53a97d8 100644 --- a/tests/test-types/main.cpp +++ b/tests/test-types/main.cpp @@ -20,7 +20,7 @@ void setUp() {} void tearDown() {} extern int test_literal_array(); -extern int test_ring_buffer(); +extern int test_ring_index(); extern int test_concat_arrays(); extern int test_span(); extern int test_static_map(); @@ -34,7 +34,7 @@ extern int test_address_parser(); int main() { int res = 0; res += test_literal_array(); - res += test_ring_buffer(); + res += test_ring_index(); res += test_concat_arrays(); res += test_span(); res += test_static_map(); diff --git a/tests/test-types/test-ring-buffer.cpp b/tests/test-types/test-ring-buffer.cpp deleted file mode 100644 index 7278d393..00000000 --- a/tests/test-types/test-ring-buffer.cpp +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2024 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include - -#include - -#include "aether/types/ring_buffer.h" - -namespace ae::test_ring_buffer { - -void test_RingBufferShifting() { - using U8RI = RingIndex; - - auto b1 = U8RI{0}; - - auto b2 = b1 + 1; - TEST_ASSERT_EQUAL(1, static_cast(b2)); - - auto b3 = b1 - 1; - TEST_ASSERT_EQUAL(10, static_cast(b3)); - - auto b4 = b1 + 10; - TEST_ASSERT_EQUAL(10, static_cast(b4)); - - auto b5 = b1 + 9; - TEST_ASSERT_EQUAL(9, static_cast(b5)); - - auto b6 = b1 + 11; - TEST_ASSERT_EQUAL(0, static_cast(b6)); - - auto b7 = b1 - 10; - TEST_ASSERT_EQUAL(1, static_cast(b7)); - - auto b8 = b1 - 8; - TEST_ASSERT_EQUAL(3, static_cast(b8)); - - using U8RI_MAX = RingIndex; - auto a1 = U8RI_MAX{}; - auto a2 = a1 + 1; - TEST_ASSERT_EQUAL(1, static_cast(a2)); - auto a3 = a1 + 255; - TEST_ASSERT_EQUAL(255, static_cast(a3)); - auto a3_1 = a3 + 1; - TEST_ASSERT_EQUAL(0, static_cast(a3_1)); - auto a4 = a1 - 200; - TEST_ASSERT_EQUAL(56, static_cast(a4)); -} - -void test_RingBufferDistance() { - using U8RI = RingIndex; - auto b1 = U8RI{0}; - - auto d1 = b1.Distance(b1 + 1); - TEST_ASSERT_EQUAL(1, d1); - - auto d2 = b1.Distance(b1 + 9); - TEST_ASSERT_EQUAL(9, d2); - - auto d3 = b1.Distance(b1 + 10); - TEST_ASSERT_EQUAL(10, d3); - - auto b2 = U8RI{9}; - auto d4 = b2.Distance(b2 + 5); - TEST_ASSERT_EQUAL(5, d4); -} - -void test_RingBufferCompare() { - using U8RI = RingIndex; - constexpr auto begin = U8RI{0}; - auto a = U8RI{10}; - auto b = U8RI{15}; - - TEST_ASSERT_TRUE(a.IsBefore(b)); - TEST_ASSERT_TRUE(b.IsAfter(a)); - TEST_ASSERT_FALSE(a.IsAfter(b)); - TEST_ASSERT_FALSE(b.IsBefore(a)); - TEST_ASSERT_TRUE(a == a); - TEST_ASSERT_TRUE(a != b); - TEST_ASSERT_FALSE(a == b); - TEST_ASSERT_FALSE(a != a); -} - -} // namespace ae::test_ring_buffer - -int test_ring_buffer() { - UNITY_BEGIN(); - RUN_TEST(ae::test_ring_buffer::test_RingBufferShifting); - RUN_TEST(ae::test_ring_buffer::test_RingBufferDistance); - RUN_TEST(ae::test_ring_buffer::test_RingBufferCompare); - return UNITY_END(); -} diff --git a/tests/test-types/test-ring-index.cpp b/tests/test-types/test-ring-index.cpp new file mode 100644 index 00000000..89069981 --- /dev/null +++ b/tests/test-types/test-ring-index.cpp @@ -0,0 +1,124 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "aether/types/ring_index.h" + +namespace ae::test_ring_index { +void test_RingIndexShifting() { + using Index = RingIndex<10>; + + auto b1 = Index{0}; + + auto b2 = b1 + 1; + TEST_ASSERT_EQUAL(1, static_cast(b2)); + + auto b3 = b1 - 1; + TEST_ASSERT_EQUAL(9, static_cast(b3)); + + auto b4 = b1 + 10; + TEST_ASSERT_EQUAL(0, static_cast(b4)); + + auto b5 = b1 + 9; + TEST_ASSERT_EQUAL(9, static_cast(b5)); + + auto b6 = b1 + 11; + TEST_ASSERT_EQUAL(1, static_cast(b6)); + + auto b7 = b1 - 10; + TEST_ASSERT_EQUAL(0, static_cast(b7)); + + auto b8 = b1 - 8; + TEST_ASSERT_EQUAL(2, static_cast(b8)); + + auto b9 = Index{6} + 5; + TEST_ASSERT_EQUAL(1, static_cast(b9)); + + using Index_Max = RingIndex::max()>; + auto a1 = Index_Max{0}; + auto a2 = a1 + 1; + TEST_ASSERT_EQUAL(1, static_cast(a2)); + auto a3 = a1 + 255; + TEST_ASSERT_EQUAL(0, static_cast(a3)); + auto a3_1 = a3 + 1; + TEST_ASSERT_EQUAL(1, static_cast(a3_1)); + auto a4 = a1 - 200; + TEST_ASSERT_EQUAL(55, static_cast(a4)); +} + +void test_RingIndexDistance() { + using Index = RingIndex<10>; + auto b1 = Index{0}; + + auto d1 = b1.Distance(b1 + 1); + TEST_ASSERT_EQUAL(1, d1); + + auto d2 = b1.Distance(b1 + 9); + TEST_ASSERT_EQUAL(9, d2); + + auto d3 = b1.Distance(b1 + 10); + TEST_ASSERT_EQUAL(0, d3); + + auto b2 = Index{9}; + auto d4 = b2.Distance(b2 + 5); + TEST_ASSERT_EQUAL(5, d4); +} + +void test_RingIndexCompare() { + using Index = RingIndex<50>; + constexpr auto begin = Index{0}; + auto a = Index{10}; + auto b = Index{15}; + + TEST_ASSERT_TRUE((IndexComparable{a, begin} < b)); + TEST_ASSERT_TRUE((IndexComparable{b, begin} > a)); + TEST_ASSERT_FALSE((IndexComparable{a, begin} > b)); + TEST_ASSERT_FALSE((IndexComparable{b, begin} < a)); + TEST_ASSERT_TRUE(a == a); + TEST_ASSERT_TRUE(a != b); + TEST_ASSERT_FALSE(a == b); + TEST_ASSERT_FALSE(a != a); +} + +void test_RangeIndex() { + using Index = RingIndex<50>; + using IndexRange = RingIndexRange; + + constexpr auto begin = Index{0}; + + auto a = IndexRange{Index{0}, Index{10}}; + TEST_ASSERT_TRUE(a.InRange(Index{0}, begin)); + TEST_ASSERT_TRUE(a.InRange(Index{10}, begin)); + TEST_ASSERT_TRUE(a.InRange(Index{5}, begin)); + TEST_ASSERT_TRUE(a.InRange(Index{4}, begin)); + TEST_ASSERT_FALSE(a.InRange(Index{11}, begin)); + TEST_ASSERT_TRUE(a.IsBefore(Index{11}, begin)); + TEST_ASSERT_FALSE(a.IsAfter(Index{11}, begin)); + TEST_ASSERT_TRUE(a.IsAfter(Index{56}, Index{55})); + TEST_ASSERT_FALSE(a.IsBefore(Index{56}, Index{55})); +} + +} // namespace ae::test_ring_index + +int test_ring_index() { + UNITY_BEGIN(); + RUN_TEST(ae::test_ring_index::test_RingIndexShifting); + RUN_TEST(ae::test_ring_index::test_RingIndexDistance); + RUN_TEST(ae::test_ring_index::test_RingIndexCompare); + RUN_TEST(ae::test_ring_index::test_RangeIndex); + return UNITY_END(); +} From 86eb5edd3c277c6af804cd256bc10daa742d8721 Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Thu, 23 Apr 2026 18:17:14 +0500 Subject: [PATCH 2/4] add timer --- aether/actions/timer.h | 64 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 aether/actions/timer.h diff --git a/aether/actions/timer.h b/aether/actions/timer.h new file mode 100644 index 00000000..c5aadc0b --- /dev/null +++ b/aether/actions/timer.h @@ -0,0 +1,64 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_ACTIONS_TIMER_H_ +#define AETHER_ACTIONS_TIMER_H_ + +#include +#include +#include + +#include "aether/clock.h" +#include "aether/ae_context.h" +#include "aether/actions/action2_.h" +#include "aether/types/small_function.h" + +namespace ae { +class Timer final : public a2::Action { + public: + using TimerFunc = SmallFunction; + + template + requires(std::is_same_v || std::is_same_v) + Timer(AeContext const& ae_context, F&& func, Time timeout) + : ae_context_{ae_context}, + alive_ctx_{std::in_place, ae_context_, this}, + func_{std::forward(func)} { + // schedule timer + ae_context_.scheduler().DelayedTask( + [imalive{alive_ctx_->View()}]() { + if (imalive) { + imalive->Invoke(); + } + }, + timeout); + } + + void Reset() { alive_ctx_.reset(); } + + private: + void Invoke() { + func_(); + Finish(); + } + + AeContext ae_context_; + std::optional> alive_ctx_; + TimerFunc func_; +}; +} // namespace ae + +#endif // AETHER_ACTIONS_TIMER_H_ From 04886fe1953a91254e5aec8a9816f9a43fa52697 Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Thu, 23 Apr 2026 14:36:24 +0500 Subject: [PATCH 3/4] update safe stream to use new async logic --- aether/CMakeLists.txt | 9 - aether/all.h | 1 - .../p2p_safe_message_stream.cpp | 17 +- .../client_messages/p2p_safe_message_stream.h | 9 +- aether/safe_stream/details/circular_buffer.h | 168 +++++++ .../details/receiving_chunk_list.h | 142 ++++++ aether/safe_stream/details/safe_stream_api.h | 68 +++ .../details/safe_stream_data_message.h | 36 ++ .../details/safe_stream_recv_action.h | 263 +++++++++++ .../details/safe_stream_send_action.h | 413 ++++++++++++++++++ .../safe_stream/details/sending_chunk_list.h | 91 ++++ aether/safe_stream/safe_stream.h | 239 ++++++++++ .../safe_stream/safe_stream_config.h | 20 +- aether/stream_api/safe_stream.cpp | 148 ------- aether/stream_api/safe_stream.h | 90 ---- .../safe_stream/receiving_chunk_list.cpp | 213 --------- .../safe_stream/receiving_chunk_list.h | 65 --- .../safe_stream/safe_stream_api.cpp | 43 -- .../stream_api/safe_stream/safe_stream_api.h | 60 --- .../safe_stream/safe_stream_recv_action.cpp | 182 -------- .../safe_stream/safe_stream_recv_action.h | 84 ---- .../safe_stream/safe_stream_send_action.cpp | 221 ---------- .../safe_stream/safe_stream_send_action.h | 90 ---- .../safe_stream/safe_stream_types.h | 186 -------- .../safe_stream/send_data_buffer.cpp | 162 ------- .../stream_api/safe_stream/send_data_buffer.h | 81 ---- .../safe_stream/sending_chunk_list.cpp | 101 ----- .../safe_stream/sending_chunk_list.h | 54 --- .../safe_stream/sending_data_action.cpp | 100 ----- .../safe_stream/sending_data_action.h | 80 ---- examples/benches/send_message_delays/main.cpp | 1 - .../benches/send_message_delays/receiver.h | 2 +- examples/benches/send_message_delays/sender.h | 1 - examples/cloud/cloud_test.cpp | 1 - 34 files changed, 1446 insertions(+), 1995 deletions(-) create mode 100644 aether/safe_stream/details/circular_buffer.h create mode 100644 aether/safe_stream/details/receiving_chunk_list.h create mode 100644 aether/safe_stream/details/safe_stream_api.h create mode 100644 aether/safe_stream/details/safe_stream_data_message.h create mode 100644 aether/safe_stream/details/safe_stream_recv_action.h create mode 100644 aether/safe_stream/details/safe_stream_send_action.h create mode 100644 aether/safe_stream/details/sending_chunk_list.h create mode 100644 aether/safe_stream/safe_stream.h rename aether/{stream_api => }/safe_stream/safe_stream_config.h (52%) delete mode 100644 aether/stream_api/safe_stream.cpp delete mode 100644 aether/stream_api/safe_stream.h delete mode 100644 aether/stream_api/safe_stream/receiving_chunk_list.cpp delete mode 100644 aether/stream_api/safe_stream/receiving_chunk_list.h delete mode 100644 aether/stream_api/safe_stream/safe_stream_api.cpp delete mode 100644 aether/stream_api/safe_stream/safe_stream_api.h delete mode 100644 aether/stream_api/safe_stream/safe_stream_recv_action.cpp delete mode 100644 aether/stream_api/safe_stream/safe_stream_recv_action.h delete mode 100644 aether/stream_api/safe_stream/safe_stream_send_action.cpp delete mode 100644 aether/stream_api/safe_stream/safe_stream_send_action.h delete mode 100644 aether/stream_api/safe_stream/safe_stream_types.h delete mode 100644 aether/stream_api/safe_stream/send_data_buffer.cpp delete mode 100644 aether/stream_api/safe_stream/send_data_buffer.h delete mode 100644 aether/stream_api/safe_stream/sending_chunk_list.cpp delete mode 100644 aether/stream_api/safe_stream/sending_chunk_list.h delete mode 100644 aether/stream_api/safe_stream/sending_data_action.cpp delete mode 100644 aether/stream_api/safe_stream/sending_data_action.h diff --git a/aether/CMakeLists.txt b/aether/CMakeLists.txt index 1167bc08..390e2f9d 100644 --- a/aether/CMakeLists.txt +++ b/aether/CMakeLists.txt @@ -204,15 +204,6 @@ list(APPEND transport_srcs list(APPEND stream_api_src "stream_api/sized_packet_gate.cpp" "stream_api/stream_api.cpp" - - "stream_api/safe_stream.cpp" - "stream_api/safe_stream/safe_stream_api.cpp" - "stream_api/safe_stream/sending_data_action.cpp" - "stream_api/safe_stream/send_data_buffer.cpp" - "stream_api/safe_stream/sending_chunk_list.cpp" - "stream_api/safe_stream/receiving_chunk_list.cpp" - "stream_api/safe_stream/safe_stream_send_action.cpp" - "stream_api/safe_stream/safe_stream_recv_action.cpp" ) list(APPEND write_action_srcs diff --git a/aether/all.h b/aether/all.h index 9a90227d..81ebba94 100644 --- a/aether/all.h +++ b/aether/all.h @@ -71,7 +71,6 @@ #include "aether/types/address_parser.h" #include "aether/stream_api/istream.h" -#include "aether/stream_api/safe_stream.h" #include "aether/stream_api/api_call_adapter.h" #include "aether/write_action/write_action.h" #include "aether/write_action/buffer_write.h" diff --git a/aether/client_messages/p2p_safe_message_stream.cpp b/aether/client_messages/p2p_safe_message_stream.cpp index 7e8dc4d1..6923306a 100644 --- a/aether/client_messages/p2p_safe_message_stream.cpp +++ b/aether/client_messages/p2p_safe_message_stream.cpp @@ -19,28 +19,31 @@ #include #include "aether/stream_api/tied_gates.h" +#include "aether/safe_stream/safe_stream.h" namespace ae { P2pSafeStream::P2pSafeStream(AeContext const& ae_context, SafeStreamConfig const& config, RcPtr p2p_stream) : sized_packet_gate_{}, - safe_stream_{ae_context, config}, + safe_stream_{std::make_unique>(ae_context, config)}, p2p_stream_{std::move(p2p_stream)}, out_data_sub_{TiedEventOutData( [this](auto const& data) { out_data_event_.Emit(data); }, - sized_packet_gate_, safe_stream_)} { - Tie(safe_stream_, *p2p_stream_); + sized_packet_gate_, *safe_stream_)} { + Tie(*safe_stream_, *p2p_stream_); } +P2pSafeStream::~P2pSafeStream() = default; + WriteAction& P2pSafeStream::Write(DataBuffer&& data) { auto sized_data = sized_packet_gate_.WriteIn(std::move(data)); - return safe_stream_.Write(std::move(sized_data)); + return safe_stream_->Write(std::move(sized_data)); } StreamInfo P2pSafeStream::stream_info() const { auto overhead = sized_packet_gate_.Overhead(); - auto info = safe_stream_.stream_info(); + auto info = safe_stream_->stream_info(); if (info.max_element_size < overhead) { info.max_element_size = 0; } else { @@ -51,13 +54,13 @@ StreamInfo P2pSafeStream::stream_info() const { P2pSafeStream::StreamUpdateEvent::Subscriber P2pSafeStream::stream_update_event() { - return safe_stream_.stream_update_event(); + return safe_stream_->stream_update_event(); } P2pSafeStream::OutDataEvent::Subscriber P2pSafeStream::out_data_event() { return EventSubscriber{out_data_event_}; } -void P2pSafeStream::Restream() { safe_stream_.Restream(); } +void P2pSafeStream::Restream() { safe_stream_->Restream(); } } // namespace ae diff --git a/aether/client_messages/p2p_safe_message_stream.h b/aether/client_messages/p2p_safe_message_stream.h index 91fc417f..34a40f13 100644 --- a/aether/client_messages/p2p_safe_message_stream.h +++ b/aether/client_messages/p2p_safe_message_stream.h @@ -22,18 +22,20 @@ #include "aether/actions/action_context.h" #include "aether/stream_api/istream.h" -#include "aether/stream_api/safe_stream.h" #include "aether/stream_api/sized_packet_gate.h" -#include "aether/stream_api/safe_stream/safe_stream_config.h" +#include "aether/safe_stream/safe_stream_config.h" #include "aether/client_messages/p2p_message_stream.h" namespace ae { +template +class SafeStream; class P2pSafeStream final : public ByteIStream { public: P2pSafeStream(AeContext const& ae_context, SafeStreamConfig const& config, RcPtr p2p_stream); + ~P2pSafeStream() override; AE_CLASS_NO_COPY_MOVE(P2pSafeStream) @@ -45,7 +47,8 @@ class P2pSafeStream final : public ByteIStream { private: SizedPacketGate sized_packet_gate_; - SafeStream safe_stream_; + // TODO: add config + std::unique_ptr> safe_stream_; RcPtr p2p_stream_; OutDataEvent out_data_event_; std::array out_data_sub_; diff --git a/aether/safe_stream/details/circular_buffer.h b/aether/safe_stream/details/circular_buffer.h new file mode 100644 index 00000000..cc27deb6 --- /dev/null +++ b/aether/safe_stream/details/circular_buffer.h @@ -0,0 +1,168 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_SAFE_STREAM_DETAILS_CIRCULAR_BUFFER_H_ +#define AETHER_SAFE_STREAM_DETAILS_CIRCULAR_BUFFER_H_ + +#include +#include +#include +#include + +#include "aether/types/result.h" +#include "aether/types/ring_index.h" + +namespace ae { +enum class CircularBufferError : unsigned char { + kDataOverflow, + kEmptyBuffer, + kIndexOutOfRange, +}; + +/** + * \brief Circular buffer to work with sending and receiving buffers + */ +template +class CircularBuffer { + public: + using value_type = std::uint8_t; + static constexpr std::size_t kCapacity = Capacity; + using index_type = RingIndex; + using index_range_type = RingIndexRange; + + struct DSpan { + constexpr std::size_t size() const noexcept { + return first.size() + second.size(); + } + + std::span first; + std::span second; + }; + + constexpr CircularBuffer() noexcept = default; + explicit constexpr CircularBuffer(std::size_t start_offset) noexcept + : begin_{index_type{start_offset}}, end_{begin_} {} + + /** + * \brief Push new data and return its range + */ + constexpr Result Push( + std::span data) noexcept { + return Insert(end_, data); + } + + /** + * \brief Insert data at the given position + * \param pos - the position to insert at must be after begin_ + * \param data - the data to insert + */ + constexpr Result Insert( + index_type pos, std::span data) noexcept { + auto available = (begin_ == pos) ? kCapacity : Distance(pos, begin_); + if (data.size() > available) { + return Error{CircularBufferError::kDataOverflow}; + } + auto start = pos; + // copy the data into array + // expect for 2 copies depends on does data overlaps buffer or not + while (!data.empty()) { + auto remaining = buffer_.size() - static_cast(pos); + auto copy_size = data.size() > remaining ? remaining : data.size(); + std::copy_n(std::begin(data), copy_size, + buffer_.data() + static_cast(pos)); + data = data.subspan(copy_size); + pos += copy_size; + } + // also move end_ to the right + if (IndexComparable{pos, begin_} > end_) { + end_ = pos; + } + // return the range of the data that was pushed + return Ok{index_range_type{.left = start, .right = pos - 1}}; + } + + /** + * \brief Read the data with size from start + * \param start - the position to read from must be in range begin_..end_ + */ + constexpr Result Read( + index_type start, std::size_t size) const noexcept { + if ((begin_ == end_) || Distance(start, end_) == 0) { + return Error{CircularBufferError::kEmptyBuffer}; + } + if (Distance(start, end_) > Distance(begin_, end_)) { + return Error{CircularBufferError::kIndexOutOfRange}; + } + + DSpan res; + + // size may be bigger than the actual available data, so we clamp it + auto max_distance = Distance(start, end_); + size = size > max_distance ? max_distance : size; + + // handle wrapping around the buffer + std::size_t first_max_size = + buffer_.size() - static_cast(start); + std::size_t first_size = first_max_size > size ? size : first_max_size; + res.first = std::span( + buffer_.data() + static_cast(start), first_size); + if (first_size < size) { + res.second = + std::span(buffer_.data(), size - first_size); + } + return Ok{res}; + } + /** + * \brief Erase data range + * \param to - the position to erase to must be in range begin_..end_ + */ + constexpr void Erase(index_type to) noexcept { + if (Distance(to, end_) > Distance(begin_, end_)) { + assert(false && "Erase out of bounds"); + return; + } + begin_ = to; + } + /** + * \brief Erase data range from the specified position + * \param from - the position to erase from must be in range begin_..end_ + */ + constexpr void EraseFrom(index_type from) noexcept { + if (Distance(from, end_) > Distance(begin_, end_)) { + assert(false && "Erase out of bounds"); + return; + } + end_ = from; + } + + constexpr void Reset(std::size_t offset = 0) noexcept { + begin_ = end_ = index_type{offset}; + } + + constexpr std::size_t size() const noexcept { return Distance(begin_, end_); } + constexpr index_type begin() const noexcept { return begin_; } + constexpr index_type end() const noexcept { return end_; } + + private: + index_type begin_{0}; + index_type end_{0}; + + std::array buffer_; +}; + +} // namespace ae + +#endif // AETHER_SAFE_STREAM_DETAILS_CIRCULAR_BUFFER_H_ diff --git a/aether/safe_stream/details/receiving_chunk_list.h b/aether/safe_stream/details/receiving_chunk_list.h new file mode 100644 index 00000000..55e8cbe4 --- /dev/null +++ b/aether/safe_stream/details/receiving_chunk_list.h @@ -0,0 +1,142 @@ +/* + * Copyright 2025 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_SAFE_STREAM_DETAILS_RECEIVING_CHUNK_LIST_H_ +#define AETHER_SAFE_STREAM_DETAILS_RECEIVING_CHUNK_LIST_H_ + +#include +#include +#include + +#include "aether/types/ring_index.h" + +namespace ae { +enum class ChunkAddResult : std::uint8_t { + kInvalid, + kDuplicate, + kAdded, + kAddRepeated, +}; + +template +class ReceiveChunkList { + public: + using IndexType = Index; + using IndexRangeType = RingIndexRange; + + struct Chunk { + IndexRangeType range; + std::uint8_t repeat_count; + }; + + struct Comparator { + using is_transparent = std::true_type; + + bool operator()(IndexType a, IndexType b) const { + return IndexComparable{a, self->buffer_begin_} < b; + } + + ReceiveChunkList* self; + }; + + explicit ReceiveChunkList(IndexType buffer_begin) noexcept + : buffer_begin_{buffer_begin}, chunks_{Comparator{.self = this}} {} + + void set_buffer_begin(IndexType buffer_begin) { + buffer_begin_ = buffer_begin; + } + + ChunkAddResult AddChunk(IndexRangeType range, std::uint8_t repeat_count) { + auto [it, inserted] = + chunks_.emplace(range.left, Chunk{range, repeat_count}); + if (!inserted && (it->second.range == range)) { + if (it->second.repeat_count >= repeat_count) { + return ChunkAddResult::kDuplicate; + } + it->second.repeat_count = repeat_count; + return ChunkAddResult::kAddRepeated; + } + // add range in sorted order + auto pos = std::lower_bound(std::begin(ranges_), std::end(ranges_), range, + [&](auto const& a, auto const& b) { + return IndexComparable{ + a.left, buffer_begin_} < b.left; + }); + ranges_.insert(pos, range); + return ChunkAddResult::kAdded; + } + + IndexRangeType ReceiveChunk() const { + IndexRangeType range; + range.left = buffer_begin_; + range.right = buffer_begin_; + auto expected_index = range.left; + + for (auto const& r : ranges_) { + // if there is a gap, break + if (IndexComparable{r.left, buffer_begin_} > expected_index) { + break; + } + // if c.right is after range.right, update range.right + if (IndexComparable{r.right, buffer_begin_} > range.right) { + range.right = r.right; + } + expected_index = range.right + 1; + } + return range; + } + + void Acknowledge(IndexType to) { + // remove chunks that are before or equal to `to` + std::erase_if(chunks_, [&](auto const& c) { + return IndexComparable{c.first, buffer_begin_} <= to; + }); + // remove ranges that are before or equal to `to` + std::erase_if(ranges_, [&](auto& r) { + if (IndexComparable{r.right, buffer_begin_} <= to) { + return true; + } + // if range overlaps with `to`, shift left boundary + if (IndexComparable{r.left, buffer_begin_} <= to) { + r.left = to + 1; + return false; + } + return false; + }); + } + + std::optional FindMissedChunk() const { + auto expected = buffer_begin_; + for (auto const& r : ranges_) { + if (IndexComparable{r.left, buffer_begin_} > expected) { + IndexRangeType missed{.left = expected, .right = r.left - 1}; + return missed; + } + expected = r.right + 1; + } + return std::nullopt; + } + + bool empty() const { return chunks_.empty(); } + + private: + IndexType buffer_begin_{}; + std::map chunks_; + std::vector ranges_; +}; +} // namespace ae + +#endif // AETHER_SAFE_STREAM_DETAILS_RECEIVING_CHUNK_LIST_H_ diff --git a/aether/safe_stream/details/safe_stream_api.h b/aether/safe_stream/details/safe_stream_api.h new file mode 100644 index 00000000..600cf216 --- /dev/null +++ b/aether/safe_stream/details/safe_stream_api.h @@ -0,0 +1,68 @@ +/* + * Copyright 2024 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_SAFE_STREAM_DETAILS_SAFE_STREAM_API_H_ +#define AETHER_SAFE_STREAM_DETAILS_SAFE_STREAM_API_H_ + +#include + +#include "aether/types/data_buffer.h" +#include "aether/api_protocol/api_protocol.h" + +namespace ae { +class SafeStreamApi : public ApiClassImpl { + public: + explicit SafeStreamApi(ProtocolContext& protocol_context) + : ApiClassImpl{protocol_context}, + ack{protocol_context}, + request_repeat{protocol_context}, + send_reset{protocol_context}, + send{protocol_context} {} + + virtual ~SafeStreamApi() = default; + + Method<3, void(std::uint16_t index)> ack; + Method<4, void(std::uint16_t index)> request_repeat; + Method<5, void(std::uint16_t index, std::uint16_t delta_offset, + std::uint8_t repeat_count, DataBuffer data)> + send_reset; + Method<6, void(std::uint16_t index, std::uint16_t delta_offset, + std::uint8_t repeat_count, DataBuffer data)> + send; + + /** + * \brief Acknowledgment for data buffer up to index + */ + virtual void AckImpl(std::uint16_t index) = 0; + /** + * \brief Request receive data from the index + */ + virtual void RequestRepeatImpl(std::uint16_t index) = 0; + virtual void SendResetImpl(std::uint16_t begin_offset, + std::uint16_t delta_offset, + std::uint8_t repeat_count, DataBuffer data) = 0; + virtual void SendImpl(std::uint16_t begin_offset, std::uint16_t delta_offset, + std::uint8_t repeat_count, DataBuffer data) = 0; + + AE_METHODS(RegMethod<3, &SafeStreamApi::AckImpl>, + RegMethod<4, &SafeStreamApi::RequestRepeatImpl>, + RegMethod<5, &SafeStreamApi::SendResetImpl>, + RegMethod<6, &SafeStreamApi::SendImpl>); +}; + +} // namespace ae + +#endif // AETHER_SAFE_STREAM_DETAILS_SAFE_STREAM_API_H_ diff --git a/aether/safe_stream/details/safe_stream_data_message.h b/aether/safe_stream/details/safe_stream_data_message.h new file mode 100644 index 00000000..0d52ec45 --- /dev/null +++ b/aether/safe_stream/details/safe_stream_data_message.h @@ -0,0 +1,36 @@ +/* + * Copyright 2024 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_SAFE_STREAM_DETAILS_SAFE_STREAM_DATA_MESSAGE_H_ +#define AETHER_SAFE_STREAM_DETAILS_SAFE_STREAM_DATA_MESSAGE_H_ + +#include + +#include "aether/types/data_buffer.h" + +namespace ae { +/** + * \brief The data message used for sending data by Safe Stream Api. + */ +struct DataMessage { + bool reset; + std::uint8_t repeat_count; + std::uint16_t delta_offset; // data offset form sender's buffer begin + DataBuffer data; +}; +} // namespace ae + +#endif // AETHER_SAFE_STREAM_DETAILS_SAFE_STREAM_DATA_MESSAGE_H_ diff --git a/aether/safe_stream/details/safe_stream_recv_action.h b/aether/safe_stream/details/safe_stream_recv_action.h new file mode 100644 index 00000000..790451eb --- /dev/null +++ b/aether/safe_stream/details/safe_stream_recv_action.h @@ -0,0 +1,263 @@ +/* + * Copyright 2025 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_SAFE_STREAM_DETAILS_SAFE_STREAM_RECV_ACTION_H_ +#define AETHER_SAFE_STREAM_DETAILS_SAFE_STREAM_RECV_ACTION_H_ + +#include + +#include "aether/common.h" +#include "aether/ae_context.h" +#include "aether/events/events.h" +#include "aether/actions/timer.h" +#include "aether/index_registry/index_registry.h" +#include "aether/safe_stream/safe_stream_config.h" +#include "aether/safe_stream/details/circular_buffer.h" +#include "aether/safe_stream/details/receiving_chunk_list.h" +#include "aether/safe_stream/details/safe_stream_data_message.h" + +#include "aether/tele/tele.h" + +namespace ae { +class ISendAckRepeat { + public: + virtual ~ISendAckRepeat() = default; + + virtual void SendAck(std::uint16_t index) = 0; + virtual void SendRepeatRequest(std::uint16_t index) = 0; +}; + +template +class SafeStreamRecvAction { + public: + static constexpr std::size_t kCapacity = Capacity; + using CircularBufferImpl = CircularBuffer; + using IndexType = CircularBufferImpl::index_type; + using IndexRangeType = CircularBufferImpl::index_range_type; + using ReceiveChunkListImpl = ReceiveChunkList; + + using ReceiveEvent = Event; + + SafeStreamRecvAction(AeContext const& ae_context, + ISendAckRepeat& send_ack_repeat, + SafeStreamConfig const& config) + : ae_context_{ae_context}, + alive_ctx_{ae_context_, this}, + send_ack_repeat_{&send_ack_repeat}, + send_ack_timeout_{config.send_ack_timeout}, + send_repeat_timeout_{config.send_repeat_timeout}, + window_size_{config.window_size} {} + + AE_CLASS_NO_COPY_MOVE(SafeStreamRecvAction) + + void PushData(std::uint16_t index, DataMessage data_message) { + AE_TELED_DEBUG( + "Data received index: {}, delta: {}, repeat {}, reset {}, size " + "{}", + index, data_message.delta_offset, + static_cast(data_message.repeat_count), data_message.reset, + data_message.data.size()); + + // The first packet received, sync with the sender + bool reset = false; + if (!chunks_) { + AE_TELED_DEBUG("Init receiver"); + reset = true; + // The sender sent first packet, sync with the sender + } else if (data_message.reset && (session_offset != index)) { + AE_TELED_DEBUG("Reset receiver"); + reset = true; + } + if (reset) { + session_offset = index; + buffer_.Reset(static_cast(index)); + chunks_.emplace(buffer_.begin()); + } + + auto received_index = + IndexType{static_cast(index + data_message.delta_offset)}; + HandleData(received_index, data_message.repeat_count, data_message.data); + } + + ReceiveEvent::Subscriber receive_event() { + return EventSubscriber{receive_event_}; + } + + private: + void HandleData(IndexType received_index, std::uint8_t repeat_count, + DataBuffer const& data) { + auto received_range = + IndexRangeType{received_index, received_index + data.size() - 1}; + auto data_span = std::span{data.data(), data.size()}; + + if (Distance(buffer_.begin(), received_range.right) > window_size_) { + // Got old package + AE_TELED_DEBUG("Got old package"); + // We know nothing about the package, either it is repeated or duplicated + // TODO: do not send ack if the package is duplicated + EnqueueAck(); + return; + } + + // if received range partially overlaps with buffer begin, adjust range + if (Distance(buffer_.begin(), received_range.left) > window_size_) { + auto dist = Distance(received_range.left, buffer_.begin()); + // make received range start from buffer begin + received_range.left = buffer_.begin(); + // and cut the data span to match the range + data_span = data_span.subspan(dist); + } + + // TODO: add handle old acknowledged chunks + auto add_res = chunks_->AddChunk(received_range, repeat_count); + switch (add_res) { + case ChunkAddResult::kDuplicate: { + AE_TELED_DEBUG("Received duplicate, ignore!"); + break; + } + case ChunkAddResult::kAddRepeated: { + AE_TELED_DEBUG("Received repeated chunk, enqueeu ack!"); + EnqueueAck(); + break; + } + default: { + auto buffer_res = buffer_.Insert(received_range.left, data_span); + if (!buffer_res) { + AE_TELED_ERROR("Failed to add chunk: {}", buffer_res.error()); + // TODO: handle error! + } + AE_TELED_DEBUG("Received packet index: {}, size: {}, repeat_count: {}", + received_range.left, data_span.size(), + static_cast(repeat_count)) + EnqueueRecv(); + break; + } + } + } + + void EnqueueRecv() { + if (recv_enqueued_) { + return; + } + recv_enqueued_ = true; + ae_context_.scheduler().Task([imalive{alive_ctx_.View()}]() { + if (imalive) { + imalive->recv_enqueued_ = false; + imalive->HandleCompletedChains(); + } + }); + } + + void EnqueueAck() { + if (ack_timer_ && !ack_timer_->is_finished()) { + return; + } + ack_timer_.emplace( + ae_context_, [this]() { HandleAcknowledgement(); }, send_ack_timeout_); + } + + void EnqueueMissing() { + if (missing_timer_ && !missing_timer_->is_finished()) { + return; + } + missing_timer_.emplace( + ae_context_, [this]() { HandleMissing(); }, send_repeat_timeout_); + } + + void HandleCompletedChains() { + if (!chunks_) { + return; + } + if (chunks_->empty()) { + return; + } + auto recv_range = chunks_->ReceiveChunk(); + if (recv_range.IsEmpty()) { + // no continuous range to emit, enqueue missing and wait for repeat + EnqueueMissing(); + return; + } + auto res = buffer_.Read(recv_range.left, recv_range.distance() + 1); + if (res) { + auto const& dspan = res.value(); + DataBuffer data_buffer(dspan.size()); + std::copy(dspan.first.begin(), dspan.first.end(), data_buffer.begin()); + if (dspan.second.size() != 0) { + std::copy( + dspan.second.begin(), dspan.second.end(), + data_buffer.begin() + + static_cast(dspan.first.size())); + } + last_emitted_ = recv_range.right; + AE_TELED_DEBUG( + "Emitted received data range: {}-{} size: {}, last_emitted_: {}", + recv_range.left, recv_range.right, data_buffer.size(), last_emitted_); + receive_event_.Emit(std::move(data_buffer)); + + chunks_->Acknowledge(recv_range.right); + buffer_.Erase(recv_range.right + 1); + chunks_->set_buffer_begin(buffer_.begin()); + EnqueueAck(); + } else { + AE_TELED_DEBUG("Receiver chunk unsuccess {}", res.error()); + // TODO: handle errors + EnqueueMissing(); + } + } + + void HandleAcknowledgement() { + auto ack_offset = static_cast(last_emitted_); + AE_TELED_DEBUG("Send acknowledgement for offset: {}", ack_offset); + send_ack_repeat_->SendAck(static_cast(ack_offset)); + } + + void HandleMissing() { + if (!chunks_) { + return; + } + if (auto res = chunks_->FindMissedChunk(); res) { + AE_TELED_DEBUG("Send repeat request for offset range {}-{}", res->left, + res->right); + auto request_offset = static_cast(res->left); + send_ack_repeat_->SendRepeatRequest( + static_cast(request_offset)); + // enqueue again + EnqueueMissing(); + } + } + + AeContext ae_context_; + IndexCtx alive_ctx_; + ISendAckRepeat* send_ack_repeat_; + + Duration send_ack_timeout_; + Duration send_repeat_timeout_; + std::size_t window_size_; + + CircularBufferImpl buffer_; + std::optional chunks_; + std::size_t session_offset{}; //< current session start offset + IndexType last_emitted_{}; + + bool recv_enqueued_{false}; + std::optional ack_timer_; + std::optional missing_timer_; + + ReceiveEvent receive_event_; +}; +} // namespace ae + +#endif // AETHER_SAFE_STREAM_DETAILS_SAFE_STREAM_RECV_ACTION_H_ diff --git a/aether/safe_stream/details/safe_stream_send_action.h b/aether/safe_stream/details/safe_stream_send_action.h new file mode 100644 index 00000000..e0cfc266 --- /dev/null +++ b/aether/safe_stream/details/safe_stream_send_action.h @@ -0,0 +1,413 @@ +/* + * Copyright 2025 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_SAFE_STREAM_DETAILS_SAFE_STREAM_SEND_ACTION_H_ +#define AETHER_SAFE_STREAM_DETAILS_SAFE_STREAM_SEND_ACTION_H_ + +#include + +#include "aether/common.h" +#include "aether/config.h" +#include "aether/ae_context.h" +#include "aether/events/events.h" +#include "aether/actions/timer.h" +#include "aether/types/statistic_counter.h" +#include "aether/events/multi_subscription.h" +#include "aether/write_action/write_action.h" +#include "aether/safe_stream/safe_stream_config.h" +#include "aether/safe_stream/details/circular_buffer.h" +#include "aether/safe_stream/details/sending_chunk_list.h" +#include "aether/safe_stream/details/safe_stream_data_message.h" + +#include "aether/tele/tele.h" + +namespace ae { + +namespace safe_stream_send_action_internal { +template +static inline Num RandomOffset() { + // set seed once + static bool const seed = + (std::srand(static_cast(time(nullptr))), true); + (void)seed; + auto value = + static_cast(std::rand()) % (std::numeric_limits::max() / 2); + return static_cast(value); +} +} // namespace safe_stream_send_action_internal + +class ISendDataPush { + public: + virtual ~ISendDataPush() = default; + virtual WriteAction& PushData(std::uint16_t index, + DataMessage&& data_message) = 0; +}; + +// TODO: split on templated and not templated parts +template +class SafeStreamSendAction { + public: + using ResponseStatistics = + StatisticsCounter; + + static constexpr std::size_t kCapacity = Capacity; + using CircularBufferImpl = CircularBuffer; + using IndexType = CircularBufferImpl::index_type; + using IndexRangeType = RingIndexRange; + using SendingChunkListImpl = SendingChunkList; + + using AcknowledgedEvent = Event; + using SendFailedEvent = Event; + using StoppedEvent = Event; + + SafeStreamSendAction(AeContext const& ae_context, + ISendDataPush& send_data_push, + SafeStreamConfig const& config) + : ae_context_{ae_context}, + send_data_push_{&send_data_push}, + alive_ctx_{ae_context_, this}, + max_repeat_count_{config.max_repeat_count}, + window_size_{config.window_size}, + sending_buffer_{ + safe_stream_send_action_internal::RandomOffset()}, + sending_chunks_{sending_buffer_.begin()}, + last_sent_{sending_buffer_.begin()} { + // set first response timeout + response_statistics_.Add(config.wait_ack_timeout); + } + + AE_CLASS_NO_COPY_MOVE(SafeStreamSendAction) + + Result SendData(std::span data) { + auto res = sending_buffer_.Push(data); + if (!res) { + AE_TELED_ERROR("Got circular buffer error {}", res.error()); + return Error{1}; + } + EnqueueSend(); + return Ok{std::move(res).value()}; + } + + /** + * \brief Stop sending data in range. + */ + void Stop(IndexRangeType range) { + // Stop only chunks what are still not sending + auto* sc = sending_chunks_.Select(range); + if (sc != nullptr) { + return; + } + AE_TELED_DEBUG("Stop range {}-{}", range.left, range.right); + stopped_event_.Emit(sending_buffer_.begin(), range.right); + sending_buffer_.EraseFrom(range.left); + } + + bool Acknowledge(std::uint16_t confirm_offset) { + auto confirm_index = IndexType(confirm_offset); + AE_TELED_DEBUG("Receive ack for offset {} index {}", confirm_offset, + confirm_index); + // if not in a current range, ignore + if (!IndexRangeType{sending_buffer_.begin(), sending_buffer_.end()}.InRange( + confirm_index, sending_buffer_.begin())) { + AE_TELED_DEBUG("Ack is not in a current range"); + return false; + } + // receiving an ack means the other side synced its state + init_state_ = false; + + if (!sending_chunks_.empty()) { + auto response_duration = std::chrono::duration_cast( + Now() - sending_chunks_.front().send_time); + response_statistics_.Add(response_duration); + } + + if (IndexComparable{last_sent_, sending_buffer_.begin()} < confirm_index) { + last_sent_ = confirm_index + 1; + } + acknowledged_event_.Emit(sending_buffer_.begin(), confirm_index); + sending_buffer_.Erase(confirm_index + 1); + sending_chunks_.RemoveUpTo(confirm_index); + sending_chunks_.set_buffer_begin(sending_buffer_.begin()); + // enqueue to send new chunks, because data on the go size reduced + EnqueueSend(); + // re-enqueu repeat timer for next waiting chunks + repeat_timer_.reset(); + EnqueueRepeatTimeout(); + return true; + } + + void RequestRepeat(std::uint16_t request_offset) { + auto request_index = IndexType(request_offset); + // if not in a current range + if (!IndexRangeType{sending_buffer_.begin(), sending_buffer_.end()}.InRange( + request_index, sending_buffer_.begin())) { + AE_TELED_WARNING("Request repeat send for not sent index {}", + request_index); + return; + } + last_sent_ = request_index; + EnqueueSend(); + } + + void SetMaxPayload(std::size_t max_payload_size) { + AE_TELED_DEBUG("Set max payload to {}", max_payload_size); + max_payload_size_ = max_payload_size; + EnqueueSend(); + } + + AcknowledgedEvent::Subscriber acknowledged_event() { + return EventSubscriber{acknowledged_event_}; + } + StoppedEvent::Subscriber stopped_event() { + return EventSubscriber{stopped_event_}; + } + SendFailedEvent::Subscriber send_failed_event() { + return EventSubscriber{send_failed_event_}; + } + + private: + void EnqueueSend() { + if (send_enqueued_) { + return; + } + send_enqueued_ = true; + ae_context_.scheduler().Task([imalive{alive_ctx_.View()}]() { + if (imalive) { + imalive->send_enqueued_ = false; + imalive->SendChunk(Now()); + } + }); + } + + void EnqueueRepeatTimeout() { + // TODO: add way to re-enqueue ack timeout + if (repeat_timer_ && !repeat_timer_->is_finished()) { + return; + } + + if (sending_chunks_.empty()) { + return; + } + + // Get timeout for the oldest chunk and setup delayed task + // If when task is executed, the same chunk is still in the list, + // it means the chunk is not acknowledged, so we need to repeat it. + auto const& selected_sch = sending_chunks_.front(); + // wait timeout is depends on repeat_count and response statistics + auto wait_ack_timeout = + static_cast(response_statistics_.percentile<99>().count()); + auto increase_factor = std::max(1.0, (AE_SAFE_STREAM_RTO_GROW_FACTOR * + (selected_sch.repeat_count - 1))); + auto wait_timeout = Duration{ + static_cast(wait_ack_timeout * increase_factor)}; + auto wait_time = selected_sch.send_time + wait_timeout; + + repeat_timer_.emplace( + ae_context_, + [this, range{selected_sch.range}]() { + ProcessRepeat(range); + // enqueue repeat timeout for the next chunk + ae_context_.scheduler().Task([imalive{alive_ctx_.View()}]() { + if (imalive) { + imalive->EnqueueRepeatTimeout(); + } + }); + }, + wait_time); + } + + void ProcessRepeat(IndexRangeType const& range) { + if (sending_chunks_.empty()) { + return; + } + AE_TELED_DEBUG("Wait ack timeout, repeat offset {}", range.left); + // if the range was partially acknowledged, repeat from the + // beginning + if (sending_buffer_.begin().Distance(range.left) > window_size_) { + last_sent_ = sending_buffer_.begin(); + } else { + last_sent_ = range.left; + } + EnqueueSend(); + } + + auto GetNextChunk() { + auto on_the_go_data_size = Distance(sending_buffer_.begin(), last_sent_); + auto payload_size = + std::min(max_payload_size_, window_size_ - on_the_go_data_size); + + // read payload_size + return sending_buffer_.Read(last_sent_, payload_size) + .Else([&](CircularBufferError err) + -> Result { + if (err == CircularBufferError::kEmptyBuffer) { + AE_TELED_DEBUG("Send buffer is empty"); + // no data to send + return Error{0}; + } + AE_TELED_ERROR("Read from buffer error: {}", static_cast(err)); + return Error{1}; + }) + .Then([&](auto const& dspan) + -> Result { + // the read result is a pair of spans + if (dspan.size() == 0) { + AE_TELED_WARNING( + "Window size exceeded: begin: {} last_sent: {} on the go data " + "size: {}, window size: {}", + sending_buffer_.begin(), last_sent_, on_the_go_data_size, + window_size_); + } + return Ok{dspan}; + }); + } + + Result, + int> + RegisterChunk(CircularBufferImpl::DSpan dspan, IndexRangeType chunk_range, + TimePoint current_time) { + auto& send_chunk = sending_chunks_.Register(chunk_range, current_time); + + send_chunk.repeat_count++; + if (send_chunk.repeat_count > max_repeat_count_) { + AE_TELED_ERROR("Repeat count exceeded"); + return Error{2}; + } + return Ok{std::pair{dspan, send_chunk}}; + } + + /** + The logic to actually send data. + - check if max_payload_size_ is set + - read chunk of data from the buffer starting from last_sent_ index. + - register new sending chunk and count repeats + - push the data to send_data_push_ and wait either for timeout or result + */ + void SendChunk(TimePoint current_time) { + auto res = GetNextChunk().Then([&](CircularBufferImpl::DSpan const& dspan) { + auto chunk_index_range = IndexRangeType{ + .left = last_sent_, + .right = last_sent_ + dspan.size() - 1, + }; + last_sent_ += dspan.size(); + + return RegisterChunk(dspan, chunk_index_range, current_time); + }); + if (!res) { + // If any error over empty buffer + if (res.error() != 0) { + AE_TELED_ERROR("Chunk send error!"); + // reject to send all chunks before the failed one + RejectSend(last_sent_ - 1); + } + AE_TELED_DEBUG("Chunk didn't sent!"); + return; + } + + // push chunk to sender stream api implementation + // and wait for send result + auto const& [dspan, send_chunk] = res.value(); + if (dspan.size() == 0) { + AE_TELED_DEBUG("No chunks to send!"); + return; + } + + send_subs_ += + PushData(dspan, send_chunk.range.left, send_chunk.repeat_count - 1) + .status_event() + .Subscribe([this, end_index{send_chunk.range.right}](auto status) { + switch (status) { + case WriteAction::Status::kSuccess: + break; + case WriteAction::Status::kFail: + case WriteAction::Status::kStop: + // if send failed, repeat + if (IndexComparable{last_sent_, sending_buffer_.begin()} > + end_index) { + last_sent_ = end_index; + } + EnqueueSend(); + break; + } + }); + // enqueue next chunk send and setup repeat timeout + EnqueueSend(); + EnqueueRepeatTimeout(); + } + + void RejectSend(IndexType end_index) { + // send failed for a chunk + send_failed_event_.Emit(sending_buffer_.begin(), end_index); + + sending_buffer_.Erase(end_index + 1); + sending_chunks_.RemoveUpTo(end_index); + sending_chunks_.set_buffer_begin(sending_buffer_.begin()); + last_sent_ = sending_buffer_.begin(); + // try sending next + EnqueueSend(); + } + + WriteAction& PushData(CircularBufferImpl::DSpan const& dspan, + IndexType data_index, std::uint8_t repeat_count) { + AE_TELED_DEBUG( + "Send data message begin index {} data_index {} repeat count {} " + "reset {} data size {}", + sending_buffer_.begin(), data_index, static_cast(repeat_count), + init_state_, dspan.size()); + + DataBuffer data_buffer(dspan.size()); + std::copy(dspan.first.begin(), dspan.first.end(), data_buffer.begin()); + std::copy(dspan.second.begin(), dspan.second.end(), + data_buffer.begin() + + static_cast(dspan.first.size())); + + return send_data_push_->PushData( + static_cast(sending_buffer_.begin()), + DataMessage{ + init_state_, + repeat_count, + static_cast( + sending_buffer_.begin().Distance(data_index)), + std::move(std::move(data_buffer)), + }); + } + + AeContext ae_context_; + ISendDataPush* send_data_push_; + IndexCtx alive_ctx_; + + std::uint8_t max_repeat_count_{}; + std::size_t window_size_{}; + std::size_t max_payload_size_{}; + + CircularBufferImpl sending_buffer_; + SendingChunkListImpl sending_chunks_; + IndexType last_sent_; //< last index for last sent data + bool init_state_{true}; + + MultiSubscription sending_data_subs_; + MultiSubscription send_subs_; + ResponseStatistics response_statistics_; + bool send_enqueued_{false}; + std::optional repeat_timer_; + AcknowledgedEvent acknowledged_event_; + StoppedEvent stopped_event_; + SendFailedEvent send_failed_event_; +}; +} // namespace ae + +#endif // AETHER_SAFE_STREAM_DETAILS_SAFE_STREAM_SEND_ACTION_H_ diff --git a/aether/safe_stream/details/sending_chunk_list.h b/aether/safe_stream/details/sending_chunk_list.h new file mode 100644 index 00000000..907ff468 --- /dev/null +++ b/aether/safe_stream/details/sending_chunk_list.h @@ -0,0 +1,91 @@ +/* + * Copyright 2024 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_SAFE_STREAM_DETAILS_SENDING_CHUNK_LIST_H_ +#define AETHER_SAFE_STREAM_DETAILS_SENDING_CHUNK_LIST_H_ + +#include + +#include "aether/clock.h" +#include "aether/types/ring_index.h" + +namespace ae { +template +class SendingChunkList { + public: + using IndexRangeType = RingIndexRange; + + struct Chunk { + IndexRangeType range; + TimePoint send_time; + std::uint8_t repeat_count; + }; + + explicit SendingChunkList(Index buffer_begin) : buffer_begin_{buffer_begin} {} + + void set_buffer_begin(Index buffer_begin) { buffer_begin_ = buffer_begin; } + + /** + * \brief Register a new sending chunk. + * If chunk with that index does not exist, it will be created at the end + * of the list. Otherwise, it will be moved to the end of the list and + * updated. + */ + Chunk& Register(IndexRangeType chunk_range, TimePoint send_time) { + auto old_it = std::find_if( + std::begin(chunks_), std::end(chunks_), + [&](auto const& c) { return c.range.left == chunk_range.left; }); + + auto chunk = + Chunk{.range = chunk_range, .send_time = send_time, .repeat_count{}}; + + if (old_it != std::end(chunks_)) { + chunk.repeat_count = old_it->repeat_count; + chunks_.erase(old_it); + } + return chunks_.emplace_back(chunk); + } + + /** + * \brief Remove all chunks up to the given offset. + */ + void RemoveUpTo(Index to) { + std::erase_if(chunks_, [&](auto const& c) { + return IndexComparable{c.range.right, buffer_begin_} <= to; + }); + } + + Chunk* Select(IndexRangeType range) { + // TODO: make more precise selection + for (auto& c : chunks_) { + if (range.InRange(c.range.left, buffer_begin_)) { + return &c; + } + } + return nullptr; + } + + Chunk& front() { return chunks_.front(); } + bool empty() const { return chunks_.empty(); } + + private: + Index buffer_begin_; + // TODO: make a logic without allocations and resorting + std::vector chunks_; +}; +} // namespace ae + +#endif // AETHER_SAFE_STREAM_DETAILS_SENDING_CHUNK_LIST_H_ diff --git a/aether/safe_stream/safe_stream.h b/aether/safe_stream/safe_stream.h new file mode 100644 index 00000000..a3293bcd --- /dev/null +++ b/aether/safe_stream/safe_stream.h @@ -0,0 +1,239 @@ +/* + * Copyright 2024 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_STREAM_API_SAFE_STREAM_H_ +#define AETHER_STREAM_API_SAFE_STREAM_H_ + +#include "aether/common.h" +#include "aether/actions/action_context.h" +#include "aether/write_action/failed_write_action.h" + +#include "aether/stream_api/api_call_adapter.h" +#include "aether/safe_stream/safe_stream_config.h" +#include "aether/safe_stream/details/safe_stream_api.h" +#include "aether/safe_stream/details/safe_stream_recv_action.h" +#include "aether/safe_stream/details/safe_stream_send_action.h" +#include "aether/safe_stream/details/safe_stream_data_message.h" + +#include "aether/stream_api/istream.h" + +#include "aether/tele/tele.h" + +namespace ae { +template +class SafeStream final : public ByteStream, // NOLINT + public SafeStreamApi, + public ISendDataPush, + public ISendAckRepeat { + using IndexType = RingIndex; + using IndexRangeType = RingIndexRange; + + class SSWriteAction final : public WriteAction { + public: + explicit SSWriteAction(IndexRangeType index_range, SafeStream& stream) + : stream_{&stream}, index_range_{index_range} {} + + // TODO: add tests for stop + void Stop() noexcept override { stream_->StopWrite(index_range_); } + + void Acknowledge() noexcept { SetStatus(Status::kSuccess); } + void Rejected() noexcept { SetStatus(Status::kFail); } + void Stopped() noexcept { SetStatus(Status::kStop); } + + private: + SafeStream* stream_; + IndexRangeType index_range_; + }; + + public: + using SafeStreamSender = SafeStreamSendAction; + using SafeStreamReceiver = SafeStreamRecvAction; + + SafeStream(AeContext const& ae_context, SafeStreamConfig config) + : SafeStreamApi{protocol_context_}, + ae_context_{ae_context}, + config_{config}, + sender_{ae_context_, *this, config_}, + receiver_{ae_context_, *this, config_}, + stream_info_{config_.max_packet_size, config_.max_packet_size, false, + LinkState::kUnlinked, false} { + sender_.acknowledged_event().Subscribe( + MethodPtr<&SafeStream::WriteAcknowledged>{this}); + sender_.send_failed_event().Subscribe( + MethodPtr<&SafeStream::WriteFailed>{this}); + + receiver_.receive_event().Subscribe(MethodPtr<&SafeStream::WriteOut>{this}); + } + + AE_CLASS_NO_COPY_MOVE(SafeStream); + + WriteAction& Write(DataBuffer&& data) override { // NOLINT(*param-not-moved) + auto res = sender_.SendData(std::span{data}); + if (!res) { + if (!failed_write_ || failed_write_->is_finished()) { + failed_write_.emplace(ae_context_); + } + return *failed_write_; + } + // TODO: make without allocation + auto& ref = sswas_.emplace_back( + std::make_pair(res.value().right, + std::make_unique(res.value(), *this))); + return *ref.second; + } + + StreamInfo stream_info() const override { return stream_info_; } + + void LinkOut(OutStream& out) override { + out_ = &out; + update_sub_ = out_->stream_update_event().Subscribe( + MethodPtr<&SafeStream::OnStreamUpdate>{this}); + out_data_sub_ = out_->out_data_event().Subscribe( + MethodPtr<&SafeStream::OnOutData>{this}); + + OnStreamUpdate(); + } + + // Api impl methods + void AckImpl(std::uint16_t index) override { sender_.Acknowledge(index); } + void RequestRepeatImpl(std::uint16_t index) override { + sender_.RequestRepeat(index); + } + void SendResetImpl(std::uint16_t begin_offset, std::uint16_t delta_offset, + std::uint8_t repeat_count, DataBuffer data) override { + receiver_.PushData(begin_offset, DataMessage{.reset = true, + .repeat_count = repeat_count, + .delta_offset = delta_offset, + .data = std::move(data)}); + } + void SendImpl(std::uint16_t begin_offset, std::uint16_t delta_offset, + std::uint8_t repeat_count, DataBuffer data) override { + receiver_.PushData(begin_offset, DataMessage{.reset = false, + .repeat_count = repeat_count, + .delta_offset = delta_offset, + .data = std::move(data)}); + } + + // Implement ISendDataPush + WriteAction& PushData( + std::uint16_t begin_offset, + DataMessage&& data_message) override { // NOLINT (*param-not-moved) + assert(out_); + auto api_adapter = ApiCallAdapter{ApiContext{*this}, *out_}; + if (data_message.reset) { + api_adapter->send_reset(begin_offset, data_message.delta_offset, + data_message.repeat_count, + std::move(data_message.data)); + } else { + api_adapter->send(begin_offset, data_message.delta_offset, + data_message.repeat_count, + std::move(data_message.data)); + } + + // cppcheck reports false positive + // cppcheck-suppress returnReference + return api_adapter.Flush(); + } + + // Implement ISendConfirmRepeat + void SendAck(std::uint16_t index) override { + assert(out_); + auto api_adapter = ApiCallAdapter{ApiContext{*this}, *out_}; + api_adapter->ack(index); + api_adapter.Flush(); + } + + void SendRepeatRequest(std::uint16_t index) override { + assert(out_); + auto api_adapter = ApiCallAdapter{ApiContext{*this}, *out_}; + api_adapter->request_repeat(index); + api_adapter.Flush(); + } + + void StopWrite(IndexRangeType index_range) { sender_.Stop(index_range); } + + private: + void WriteOut(DataBuffer const& data) { out_data_event_.Emit(data); } + void OnStreamUpdate() { + AE_TELED_DEBUG("Safe stream update"); + static constexpr std::size_t kSendOverhead = + 1 + sizeof(std::uint16_t) + 1 + 1 + 2; + + auto out_info = out_->stream_info(); + stream_info_.link_state = out_info.link_state; + stream_info_.is_writable = out_info.is_writable; + stream_info_.is_reliable = + true; // safe stream here to make stream reliable + stream_info_.rec_element_size = out_info.rec_element_size; + stream_info_.max_element_size = config_.max_packet_size; + + sender_.SetMaxPayload((out_info.rec_element_size > 0) + ? (out_info.rec_element_size - kSendOverhead) + : 0); + + stream_update_event_.Emit(); + } + + void OnOutData(DataBuffer const& data) { + AE_TELED_DEBUG("Received data {}", data); + auto api_parser = ApiParser{protocol_context_, data}; + api_parser.Parse(*this); + } + + void WriteAcknowledged(IndexType buffer_begin, IndexType ack_index) { + for (auto& [index, sswa] : sswas_) { + if (IndexComparable{index, buffer_begin} <= ack_index) { + sswa->Acknowledge(); + } + } + std::erase_if(sswas_, + [](auto const& e) { return e.second->is_finished(); }); + } + + void WriteStopped(IndexType buffer_begin, IndexType stop_index) { + for (auto& [index, sswa] : sswas_) { + if (IndexComparable{index, buffer_begin} <= stop_index) { + sswa->Stopped(); + } + } + std::erase_if(sswas_, + [](auto const& e) { return e.second->is_finished(); }); + } + + void WriteFailed(IndexType buffer_begin, IndexType failed_index) { + for (auto& [index, sswa] : sswas_) { + if (IndexComparable{index, buffer_begin} <= failed_index) { + sswa->Rejected(); + } + } + std::erase_if(sswas_, + [](auto const& e) { return e.second->is_finished(); }); + } + + AeContext ae_context_; + SafeStreamConfig config_; + ProtocolContext protocol_context_; + SafeStreamSender sender_; + SafeStreamReceiver receiver_; + + std::optional failed_write_; + std::vector>> sswas_; + + StreamInfo stream_info_; +}; +} // namespace ae + +#endif // AETHER_STREAM_API_SAFE_STREAM_H_ diff --git a/aether/stream_api/safe_stream/safe_stream_config.h b/aether/safe_stream/safe_stream_config.h similarity index 52% rename from aether/stream_api/safe_stream/safe_stream_config.h rename to aether/safe_stream/safe_stream_config.h index 6615f8bc..cd92a285 100644 --- a/aether/stream_api/safe_stream/safe_stream_config.h +++ b/aether/safe_stream/safe_stream_config.h @@ -14,25 +14,23 @@ * limitations under the License. */ -#ifndef AETHER_STREAM_API_SAFE_STREAM_SAFE_STREAM_CONFIG_H_ -#define AETHER_STREAM_API_SAFE_STREAM_SAFE_STREAM_CONFIG_H_ +#ifndef AETHER_SAFE_STREAM_SAFE_STREAM_CONFIG_H_ +#define AETHER_SAFE_STREAM_SAFE_STREAM_CONFIG_H_ #include -#include "aether/common.h" -#include "aether/stream_api/safe_stream/safe_stream_types.h" +#include "aether/clock.h" namespace ae { struct SafeStreamConfig { - SSRingIndex::type buffer_capacity; //< sending buffer capacity - SSRingIndex::type window_size; //< size of sending window - SSRingIndex::type max_packet_size; //< max size of sending data - std::uint8_t max_repeat_count; //< max repeat count for sending packet - Duration wait_ack_timeout; //< Timeout for waiting ack - Duration send_ack_timeout; //< max time to wait before send ack + std::size_t window_size; //< size of sending window + std::size_t max_packet_size; //< max size of sending data + std::uint8_t max_repeat_count; //< max repeat count for sending packet + Duration wait_ack_timeout; //< Timeout for waiting ack + Duration send_ack_timeout; //< max time to wait before send ack Duration send_repeat_timeout; //< max time to wait before send repeat request }; } // namespace ae -#endif // AETHER_STREAM_API_SAFE_STREAM_SAFE_STREAM_CONFIG_H_ +#endif // AETHER_SAFE_STREAM_SAFE_STREAM_CONFIG_H_ diff --git a/aether/stream_api/safe_stream.cpp b/aether/stream_api/safe_stream.cpp deleted file mode 100644 index 4019e9e9..00000000 --- a/aether/stream_api/safe_stream.cpp +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2024 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "aether/stream_api/safe_stream.h" - -#include - -#include "aether/stream_api/api_call_adapter.h" - -#include "aether/tele/tele.h" - -namespace ae { - -SafeStreamWriteAction::SafeStreamWriteAction( - ActionPtr sending_data_action) - : sending_data_action_{std::move(sending_data_action)} { - subscriptions_.Push( - sending_data_action_->StatusEvent().Subscribe([this](auto status) { - status.OnResult([&]() { SetStatus(WriteAction::Status::kSuccess); }) - .OnError([&]() { SetStatus(WriteAction::Status::kFail); }) - .OnStop([&]() { SetStatus(WriteAction::Status::kStop); }); - })); -} - -// TODO: add tests for stop -void SafeStreamWriteAction::Stop() noexcept { - if (sending_data_action_) { - sending_data_action_->Stop(); - } -} - -SafeStream::SafeStream(AeContext const& ae_context, SafeStreamConfig config) - : config_{config}, - safe_stream_api_{protocol_context_, *this}, - send_action_{ae_context, *this, config_}, - recv_acion_{ae_context, *this, config_}, - stream_info_{config_.max_packet_size, config_.max_packet_size, false, - LinkState::kUnlinked, false} { - recv_acion_->receive_event().Subscribe( - MethodPtr<&SafeStream::WriteOut>{this}); -} - -WriteAction& SafeStream::Write(DataBuffer&& data) { - sswas_.emplace_back(std::make_unique( - send_action_->SendData(std::move(data)))); - // TODO: cleanup finished actions - return *sswas_.back(); -} - -StreamInfo SafeStream::stream_info() const { return stream_info_; } - -void SafeStream::LinkOut(OutStream& out) { - out_ = &out; - update_sub_ = out_->stream_update_event().Subscribe( - MethodPtr<&SafeStream::OnStreamUpdate>{this}); - out_data_sub_ = - out_->out_data_event().Subscribe(MethodPtr<&SafeStream::OnOutData>{this}); - - OnStreamUpdate(); -} - -void SafeStream::WriteOut(DataBuffer const& data) { - out_data_event_.Emit(data); -} - -void SafeStream::OnStreamUpdate() { - AE_TELED_DEBUG("Safe stream update"); - static constexpr std::size_t kSendOverhead = - 1 + sizeof(SSRingIndex::type) + 1 + 1 + 2; - - auto out_info = out_->stream_info(); - stream_info_.link_state = out_info.link_state; - stream_info_.is_writable = out_info.is_writable; - stream_info_.is_reliable = true; // safe stream here to make stream reliable - stream_info_.rec_element_size = out_info.rec_element_size; - stream_info_.max_element_size = config_.max_packet_size; - - send_action_->SetMaxPayload((out_info.rec_element_size > 0) - ? (out_info.rec_element_size - kSendOverhead) - : 0); - - stream_update_event_.Emit(); -} - -void SafeStream::OnOutData(DataBuffer const& data) { - AE_TELED_DEBUG("received data {}", data); - auto api_parser = ApiParser{protocol_context_, data}; - api_parser.Parse(safe_stream_api_); -} - -void SafeStream::Ack(SSRingIndex::type offset) { - auto confirm_offset = SSRingIndex{offset}; - send_action_->Acknowledge(confirm_offset); -} - -void SafeStream::RequestRepeat(SSRingIndex::type offset) { - auto request_offset = SSRingIndex{offset}; - send_action_->RequestRepeat(request_offset); -} - -void SafeStream::Send(SSRingIndex::type begin_offset, - DataMessage data_message) { - auto received_offset = SSRingIndex{begin_offset}; - recv_acion_->PushData(received_offset, std::move(data_message)); -} - -WriteAction& SafeStream::PushData(SSRingIndex begin, - DataMessage&& data_message) { - assert(out_); - auto api_adapter = ApiCallAdapter{ApiContext{safe_stream_api_}, *out_}; - api_adapter->send(static_cast(begin), - std::move(data_message)); - - // cppcheck reports false positive - // cppcheck-suppress returnReference - return api_adapter.Flush(); -} - -void SafeStream::SendAck(SSRingIndex offset) { - assert(out_); - auto api_adapter = ApiCallAdapter{ApiContext{safe_stream_api_}, *out_}; - - api_adapter->ack(static_cast(offset)); - api_adapter.Flush(); -} - -void SafeStream::SendRepeatRequest(SSRingIndex offset) { - assert(out_); - auto api_adapter = ApiCallAdapter{ApiContext{safe_stream_api_}, *out_}; - - api_adapter->request_repeat(static_cast(offset)); - api_adapter.Flush(); -} - -} // namespace ae diff --git a/aether/stream_api/safe_stream.h b/aether/stream_api/safe_stream.h deleted file mode 100644 index 720093eb..00000000 --- a/aether/stream_api/safe_stream.h +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2024 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AETHER_STREAM_API_SAFE_STREAM_H_ -#define AETHER_STREAM_API_SAFE_STREAM_H_ - -#include "aether/common.h" -#include "aether/actions/action_ptr.h" -#include "aether/actions/action_context.h" -#include "aether/events/multi_subscription.h" - -#include "aether/stream_api/safe_stream/safe_stream_api.h" -#include "aether/stream_api/safe_stream/safe_stream_types.h" -#include "aether/stream_api/safe_stream/safe_stream_config.h" -#include "aether/stream_api/safe_stream/safe_stream_recv_action.h" -#include "aether/stream_api/safe_stream/safe_stream_send_action.h" - -#include "aether/stream_api/istream.h" - -namespace ae { -class SafeStreamWriteAction final : public WriteAction { - public: - explicit SafeStreamWriteAction( - ActionPtr sending_data_action); - - // TODO: add tests for stop - void Stop() noexcept override; - - private: - ActionPtr sending_data_action_; - MultiSubscription subscriptions_; -}; - -class SafeStream final : public ByteStream, - public SafeStreamApiImpl, - public ISendDataPush, - public ISendAckRepeat { - public: - SafeStream(AeContext const& ae_context, SafeStreamConfig config); - - AE_CLASS_NO_COPY_MOVE(SafeStream); - - WriteAction& Write(DataBuffer&& data) override; - StreamInfo stream_info() const override; - - void LinkOut(OutStream& out) override; - - // Api impl methods - void Ack(SSRingIndex::type offset) override; - void RequestRepeat(SSRingIndex::type offset) override; - void Send(SSRingIndex::type begin_offset, DataMessage data_message) override; - - // Implement ISendDataPush - WriteAction& PushData(SSRingIndex begin, DataMessage&& data_message) override; - - // Implement ISendConfirmRepeat - void SendAck(SSRingIndex offset) override; - void SendRepeatRequest(SSRingIndex offset) override; - - private: - void WriteOut(DataBuffer const& data); - void OnStreamUpdate(); - void OnOutData(DataBuffer const& data); - - SafeStreamConfig config_; - ProtocolContext protocol_context_; - SafeStreamApi safe_stream_api_; - ActionPtr send_action_; - ActionPtr recv_acion_; - - std::vector> sswas_; - - StreamInfo stream_info_; -}; -} // namespace ae - -#endif // AETHER_STREAM_API_SAFE_STREAM_H_ diff --git a/aether/stream_api/safe_stream/receiving_chunk_list.cpp b/aether/stream_api/safe_stream/receiving_chunk_list.cpp deleted file mode 100644 index 79cca1cc..00000000 --- a/aether/stream_api/safe_stream/receiving_chunk_list.cpp +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2025 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "aether/stream_api/safe_stream/receiving_chunk_list.h" - -#include - -#include "aether/tele/tele.h" - -namespace ae { -ReceiveChunkList::AddResult ReceiveChunkList::AddChunk(ReceivingChunk chunk) { - /* Possible cases ARE: - * - received a full duplicate of existing chunk - * - chunk that is in range of existing chunk - * - chunk that is partially overlaps existing chunk either left or right - * - chunk that is overlaps a few chunks - * - chunk with the most biggest offset - * - chunk overlapped with already acknowledged - */ - // find the biggest chunk - auto pos = - std::find_if(std::begin(chunks_), std::end(chunks_), [&](auto const& c) { - return chunk.offset.IsBefore(c.offset) || (chunk.offset == c.offset); - }); - - // the same chunk - if ((pos != std::end(chunks_)) && - (pos->offset_range() == chunk.offset_range())) { - if (pos->repeat_count >= chunk.repeat_count) { - return AddResult::kDuplicate; - } - pos->repeat_count = std::max(pos->repeat_count, chunk.repeat_count); - return AddResult::kAdded; - } - - if (pos != std::end(chunks_)) { - // fix found chunk if overlaps - auto chunk_range = chunk.offset_range(); - if (pos->offset_range().InRange(chunk_range.right)) { - MergeChunkProperties(chunk, *pos); - FixChunkOverlapsBegin(*pos, chunk_range.right + 1); - } - } - - // chunk is not the same with any of the existing chunks - auto inserted_it = chunks_.insert(pos, std::move(chunk)); - auto next_offset = inserted_it->offset; - // fix overlapping chunks offsets and remove empty - chunks_.erase(std::remove_if(std::begin(chunks_), inserted_it, - [&](auto& c) { - // overlapping offset - auto range = c.offset_range(); - if (range.InRange(next_offset)) { - MergeChunkProperties(*inserted_it, c); - FixChunkOverlapsEnd(c, next_offset - 1); - } - // remove if empty - return c.begin == c.end; - }), - inserted_it); - return AddResult::kAdded; -} - -std::optional ReceiveChunkList::ReceiveChunk( - SSRingIndex start_chunks) { - auto chank_chain = GetContinuousChunkChain(start_chunks); - if (chank_chain.empty()) { - return std::nullopt; - } - - auto data = JoinChunks(chank_chain); - AE_TELED_DEBUG("Data chunk chain received length: {}", data.size()); - return ReceivingChunk{start_chunks, std::move(data), {}}; -} - -void ReceiveChunkList::Acknowledge(SSRingIndex from, SSRingIndex to) { - chunks_.erase(std::remove_if(std::begin(chunks_), std::end(chunks_), - [&](auto& c) { - // remove old chunks - auto range = c.offset_range(); - if (range.IsBefore(from)) { - return true; - } - if (range.IsBefore(to + 1)) { - return true; - } - return false; - }), - std::end(chunks_)); -} - -std::vector ReceiveChunkList::FindMissedChunks( - SSRingIndex start_chunks) { - auto it = - std::find_if(std::begin(chunks_), std::end(chunks_), [&](auto const& c) { - auto range = c.offset_range(); - return range.InRange(start_chunks) || range.IsAfter(start_chunks); - }); - - if (it == std::end(chunks_)) { - return {}; - } - - std::vector res; - auto next_chunk_offset = [&]() { - if (auto range = it->offset_range(); range.InRange(start_chunks)) { - return range.left; - } - return start_chunks; - }(); - - for (; it != std::end(chunks_); ++it) { - // if got not expected chunk - if (next_chunk_offset != it->offset) { - res.emplace_back(MissedChunk{next_chunk_offset, &*it}); - } - next_chunk_offset = it->offset_range().right + 1; - } - return res; -} - -void ReceiveChunkList::Clear() { chunks_.clear(); } - -DataBuffer ReceiveChunkList::JoinChunks( - std::vector> const& - chunk_chain) { - DataBuffer data; - // count size first - std::size_t size = 0; - for (auto const& c : chunk_chain) { - size += static_cast(std::distance(c.first, c.second)); - } - data.reserve(size); - // then copy data - for (auto const& c : chunk_chain) { - std::copy(c.first, c.second, std::back_inserter(data)); - } - return data; -} - -void ReceiveChunkList::FixChunkOverlapsBegin(ReceivingChunk& overlapped, - SSRingIndex expected_offset) { - // fix offset and begin if chunks overlap - auto const distance = - static_cast(overlapped.offset.Distance(expected_offset)); - overlapped.offset = expected_offset; - overlapped.begin = ((overlapped.end - overlapped.begin) > distance) - ? overlapped.begin + distance - : overlapped.end; -} - -void ReceiveChunkList::FixChunkOverlapsEnd(ReceivingChunk& overlapped, - SSRingIndex expected_end) { - auto const distance = static_cast( - expected_end.Distance(overlapped.offset_range().right)); - auto overlapped_size = (overlapped.end - overlapped.begin); - // shift overlapped.end left - overlapped.end = (overlapped_size > distance) ? overlapped.end - distance - : overlapped.begin; -} - -void ReceiveChunkList::MergeChunkProperties(ReceivingChunk& chunk, - ReceivingChunk& overlapped) { - chunk.repeat_count = std::max(overlapped.repeat_count, chunk.repeat_count); -} -std::vector> -ReceiveChunkList::GetContinuousChunkChain(SSRingIndex start_chunks) { - auto from = std::find_if( - std::begin(chunks_), std::end(chunks_), - [&](auto const& c) { return c.offset_range().InRange(start_chunks); }); - if (from == std::end(chunks_)) { - return {}; - } - if (from->data.empty()) { - return {}; - } - - std::vector> res; - - auto distance = - static_cast(from->offset.Distance(start_chunks)); - res.emplace_back(from->begin + distance, from->end); - - auto it = std::next(from); - auto next_chunk_offset = from->offset_range().right + 1; - for (; it != std::end(chunks_); it++) { - auto& chunk = *it; - if (next_chunk_offset != chunk.offset) { - break; - } - if (chunk.data.empty()) { - break; - } - next_chunk_offset = chunk.offset_range().right + 1; - res.emplace_back(chunk.begin, chunk.end); - } - return res; -} - -} // namespace ae diff --git a/aether/stream_api/safe_stream/receiving_chunk_list.h b/aether/stream_api/safe_stream/receiving_chunk_list.h deleted file mode 100644 index 470b4542..00000000 --- a/aether/stream_api/safe_stream/receiving_chunk_list.h +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2025 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AETHER_STREAM_API_SAFE_STREAM_RECEIVING_CHUNK_LIST_H_ -#define AETHER_STREAM_API_SAFE_STREAM_RECEIVING_CHUNK_LIST_H_ - -#include -#include - -#include "aether/stream_api/safe_stream/safe_stream_types.h" - -namespace ae { -class ReceiveChunkList { - public: - enum class AddResult : std::uint8_t { - kDuplicate, - kAdded, - }; - - ReceiveChunkList() = default; - - AddResult AddChunk(ReceivingChunk chunk); - - std::optional ReceiveChunk(SSRingIndex start_chunks); - void Acknowledge(SSRingIndex from, SSRingIndex to); - std::vector FindMissedChunks(SSRingIndex start_chunks); - - void Clear(); - - std::size_t size() const { return chunks_.size(); } - bool empty() const { return chunks_.empty(); } - - private: - static DataBuffer JoinChunks( - std::vector> const& - chunk_chain); - - static void FixChunkOverlapsBegin(ReceivingChunk& overlapped, - SSRingIndex expected_offset); - static void FixChunkOverlapsEnd(ReceivingChunk& overlapped, - SSRingIndex expected_end); - static void MergeChunkProperties(ReceivingChunk& chunk, - ReceivingChunk& overlapped); - - std::vector> - GetContinuousChunkChain(SSRingIndex start_chunks); - - std::vector chunks_; -}; -} // namespace ae - -#endif // AETHER_STREAM_API_SAFE_STREAM_RECEIVING_CHUNK_LIST_H_ diff --git a/aether/stream_api/safe_stream/safe_stream_api.cpp b/aether/stream_api/safe_stream/safe_stream_api.cpp deleted file mode 100644 index cbe35a68..00000000 --- a/aether/stream_api/safe_stream/safe_stream_api.cpp +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2024 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "aether/stream_api/safe_stream/safe_stream_api.h" - -#include - -namespace ae { -SafeStreamApi::SafeStreamApi(ProtocolContext& protocol_context, - SafeStreamApiImpl& safe_stream_api_impl) - : ApiClassImpl{protocol_context}, - ack{protocol_context}, - request_repeat{protocol_context}, - send{protocol_context}, - safe_stream_api_impl_{&safe_stream_api_impl} {} - -void SafeStreamApi::AckImpl(SSRingIndex::type offset) { - safe_stream_api_impl_->Ack(offset); -} - -void SafeStreamApi::RequestRepeatImpl(SSRingIndex::type offset) { - safe_stream_api_impl_->RequestRepeat(offset); -} - -void SafeStreamApi::SendImpl(SSRingIndex::type begin_offset, - DataMessage data_message) { - safe_stream_api_impl_->Send(begin_offset, std::move(data_message)); -} - -} // namespace ae diff --git a/aether/stream_api/safe_stream/safe_stream_api.h b/aether/stream_api/safe_stream/safe_stream_api.h deleted file mode 100644 index 58a998b4..00000000 --- a/aether/stream_api/safe_stream/safe_stream_api.h +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2024 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AETHER_STREAM_API_SAFE_STREAM_SAFE_STREAM_API_H_ -#define AETHER_STREAM_API_SAFE_STREAM_SAFE_STREAM_API_H_ - -#include - -#include "aether/api_protocol/api_method.h" -#include "aether/api_protocol/api_class_impl.h" -#include "aether/stream_api/safe_stream/safe_stream_types.h" - -namespace ae { -class SafeStreamApiImpl { - public: - virtual ~SafeStreamApiImpl() = default; - virtual void Ack(SSRingIndex::type offset) = 0; - virtual void RequestRepeat(SSRingIndex::type offset) = 0; - virtual void Send(SSRingIndex::type begin_offset, - DataMessage data_message) = 0; -}; - -class SafeStreamApi final : public ApiClassImpl { - public: - explicit SafeStreamApi(ProtocolContext& protocol_context, - SafeStreamApiImpl& safe_stream_api_impl); - - Method<3, void(SSRingIndex::type offset)> ack; - Method<4, void(SSRingIndex::type offset)> request_repeat; - Method<5, void(SSRingIndex::type begin_offset, DataMessage data_message)> - send; - - void AckImpl(SSRingIndex::type offset); - void RequestRepeatImpl(SSRingIndex::type offset); - void SendImpl(SSRingIndex::type begin_offset, DataMessage data_message); - - AE_METHODS(RegMethod<3, &SafeStreamApi::AckImpl>, - RegMethod<4, &SafeStreamApi::RequestRepeatImpl>, - RegMethod<5, &SafeStreamApi::SendImpl>); - - private: - SafeStreamApiImpl* safe_stream_api_impl_; -}; - -} // namespace ae - -#endif // AETHER_STREAM_API_SAFE_STREAM_SAFE_STREAM_API_H_ diff --git a/aether/stream_api/safe_stream/safe_stream_recv_action.cpp b/aether/stream_api/safe_stream/safe_stream_recv_action.cpp deleted file mode 100644 index 5fd4cba3..00000000 --- a/aether/stream_api/safe_stream/safe_stream_recv_action.cpp +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright 2025 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "aether/stream_api/safe_stream/safe_stream_recv_action.h" - -#include -#include - -#include "aether/tele/tele.h" - -namespace ae { -SafeStreamRecvAction::SafeStreamRecvAction(AeContext const& ae_context, - ISendAckRepeat& send_confirm_repeat, - SafeStreamConfig const& config) - : Action{FromAeContext(ae_context)}, - send_confirm_repeat_{&send_confirm_repeat}, - send_ack_timeout_{config.send_ack_timeout}, - send_repeat_timeout_{config.send_repeat_timeout}, - window_size_{config.window_size}, - acknowledgement_req_{false} {} - -UpdateStatus SafeStreamRecvAction::Update(TimePoint current_time) { - CheckCompletedChains(); - return UpdateStatus::Merge(CheckAcknowledgement(current_time), - CheckMissing(current_time)); -} - -void SafeStreamRecvAction::PushData(SSRingIndex begin, - DataMessage data_message) { - AE_TELED_DEBUG( - "Data received begin offset: {}, delta: {}, repeat {}, reset {}, size {}", - begin, data_message.delta_offset, - static_cast(data_message.repeat_count()), data_message.reset(), - data_message.data.size()); - - // The first packet received, sync with the sender - if (!begin_) { - AE_TELED_DEBUG("Init receiver"); - session_start_ = begin; - begin_ = begin; - last_emitted_ = *begin_; - // The sender sent first packet, sync with the sender - } else if (data_message.reset() && (session_start_ != begin)) { - AE_TELED_DEBUG("Reset receiver"); - session_start_ = begin; - begin_ = begin; - last_emitted_ = *begin_; - chunks_.Clear(); - sent_ack_time_.reset(); - repeat_request_time_.reset(); - acknowledgement_req_ = false; - } - - auto received_offset = begin + data_message.delta_offset; - HandleData(received_offset, data_message.repeat_count(), - std::move(data_message.data)); -} - -SafeStreamRecvAction::ReceiveEvent::Subscriber -SafeStreamRecvAction::receive_event() { - return EventSubscriber{receive_event_}; -} - -void SafeStreamRecvAction::HandleData(SSRingIndex received_offset, - std::uint8_t repeat_count, - DataBuffer&& data) { - assert(begin_); - Action::Trigger(); - - auto res = chunks_.AddChunk( - ReceivingChunk{received_offset, std::move(data), repeat_count}); - switch (res) { - case ReceiveChunkList::AddResult::kDuplicate: { - AE_TELED_DEBUG("Received duplicate"); - break; - } - default: - acknowledgement_req_ = true; - break; - } -} - -void SafeStreamRecvAction::CheckCompletedChains() { - if (!session_start_) { - return; - } - auto joined_chunk = chunks_.ReceiveChunk(last_emitted_); - if (joined_chunk) { - last_emitted_ = joined_chunk->offset_range().right + 1; - chunks_.Acknowledge(last_emitted_ - window_size_, last_emitted_); - acknowledgement_req_ = true; - - AE_TELED_DEBUG("Emit received data range: {}-{}, last_emitted_: {}", - joined_chunk->offset_range().left, - joined_chunk->offset_range().right, last_emitted_); - receive_event_.Emit(std::move(joined_chunk->data)); - } -} - -UpdateStatus SafeStreamRecvAction::CheckAcknowledgement( - TimePoint current_time) { - if (!acknowledgement_req_) { - sent_ack_time_.reset(); - return {}; - } - - if (!sent_ack_time_) { - sent_ack_time_ = current_time + send_ack_timeout_; - } - - if (*sent_ack_time_ > current_time) { - return UpdateStatus::Delay(*sent_ack_time_); - } - sent_ack_time_.reset(); - acknowledgement_req_ = false; - - SendAcknowledgement(last_emitted_); - Action::Trigger(); - - return {}; -} - -UpdateStatus SafeStreamRecvAction::CheckMissing(TimePoint current_time) { - if (chunks_.empty()) { - repeat_request_time_.reset(); - return {}; - } - - if (!repeat_request_time_) { - repeat_request_time_ = current_time + send_repeat_timeout_; - } - - if (*repeat_request_time_ > current_time) { - return UpdateStatus::Delay(*repeat_request_time_); - } - repeat_request_time_.reset(); - - auto missed = chunks_.FindMissedChunks(last_emitted_); - - if (missed.empty()) { - return {}; - } - - auto min = std::min_element( - missed.begin(), missed.end(), [](const auto& left, const auto& right) { - return left.expected_offset.IsBefore(right.expected_offset); - }); - - AE_TELED_DEBUG( - "Request to repeat offset: {} for last_emitted: {} next_saved: {}", - min->expected_offset, last_emitted_, min->chunk->offset); - - SendRequestRepeat(min->expected_offset); - - Action::Trigger(); - return {}; -} - -void SafeStreamRecvAction::SendAcknowledgement(SSRingIndex offset) { - AE_TELED_DEBUG("Send acknowledgement for offset {}", offset); - send_confirm_repeat_->SendAck(offset); -} - -void SafeStreamRecvAction::SendRequestRepeat(SSRingIndex offset) { - AE_TELED_DEBUG("Send request repeat for offset {}", offset); - send_confirm_repeat_->SendRepeatRequest(offset); -} - -} // namespace ae diff --git a/aether/stream_api/safe_stream/safe_stream_recv_action.h b/aether/stream_api/safe_stream/safe_stream_recv_action.h deleted file mode 100644 index 5cca45dc..00000000 --- a/aether/stream_api/safe_stream/safe_stream_recv_action.h +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2025 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AETHER_STREAM_API_SAFE_STREAM_SAFE_STREAM_RECV_ACTION_H_ -#define AETHER_STREAM_API_SAFE_STREAM_SAFE_STREAM_RECV_ACTION_H_ - -#include - -#include "aether/common.h" -#include "aether/events/events.h" -#include "aether/actions/action.h" -#include "aether/actions/action_context.h" -#include "aether/stream_api/safe_stream/safe_stream_types.h" -#include "aether/stream_api/safe_stream/safe_stream_config.h" -#include "aether/stream_api/safe_stream/receiving_chunk_list.h" - -namespace ae { -class ISendAckRepeat { - public: - virtual ~ISendAckRepeat() = default; - - virtual void SendAck(SSRingIndex offset) = 0; - virtual void SendRepeatRequest(SSRingIndex offset) = 0; -}; - -class SafeStreamRecvAction : public Action { - public: - using ReceiveEvent = Event; - - SafeStreamRecvAction(AeContext const& ae_context, - ISendAckRepeat& send_confirm_repeat, - SafeStreamConfig const& config); - - AE_CLASS_NO_COPY_MOVE(SafeStreamRecvAction) - - UpdateStatus Update(TimePoint current_time); - - void PushData(SSRingIndex begin, DataMessage data_message); - - ReceiveEvent::Subscriber receive_event(); - - private: - void HandleData(SSRingIndex received_offset, std::uint8_t repeat_count, - DataBuffer&& data); - - void CheckCompletedChains(); - UpdateStatus CheckAcknowledgement(TimePoint current_time); - UpdateStatus CheckMissing(TimePoint current_time); - void SendAcknowledgement(SSRingIndex offset); - void SendRequestRepeat(SSRingIndex offset); - - ISendAckRepeat* send_confirm_repeat_; - - std::optional session_start_; //< current session start offset - std::optional begin_; //< last data offset confirmed - SSRingIndex last_emitted_; //< last data offset emitted - ReceiveChunkList chunks_; - - Duration send_ack_timeout_; - Duration send_repeat_timeout_; - SSRingIndex::type window_size_; - - std::optional sent_ack_time_; - std::optional repeat_request_time_; - bool acknowledgement_req_; - - ReceiveEvent receive_event_; -}; -} // namespace ae - -#endif // AETHER_STREAM_API_SAFE_STREAM_SAFE_STREAM_RECV_ACTION_H_ diff --git a/aether/stream_api/safe_stream/safe_stream_send_action.cpp b/aether/stream_api/safe_stream/safe_stream_send_action.cpp deleted file mode 100644 index 0e2874b3..00000000 --- a/aether/stream_api/safe_stream/safe_stream_send_action.cpp +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright 2025 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "aether/stream_api/safe_stream/safe_stream_send_action.h" - -#include - -#include "aether/tele/tele.h" - -namespace ae { -namespace safe_stream_send_action_internal { -SSRingIndex RandomOffset() { - // set seed once - static bool const seed = - (std::srand(static_cast(time(nullptr))), true); - (void)seed; - auto value = std::rand(); - return SSRingIndex{static_cast(value)}; -} -} // namespace safe_stream_send_action_internal - -SafeStreamSendAction::SafeStreamSendAction(AeContext const& ae_context, - ISendDataPush& send_data_push, - SafeStreamConfig const& config) - : Action{FromAeContext(ae_context)}, - send_data_push_{&send_data_push}, - begin_{safe_stream_send_action_internal::RandomOffset()}, - last_sent_{begin_}, - last_added_{begin_}, - init_state_{true}, - max_repeat_count_{config.max_repeat_count}, - max_payload_size_{0}, - window_size_{config.window_size}, - send_data_buffer_{FromAeContext(ae_context)} { - // add wait ack timeout as base value for response statistics - response_statistics_.Add(config.wait_ack_timeout); -} - -UpdateStatus SafeStreamSendAction::Update(TimePoint current_time) { - SendChunk(current_time); - return SendTimeouts(current_time); -} - -bool SafeStreamSendAction::Acknowledge(SSRingIndex confirm_offset) { - AE_TELED_DEBUG("Receive ack for offset {}", confirm_offset); - if (begin_.IsAfter(confirm_offset)) { - return false; - } - init_state_ = false; - - if (!sending_chunks_.empty()) { - auto response_duration = std::chrono::duration_cast( - Now() - sending_chunks_.front().send_time); - AE_TELED_DEBUG("Response duration is {:%S}", response_duration); - response_statistics_.Add(response_duration); - } - - sending_chunks_.RemoveUpTo(confirm_offset); - send_data_buffer_.Acknowledge(confirm_offset); - begin_ = confirm_offset; - Action::Trigger(); - return true; -} - -void SafeStreamSendAction::RequestRepeat(SSRingIndex request_offset) { - if (last_sent_.IsBefore(request_offset)) { - AE_TELED_DEBUG("Request repeat send for not sent offset {}", - request_offset); - return; - } - last_sent_ = request_offset; - Action::Trigger(); -} - -void SafeStreamSendAction::SetMaxPayload(std::size_t max_payload_size) { - AE_TELED_DEBUG("Set max payload to {}", max_payload_size); - max_payload_size_ = static_cast(max_payload_size); -} - -ActionPtr SafeStreamSendAction::SendData(DataBuffer&& data) { - auto data_size = data.size(); - auto sending_data = SendingData{last_added_, std::move(data)}; - last_added_ += static_cast(data_size); - - auto send_action = send_data_buffer_.AddData(std::move(sending_data)); - sending_data_subs_.Push( - // stop data chunks sending if main action is stopped - send_action->stop_event().Subscribe([this](auto const& sending_data) { - auto removed_size = send_data_buffer_.Stop(sending_data.offset); - last_added_ -= static_cast(removed_size); - })); - return send_action; -} - -void SafeStreamSendAction::SendChunk(TimePoint current_time) { - if (max_payload_size_ == 0) { - return; - } - - auto data_chunk = send_data_buffer_.GetSlice(last_sent_, max_payload_size_); - if (data_chunk.data.empty()) { - // no data to send - return; - } - auto delta = begin_.Distance(data_chunk.offset); - auto delta_end = - delta + static_cast(data_chunk.data.size()); - if (delta_end > window_size_) { - AE_TELED_DEBUG("Window size exceeded begin: {} last_sent: {} delta end: {}", - begin_, last_sent_, delta_end); - return; - } - AE_TELED_DEBUG("Sending chunk begin: {}, last_sent_: {},offset: {} size: {}", - begin_, last_sent_, data_chunk.offset, data_chunk.data.size()); - last_sent_ = begin_ + delta_end; - - auto& send_chunk = sending_chunks_.Register( - data_chunk.offset, - data_chunk.offset + - static_cast(data_chunk.data.size() - 1), - current_time); - - auto repeat_count = send_chunk.repeat_count; - send_chunk.repeat_count++; - if (send_chunk.repeat_count > max_repeat_count_) { - AE_TELED_ERROR("Repeat count exceeded"); - RejectSend(send_chunk); - return; - } - - auto end_offset = data_chunk.offset + - static_cast(data_chunk.data.size()); - - auto& write_action = - PushData(std::move(data_chunk.data), delta, repeat_count); - - send_subs_.Push( - write_action.status_event().Subscribe([this, end_offset](auto status) { - switch (status) { - case WriteAction::Status::kSuccess: - break; - case WriteAction::Status::kFail: - sending_chunks_.RemoveUpTo(end_offset); - send_data_buffer_.Reject(end_offset); - break; - case WriteAction::Status::kStop: - sending_chunks_.RemoveUpTo(end_offset); - send_data_buffer_.Stop(end_offset); - break; - } - })); -} - -void SafeStreamSendAction::RejectSend(SendingChunk& sending_chunk) { - auto end_offset = sending_chunk.offset_range.right; - sending_chunks_.RemoveUpTo(end_offset); - send_data_buffer_.Reject(end_offset); -} - -UpdateStatus SafeStreamSendAction::SendTimeouts(TimePoint current_time) { - if (sending_chunks_.empty()) { - return {}; - } - - auto const& selected_sch = sending_chunks_.front(); - // wait timeout is depends on repeat_count and response statistics - auto wait_ack_timeout = - static_cast(response_statistics_.percentile<99>().count()); - auto increase_factor = std::max( - 1.0, (AE_SAFE_STREAM_RTO_GROW_FACTOR * (selected_sch.repeat_count - 1))); - auto wait_timeout = - Duration{static_cast(wait_ack_timeout * increase_factor)}; - - auto wait_time = selected_sch.send_time + wait_timeout; - if (wait_time <= current_time) { - // timeout - AE_TELED_DEBUG("Wait ack timeout {:%S}, repeat offset {}", wait_timeout, - selected_sch.offset_range.left); - // move offset to repeat send - last_sent_ = selected_sch.offset_range.left; - Action::Trigger(); - return {}; - } - - return UpdateStatus::Delay(wait_time); -} - -WriteAction& SafeStreamSendAction::PushData(DataBuffer&& data_buffer, - SSRingIndex::type delta, - std::uint8_t repeat_count) { - AE_TELED_DEBUG( - "Send data message begin offset {} delta {} repeat count {} reset {} " - "data size {}", - begin_, delta, static_cast(repeat_count), init_state_, - - data_buffer.size()); - assert(delta < window_size_); - - return send_data_push_->PushData(begin_, - DataMessage{ - repeat_count, - init_state_, - static_cast(delta), - std::move(std::move(data_buffer)), - }); -} - -} // namespace ae diff --git a/aether/stream_api/safe_stream/safe_stream_send_action.h b/aether/stream_api/safe_stream/safe_stream_send_action.h deleted file mode 100644 index fbac248a..00000000 --- a/aether/stream_api/safe_stream/safe_stream_send_action.h +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2025 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AETHER_STREAM_API_SAFE_STREAM_SAFE_STREAM_SEND_ACTION_H_ -#define AETHER_STREAM_API_SAFE_STREAM_SAFE_STREAM_SEND_ACTION_H_ - -#include "aether/common.h" -#include "aether/config.h" -#include "aether/actions/action.h" -#include "aether/actions/action_context.h" -#include "aether/types/statistic_counter.h" -#include "aether/events/multi_subscription.h" -#include "aether/write_action/write_action.h" -#include "aether/stream_api/safe_stream/send_data_buffer.h" -#include "aether/stream_api/safe_stream/safe_stream_types.h" -#include "aether/stream_api/safe_stream/safe_stream_config.h" -#include "aether/stream_api/safe_stream/sending_chunk_list.h" - -namespace ae { -class ISendDataPush { - public: - virtual ~ISendDataPush() = default; - virtual WriteAction& PushData(SSRingIndex begin, - DataMessage&& data_message) = 0; -}; - -class SafeStreamSendAction : public Action { - public: - using ResponseStatistics = - StatisticsCounter; - - SafeStreamSendAction(AeContext const& ae_context, - ISendDataPush& send_data_push, - SafeStreamConfig const& config); - - AE_CLASS_NO_COPY_MOVE(SafeStreamSendAction) - - UpdateStatus Update(TimePoint current_time); - - bool Acknowledge(SSRingIndex confirm_offset); - void RequestRepeat(SSRingIndex request_offset); - - void SetMaxPayload(std::size_t max_payload_size); - - ActionPtr SendData(DataBuffer&& data); - - private: - void SendChunk(TimePoint current_time); - void Send(std::uint16_t repeat_count, DataChunk&& data_chunk, - TimePoint current_time); - void RejectSend(SendingChunk& sending_chunk); - UpdateStatus SendTimeouts(TimePoint current_time); - - WriteAction& PushData(DataBuffer&& data_buffer, SSRingIndex::type delta, - std::uint8_t repeat_count); - - ISendDataPush* send_data_push_; - - SSRingIndex begin_; //< begin data offset - SSRingIndex last_sent_; //< last offset for last sent data - SSRingIndex last_added_; //< last offset for last sent data - bool init_state_; - - std::uint8_t max_repeat_count_; - SSRingIndex::type max_payload_size_; - SSRingIndex::type window_size_; - - SendDataBuffer send_data_buffer_; - SendingChunkList sending_chunks_; - - MultiSubscription sending_data_subs_; - MultiSubscription send_subs_; - ResponseStatistics response_statistics_; -}; -} // namespace ae - -#endif // AETHER_STREAM_API_SAFE_STREAM_SAFE_STREAM_SEND_ACTION_H_ diff --git a/aether/stream_api/safe_stream/safe_stream_types.h b/aether/stream_api/safe_stream/safe_stream_types.h deleted file mode 100644 index ab48d975..00000000 --- a/aether/stream_api/safe_stream/safe_stream_types.h +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright 2024 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AETHER_STREAM_API_SAFE_STREAM_SAFE_STREAM_TYPES_H_ -#define AETHER_STREAM_API_SAFE_STREAM_SAFE_STREAM_TYPES_H_ - -#include - -#include "aether/common.h" -#include "aether/types/ring_buffer.h" -#include "aether/types/data_buffer.h" - -namespace ae { - -struct SafeStreamInit { - std::uint16_t offset; - std::uint16_t window_size; - std::uint16_t max_packet_size; - - AE_REFLECT_MEMBERS(offset, window_size, max_packet_size) -}; - -using SSRingIndex = RingIndex; - -struct OffsetRange { - SSRingIndex left; - SSRingIndex right; - - // offset in [begin:end] range - constexpr bool InRange(SSRingIndex offset) const { - return ((left == offset) || left.IsBefore(offset)) && - ((right == offset) || right.IsAfter(offset)); - } - - // offset range is before offset ( end < offset) - constexpr bool IsBefore(SSRingIndex offset) const { - return right.IsBefore(offset); - } - - // offset range is after offset ( begin > offset) - constexpr bool IsAfter(SSRingIndex offset) const { - return left.IsAfter(offset); - } - - constexpr auto distance() const { return left.Distance(right); } - - constexpr bool operator==(const OffsetRange& other) const { - return (left == other.left) && (right == other.right); - } - - constexpr bool operator!=(const OffsetRange& other) const { - return !(*this == other); - } -}; - -/** - * \brief The data message used for sending data by Safe Stream Api. - */ -struct DataMessage { - DataMessage() = default; - DataMessage(std::uint8_t repeat_count, bool reset, std::uint16_t delta, - DataBuffer&& d) - : control{static_cast(repeat_count & 0x1F), - (reset ? std::uint8_t{0x01} : std::uint8_t{0x00}), - {}}, - delta_offset{delta}, - data{std::move(d)} {} - - AE_CLASS_COPY_MOVE(DataMessage) - - AE_REFLECT(ae::reflect::reflect_internal::FieldGetter{}, - AE_MMBRS(delta_offset, data)) - - std::uint8_t repeat_count() const { - return static_cast(control.repeat_count); - } - bool reset() const { return control.reset != 0; } - - struct Control { - static std::uint8_t* get(DataMessage* obj) { - return reinterpret_cast(&obj->control); - } - - std::uint8_t repeat_count : 5; - std::uint8_t reset : 1; - std::uint8_t reserved : 2; - } control{}; // control flags related to message - std::uint16_t delta_offset{}; // data offset form sender's buffer begin - DataBuffer data; -}; - -/** - * \brief Sending data stored in sending data buffer - */ -struct SendingData { - SendingData(SSRingIndex off, DataBuffer&& d) - : offset(off), - data(std::move(d)), - begin{std::begin(data)}, - end{std::end(data)} {} - - auto offset_range() const { - auto distance = end - begin; - return OffsetRange{offset, - offset + static_cast(distance - 1)}; - } - - std::size_t size() const { return static_cast(end - begin); } - - SSRingIndex offset; - DataBuffer data; - DataBuffer::iterator begin; - DataBuffer::iterator end; -}; - -/** - * \brief Saved information about sending chunk - */ -struct SendingChunk { - OffsetRange offset_range; - std::uint8_t repeat_count; - TimePoint send_time; -}; - -/** - * \brief The data chunk retrieved from sending data buffer to send - */ -struct DataChunk { - DataBuffer data; - SSRingIndex offset; -}; - -/** - * \brief Received chunk of data stored in receiving chunk list - */ -struct ReceivingChunk { - ReceivingChunk(SSRingIndex off, DataBuffer&& d, std::uint8_t rc) - : data{std::move(d)}, - repeat_count{rc}, - offset{off}, - begin{std::begin(data)}, - end{std::end(data)} {} - - auto offset_range() const { - auto distance = end - begin; - return OffsetRange{offset, - offset + static_cast(distance - 1)}; - } - - DataBuffer data; - std::uint8_t repeat_count; - SSRingIndex offset; - DataBuffer::iterator begin; - DataBuffer::iterator end; -}; - -/** - * \brief Data chunk missed in receiving chunk list - */ -struct MissedChunk { - SSRingIndex expected_offset; - ReceivingChunk* chunk; -}; - -struct ChunkAcknowledge { - OffsetRange offset_range; - std::uint8_t repeat_count; - bool acknowledged; -}; - -} // namespace ae - -#endif // AETHER_STREAM_API_SAFE_STREAM_SAFE_STREAM_TYPES_H_ diff --git a/aether/stream_api/safe_stream/send_data_buffer.cpp b/aether/stream_api/safe_stream/send_data_buffer.cpp deleted file mode 100644 index 4cb888ba..00000000 --- a/aether/stream_api/safe_stream/send_data_buffer.cpp +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2024 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "aether/stream_api/safe_stream/send_data_buffer.h" - -#include -#include - -#include "aether/tele/tele.h" - -namespace ae { -SendDataBuffer::SendDataBuffer(ActionContext action_context) - : action_context_{action_context}, buffer_size_{} {} - -ActionPtr SendDataBuffer::AddData(SendingData&& data) { - AE_TELED_DEBUG("Add data size {} with offset {}", data.data.size(), - data.offset); - buffer_size_ += data.data.size(); - - auto& action = send_actions_.emplace_back(action_context_, std::move(data)); - return action; -} - -DataChunk SendDataBuffer::GetSlice(SSRingIndex offset, std::size_t max_size) { - using DataDiffType = DataBuffer::difference_type; - - // find the first sending data with required offset - auto it = std::find_if(std::begin(send_actions_), std::end(send_actions_), - [offset](auto& action) { - auto& sending_data = action->sending_data(); - return sending_data.offset_range().InRange(offset) || - sending_data.offset_range().IsAfter(offset); - }); - - if (it == std::end(send_actions_)) { - return {}; - } - - // select current offset for that chunk. - // offset may overlap with sending data offset range - auto current_offset = ((*it)->sending_data().offset.IsAfter(offset)) - ? (*it)->sending_data().offset - : offset; - DataChunk chunk{{}, current_offset}; - chunk.data.reserve(max_size); - - std::size_t remaining = max_size; - - // collect data from the continuous list of sending data - for (; (it != std::end(send_actions_)) && (remaining > 0); ++it) { - auto& sending_action = *it; - // sending action now in Sending state - sending_action->Sending(); - - auto const& sending_data = sending_action->sending_data(); - // Select data begin, copy size and distance from sending data offset to - // current offset and remaining size - auto distance = - static_cast(sending_data.offset.Distance(current_offset)); - auto data_begin = std::next(sending_data.begin, distance); - auto data_size = std::min( - sending_data.size() - static_cast(distance), remaining); - - // copy data to the chunk - std::copy_n(data_begin, data_size, std::back_inserter(chunk.data)); - current_offset += static_cast(data_size); - remaining -= data_size; - } - - return chunk; -} - -std::size_t SendDataBuffer::Acknowledge(SSRingIndex offset) { - std::size_t removed_size = 0; - // iterate and remove acknowledged actions - send_actions_.erase( - std::remove_if(std::begin(send_actions_), std::end(send_actions_), - [&removed_size, offset](auto& action) { - auto const& sending_data = action->sending_data(); - auto offset_range = sending_data.offset_range(); - // check if sending data offset range either is before - // offset or offset is in range - if (offset_range.IsBefore(offset) || - offset_range.InRange(offset - 1)) { - auto size_before = sending_data.size(); - auto res = action->Acknowledge(offset); - // update removed size with delta between before and - // after ack - removed_size += (size_before - sending_data.size()); - return res; - } - return false; - }), - std::end(send_actions_)); - - buffer_size_ -= removed_size; - return removed_size; -} - -std::size_t SendDataBuffer::Reject(SSRingIndex offset) { - std::size_t removed_size = 0; - // iterate and remove the sending actions fitting to the offset - send_actions_.erase( - std::remove_if(std::begin(send_actions_), std::end(send_actions_), - [&removed_size, offset](auto& action) { - auto const& sending_data = action->sending_data(); - auto offset_range = sending_data.offset_range(); - if (offset_range.IsBefore(offset) || - offset_range.InRange(offset - 1)) { - removed_size += sending_data.size(); - action->Failed(); - return true; - } - return false; - }), - std::end(send_actions_)); - - buffer_size_ -= removed_size; - return removed_size; -} - -std::size_t SendDataBuffer::Stop(SSRingIndex offset) { - // Stop means the sending action should not be sent like it was never added. - // find the action to stop - auto it = std::find_if(std::begin(send_actions_), std::end(send_actions_), - [offset](auto& action) { - auto& sending_data = action->sending_data(); - return sending_data.offset == offset; - }); - if (it == std::end(send_actions_)) { - return 0; - } - - (*it)->Stopped(); - auto const& sending_data = (*it)->sending_data(); - buffer_size_ -= sending_data.size(); - // move the other sending actions on the current place - auto current_offset = sending_data.offset; - for (auto fix_it = std::next(it); fix_it != std::end(send_actions_); - ++fix_it) { - // set new offset to next chunk - (*fix_it)->UpdateOffset(current_offset); - auto const& sd = (*fix_it)->sending_data(); - current_offset = sd.offset_range().right + 1; - } - send_actions_.erase(it); - return sending_data.data.size(); -} -} // namespace ae diff --git a/aether/stream_api/safe_stream/send_data_buffer.h b/aether/stream_api/safe_stream/send_data_buffer.h deleted file mode 100644 index 65b72cda..00000000 --- a/aether/stream_api/safe_stream/send_data_buffer.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2024 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AETHER_STREAM_API_SAFE_STREAM_SEND_DATA_BUFFER_H_ -#define AETHER_STREAM_API_SAFE_STREAM_SEND_DATA_BUFFER_H_ - -#include -#include - -#include "aether/actions/action_ptr.h" -#include "aether/actions/action_context.h" -#include "aether/stream_api/safe_stream/safe_stream_types.h" -#include "aether/stream_api/safe_stream/sending_data_action.h" - -namespace ae { -class SendDataBuffer { - public: - explicit SendDataBuffer(ActionContext action_context); - - /** - * \brief Add new data to the sending buffer. - * \param data - Sending data to send. - * \return new view to SendingDataAction - */ - ActionPtr AddData(SendingData&& data); - /** - * \brief Get the slice of data to send. - * \param offset - the begin offset from which to start the slice. - * \param max_size - the maximum size of the slice. - * \return DataChunk containing the slice of data and its offset. - */ - DataChunk GetSlice(SSRingIndex offset, std::size_t max_size); - - /** - * \brief Acknowledge data up to offset. - * All the SendingDataAction fully acknowledged will through the result event. - */ - std::size_t Acknowledge(SSRingIndex offset); - /** - * \brief Reject data from sending. - * This removes all the sending data with less or overlapped offset range. - * This leads to SendingDataAction through the error event. - */ - std::size_t Reject(SSRingIndex offset); - /** - * \brief Stop data from sending. - * This removes all the sending data with less or overlapped offset range. - * This leads to SendingDataAction through the stop event. - */ - std::size_t Stop(SSRingIndex offset); - - /** - * \brief Get current buffer size in bytes. - */ - std::size_t size() const { return buffer_size_; } - - private: - ActionContext action_context_; - - // view store used for iteration over sending data - std::vector> send_actions_; - - std::size_t buffer_size_; -}; - -} // namespace ae - -#endif // AETHER_STREAM_API_SAFE_STREAM_SEND_DATA_BUFFER_H_ diff --git a/aether/stream_api/safe_stream/sending_chunk_list.cpp b/aether/stream_api/safe_stream/sending_chunk_list.cpp deleted file mode 100644 index e105a1fe..00000000 --- a/aether/stream_api/safe_stream/sending_chunk_list.cpp +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2024 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "aether/stream_api/safe_stream/sending_chunk_list.h" - -#include -#include - -namespace ae { - -SendingChunk& SendingChunkList::Register(SSRingIndex begin, SSRingIndex end, - TimePoint send_time) { - auto offset_range = OffsetRange{begin, end}; - - // find the chunk with the same or overlapped offset - // and if there is no one emplace it at the end of list - auto it = std::find_if(std::begin(chunks_), std::end(chunks_), - [&](auto const& sch) { - return offset_range.InRange(sch.offset_range.left); - }); - - if (it == std::end(chunks_)) { - auto& ref = chunks_.emplace_back(SendingChunk{{begin, end}, {}, send_time}); - return ref; - } - - // there is a chunk with the same or overlapped offset - // if it's the same chunk, move it to the end - if ((offset_range == it->offset_range)) { - // move it to the end - auto sch = *it; - sch.send_time = send_time; - chunks_.erase(it); - chunks_.emplace_back(sch); - return chunks_.back(); - } - - // !the worst case - // chunk's offsets is overlapped - // create a new chunk at the end of list and merge to it all the chunks with - // overlapping offsets - auto new_sch = SendingChunk{{begin, end}, it->repeat_count, send_time}; - - // modify any overlapping chunks - chunks_.erase( - std::remove_if( - std::begin(chunks_), std::end(chunks_), - [&](auto& chunk) { - if (offset_range.InRange(chunk.offset_range.left)) { - new_sch.repeat_count = - std::min(new_sch.repeat_count, chunk.repeat_count); - - if (offset_range.InRange(chunk.offset_range.right)) { - // remove this fully overlapped chunk - return true; - } - chunk.offset_range.left = offset_range.right + 1; - } else if (offset_range.InRange(chunk.offset_range.right)) { - new_sch.repeat_count = - std::min(new_sch.repeat_count, chunk.repeat_count); - chunk.offset_range.right = offset_range.left - 1; - } - return false; - }), - std::end(chunks_)); - - return chunks_.emplace_back(new_sch); -} - -void SendingChunkList::RemoveUpTo(SSRingIndex offset) { - chunks_.erase( - std::remove_if( - std::begin(chunks_), std::end(chunks_), - [&](auto& sch) { - if (sch.offset_range.IsBefore(offset)) { - return true; - } - if (sch.offset_range.InRange(offset)) { - sch.offset_range.left = offset; - // if chunk is collapsed - return sch.offset_range.left.IsAfter(sch.offset_range.right) || - (sch.offset_range.left == sch.offset_range.right); - } - return false; - }), - std::end(chunks_)); -} -} // namespace ae diff --git a/aether/stream_api/safe_stream/sending_chunk_list.h b/aether/stream_api/safe_stream/sending_chunk_list.h deleted file mode 100644 index 2e04dec1..00000000 --- a/aether/stream_api/safe_stream/sending_chunk_list.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2024 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AETHER_STREAM_API_SAFE_STREAM_SENDING_CHUNK_LIST_H_ -#define AETHER_STREAM_API_SAFE_STREAM_SENDING_CHUNK_LIST_H_ - -#include - -#include "aether/clock.h" - -#include "aether/stream_api/safe_stream/safe_stream_types.h" - -namespace ae { -class SendingChunkList { - public: - explicit SendingChunkList() = default; - - /** - * \brief Register a new sending chunk. - * If chunk with that offset does not exist, it will be created at the end of - * the list. Otherwise, it will be moved to the end of the list and possibly - * merged with other chunks if offsets are overlaps. - */ - SendingChunk& Register(SSRingIndex begin, SSRingIndex end, - TimePoint send_time); - - /** - * \brief Remove all chunks up to the given offset. - */ - void RemoveUpTo(SSRingIndex offset); - - SendingChunk& front() { return chunks_.front(); } - bool empty() const { return chunks_.empty(); } - std::size_t size() const { return chunks_.size(); } - - private: - std::vector chunks_; -}; -} // namespace ae - -#endif // AETHER_STREAM_API_SAFE_STREAM_SENDING_CHUNK_LIST_H_ diff --git a/aether/stream_api/safe_stream/sending_data_action.cpp b/aether/stream_api/safe_stream/sending_data_action.cpp deleted file mode 100644 index ecefd1ce..00000000 --- a/aether/stream_api/safe_stream/sending_data_action.cpp +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2024 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "aether/stream_api/safe_stream/sending_data_action.h" - -#include "aether/tele/tele.h" - -namespace ae { -SendingDataAction::SendingDataAction(ActionContext action_context, - SendingData sending_data) - : Action{action_context}, - sending_data_{std::move(sending_data)}, - state_{State::kWaiting} {} - -UpdateStatus SendingDataAction::Update() { - if (state_.changed()) { - switch (state_.Acquire()) { - case State::kWaiting: - case State::kSending: - break; - case State::kDone: - return UpdateStatus::Result(); - case State::kStopped: - return UpdateStatus::Stop(); - case State::kFailed: - return UpdateStatus::Error(); - } - } - return {}; -} - -SendingData const& SendingDataAction::sending_data() const { - return sending_data_; -} - -EventSubscriber SendingDataAction::stop_event() { - return EventSubscriber{stop_event_}; -} - -void SendingDataAction::Stop() { - if (state_ == State::kSending) { - AE_TELED_ERROR("Unable to stop sending data action while sending"); - return; - } - stop_event_.Emit(sending_data_); -} - -bool SendingDataAction::Acknowledge(SSRingIndex offset) { - assert(sending_data_.offset.IsBefore(offset)); - - auto distance = - static_cast(sending_data_.offset.Distance(offset)); - auto size = sending_data_.end - sending_data_.begin; - - sending_data_.begin = - size > distance ? sending_data_.begin + distance : sending_data_.end; - - if (sending_data_.begin == sending_data_.end) { - state_ = State::kDone; - Action::Trigger(); - return true; - } - sending_data_.offset = offset; - - return false; -} - -void SendingDataAction::Sending() { - state_ = State::kSending; - Action::Trigger(); -} - -void SendingDataAction::Stopped() { - state_ = State::kStopped; - Action::Trigger(); -} - -void SendingDataAction::Failed() { - state_ = State::kFailed; - Action::Trigger(); -} - -SSRingIndex SendingDataAction::UpdateOffset(SSRingIndex offset) { - std::swap(sending_data_.offset, offset); - return offset; -} -} // namespace ae diff --git a/aether/stream_api/safe_stream/sending_data_action.h b/aether/stream_api/safe_stream/sending_data_action.h deleted file mode 100644 index 3005ed96..00000000 --- a/aether/stream_api/safe_stream/sending_data_action.h +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2024 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AETHER_STREAM_API_SAFE_STREAM_SENDING_DATA_ACTION_H_ -#define AETHER_STREAM_API_SAFE_STREAM_SENDING_DATA_ACTION_H_ - -#include "aether/events/events.h" -#include "aether/actions/action.h" -#include "aether/types/state_machine.h" -#include "aether/actions/action_context.h" - -#include "aether/stream_api/safe_stream/safe_stream_types.h" - -namespace ae { -class SendingDataAction : public Action { - enum class State : std::uint8_t { - kWaiting, - kSending, - kDone, - kStopped, - kFailed, - }; - - public: - using Action::Action; - - SendingDataAction(ActionContext action_context, SendingData data); - - UpdateStatus Update(); - - SendingData const& sending_data() const; - // The stop event is emitted by stop command. - EventSubscriber stop_event(); - - // command to stop the sending action - void Stop(); - - /** - * \brief Acknowledge part of the data till offset as acknowledged. - * If the whole sending_data_ is acknowledged, the action is marked as done. - * \param offset The offset to acknowledge. - * \return true if the whole sending_data_ is acknowledged. - */ - bool Acknowledge(SSRingIndex offset); - - // mark action as sending - void Sending(); - // mark action as stopped - void Stopped(); - // mark action as failed - void Failed(); - - /** - * \brief Update the offset of sending_data_ to new value. - * \param offset The new offset. - * \return Old value. - */ - SSRingIndex UpdateOffset(SSRingIndex offset); - - private: - SendingData sending_data_; - StateMachine state_; - Event stop_event_; -}; -} // namespace ae - -#endif // AETHER_STREAM_API_SAFE_STREAM_SENDING_DATA_ACTION_H_ diff --git a/examples/benches/send_message_delays/main.cpp b/examples/benches/send_message_delays/main.cpp index 0b574161..dd745d21 100644 --- a/examples/benches/send_message_delays/main.cpp +++ b/examples/benches/send_message_delays/main.cpp @@ -94,7 +94,6 @@ class TestSendMessageDelaysAction : public Action { void MakeTest() { AE_TELED_INFO("Make a test"); SafeStreamConfig safe_stream_config{ - std::numeric_limits::max(), (std::numeric_limits::max() / 2) - 1, (std::numeric_limits::max() / 2) - 1, 10, diff --git a/examples/benches/send_message_delays/receiver.h b/examples/benches/send_message_delays/receiver.h index b19b66d6..2e8310bc 100644 --- a/examples/benches/send_message_delays/receiver.h +++ b/examples/benches/send_message_delays/receiver.h @@ -20,11 +20,11 @@ #include "aether/memory.h" #include "aether/client.h" #include "aether/ae_context.h" +#include "aether/actions/action_ptr.h" #include "aether/events/event_subscription.h" #include "aether/events/multi_subscription.h" #include "aether/client_messages/p2p_message_stream.h" #include "aether/client_messages/p2p_safe_message_stream.h" -#include "aether/stream_api/safe_stream/safe_stream_config.h" #include "send_message_delays/timed_receiver.h" #include "send_message_delays/api/bench_delays_api.h" diff --git a/examples/benches/send_message_delays/sender.h b/examples/benches/send_message_delays/sender.h index f144cbe1..9765ddac 100644 --- a/examples/benches/send_message_delays/sender.h +++ b/examples/benches/send_message_delays/sender.h @@ -27,7 +27,6 @@ #include "aether/actions/action_ptr.h" #include "aether/client_messages/p2p_message_stream.h" #include "aether/client_messages/p2p_safe_message_stream.h" -#include "aether/stream_api/safe_stream/safe_stream_config.h" #include "send_message_delays/timed_sender.h" #include "send_message_delays/api/bench_delays_api.h" diff --git a/examples/cloud/cloud_test.cpp b/examples/cloud/cloud_test.cpp index 14d9926e..28b5687d 100644 --- a/examples/cloud/cloud_test.cpp +++ b/examples/cloud/cloud_test.cpp @@ -37,7 +37,6 @@ namespace ae::cloud_test { constexpr ae::SafeStreamConfig kSafeStreamConfig{ - std::numeric_limits::max(), // buffer_capacity (std::numeric_limits::max() / 2) - 1, // window_size (std::numeric_limits::max() / 2) - 1 - 1, // max_data_size 10, // max_repeat_count From 4b611b9792dc6f3c548dc899d225771391f6ea90 Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Thu, 23 Apr 2026 14:36:39 +0500 Subject: [PATCH 4/4] fix tests for safe stream --- tests/CMakeLists.txt | 1 + tests/test-safe-stream/CMakeLists.txt | 45 +++ tests/test-safe-stream/main.cpp | 44 +++ .../mock_bad_streams.cpp | 7 +- .../mock_bad_streams.h | 2 + tests/test-safe-stream/stream-test-ctx.h | 49 +++ .../test-safe-stream/test_circular_buffer.cpp | 152 ++++++++ .../test_receiving_chunks.cpp | 195 ++++++++++ .../test_safe_stream.cpp | 7 +- .../test_safe_stream_recv.cpp | 255 +++++++----- .../test_safe_stream_reliability.cpp | 44 ++- .../test_safe_stream_send.cpp | 364 ++++++++++++------ .../test_safe_stream_send_recv.cpp | 150 +++++--- .../test_sending_chunk_list.cpp | 121 ++++++ tests/test-stream/CMakeLists.txt | 12 +- tests/test-stream/main.cpp | 22 +- .../safe-stream/test_receiving_chunks.cpp | 279 -------------- .../safe-stream/test_safe_stream_types.cpp | 55 --- .../safe-stream/test_send_data_buffer.cpp | 232 ----------- .../safe-stream/test_sending_chunk_list.cpp | 123 ------ tests/test-stream/stream-test-ctx.h | 7 +- tests/test-stream/to_data_buffer.h | 6 + 22 files changed, 1158 insertions(+), 1014 deletions(-) create mode 100644 tests/test-safe-stream/CMakeLists.txt create mode 100644 tests/test-safe-stream/main.cpp rename tests/{test-stream => test-safe-stream}/mock_bad_streams.cpp (94%) rename tests/{test-stream => test-safe-stream}/mock_bad_streams.h (97%) create mode 100644 tests/test-safe-stream/stream-test-ctx.h create mode 100644 tests/test-safe-stream/test_circular_buffer.cpp create mode 100644 tests/test-safe-stream/test_receiving_chunks.cpp rename tests/{test-stream/safe-stream => test-safe-stream}/test_safe_stream.cpp (96%) rename tests/{test-stream/safe-stream => test-safe-stream}/test_safe_stream_recv.cpp (64%) rename tests/{test-stream/safe-stream => test-safe-stream}/test_safe_stream_reliability.cpp (81%) rename tests/{test-stream/safe-stream => test-safe-stream}/test_safe_stream_send.cpp (65%) rename tests/{test-stream/safe-stream => test-safe-stream}/test_safe_stream_send_recv.cpp (64%) create mode 100644 tests/test-safe-stream/test_sending_chunk_list.cpp delete mode 100644 tests/test-stream/safe-stream/test_receiving_chunks.cpp delete mode 100644 tests/test-stream/safe-stream/test_safe_stream_types.cpp delete mode 100644 tests/test-stream/safe-stream/test_send_data_buffer.cpp delete mode 100644 tests/test-stream/safe-stream/test_sending_chunk_list.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c12fd233..a3539cea 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -58,6 +58,7 @@ add_subdirectory(test-actions) add_subdirectory(test-events) add_subdirectory(test-transport) add_subdirectory(test-stream) +add_subdirectory(test-safe-stream) add_subdirectory(test-ptr) add_subdirectory(test-format) add_subdirectory(test-reflect) diff --git a/tests/test-safe-stream/CMakeLists.txt b/tests/test-safe-stream/CMakeLists.txt new file mode 100644 index 00000000..4b0d4d57 --- /dev/null +++ b/tests/test-safe-stream/CMakeLists.txt @@ -0,0 +1,45 @@ +# Copyright 2024 Aethernet Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required( VERSION 3.18 ) + +list(APPEND test_srcs + main.cpp + mock_bad_streams.cpp + test_circular_buffer.cpp + test_sending_chunk_list.cpp + test_receiving_chunks.cpp + test_safe_stream_send.cpp + test_safe_stream_recv.cpp + test_safe_stream_send_recv.cpp + test_safe_stream.cpp + test_safe_stream_reliability.cpp + ${CMAKE_CURRENT_LIST_DIR}/../test-object-system/map_domain_storage.cpp +) + +if(NOT CM_PLATFORM) + + project(test-safe-stream LANGUAGES CXX) + + add_executable(${PROJECT_NAME}) + target_sources(${PROJECT_NAME} PRIVATE ${test_srcs}) + # for aether + target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) + target_include_directories(${PROJECT_NAME} PRIVATE ${ROOT_DIR}) + target_link_libraries(${PROJECT_NAME} PRIVATE aether unity gcem) + + add_test(NAME ${PROJECT_NAME} COMMAND $) +else() + message(WARNING "Not implemented for ${CM_PLATFORM}") +endif() diff --git a/tests/test-safe-stream/main.cpp b/tests/test-safe-stream/main.cpp new file mode 100644 index 00000000..ea1a9147 --- /dev/null +++ b/tests/test-safe-stream/main.cpp @@ -0,0 +1,44 @@ +/* + * Copyright 2024 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "aether/tele/tele_init.h" + +void setUp() { ae::tele::TeleInit::Init(); } +void tearDown() {} + +extern int test_circular_buffer(); +extern int test_sending_chunk_list(); +extern int test_receiving_chunks(); +extern int test_safe_stream_send(); +extern int test_safe_stream_recv(); +extern int test_safe_stream_send_recv(); +extern int test_safe_stream(); +extern int test_safe_stream_reliability(); + +int main() { + int res = 0; + res += test_circular_buffer(); + res += test_sending_chunk_list(); + res += test_receiving_chunks(); + res += test_safe_stream_send(); + res += test_safe_stream_recv(); + res += test_safe_stream_send_recv(); + res += test_safe_stream(); + res += test_safe_stream_reliability(); + return res; +} diff --git a/tests/test-stream/mock_bad_streams.cpp b/tests/test-safe-stream/mock_bad_streams.cpp similarity index 94% rename from tests/test-stream/mock_bad_streams.cpp rename to tests/test-safe-stream/mock_bad_streams.cpp index 1581e4cd..85ab353d 100644 --- a/tests/test-stream/mock_bad_streams.cpp +++ b/tests/test-safe-stream/mock_bad_streams.cpp @@ -14,7 +14,7 @@ * limitations under the License. */ -#include "test-stream/mock_bad_streams.h" +#include "mock_bad_streams.h" #include @@ -72,9 +72,12 @@ WriteAction& PacketDelayStream::Write(DataBuffer&& data_buffer) { if (bad_streams_internal::IsHitTheRate(delay_rate_)) { AE_TELED_DEBUG("Packet delay!"); // delay send data by packet delay action + data_queue_.push(std::move(data_buffer)); ae_context_.scheduler().DelayedTask( - [this, d{std::move(data_buffer)}]() mutable { + [this]() mutable { AE_TELED_DEBUG("Delayed packet send!"); + auto d = std::move(data_queue_.front()); + data_queue_.pop(); out_->Write(std::move(d)); }, std::chrono::duration_cast( diff --git a/tests/test-stream/mock_bad_streams.h b/tests/test-safe-stream/mock_bad_streams.h similarity index 97% rename from tests/test-stream/mock_bad_streams.h rename to tests/test-safe-stream/mock_bad_streams.h index a7e88846..c4ac3d96 100644 --- a/tests/test-stream/mock_bad_streams.h +++ b/tests/test-safe-stream/mock_bad_streams.h @@ -17,6 +17,7 @@ #ifndef TESTS_TEST_STREAM_MOCK_BAD_STREAMS_H_ #define TESTS_TEST_STREAM_MOCK_BAD_STREAMS_H_ +#include #include #include "aether/ae_context.h" @@ -58,6 +59,7 @@ class PacketDelayStream : public ByteStream { AeContext ae_context_; float delay_rate_; Duration max_delay_; + std::queue data_queue_; std::optional dsw_; }; } // namespace ae diff --git a/tests/test-safe-stream/stream-test-ctx.h b/tests/test-safe-stream/stream-test-ctx.h new file mode 100644 index 00000000..736333fc --- /dev/null +++ b/tests/test-safe-stream/stream-test-ctx.h @@ -0,0 +1,49 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef TESTS_TEST_STREAM_STREAM_TEST_CTX_H +#define TESTS_TEST_STREAM_STREAM_TEST_CTX_H + +#include "aether/ae_context.h" + +namespace ae { +struct TestContext { + AeCtx ToAeContext() const { + static constexpr auto table = + AeCtxTable{nullptr, + [](void* obj) -> TaskScheduler& { + return static_cast(obj)->sched; + }, + [](void* obj) -> IndexRegistry& { + return static_cast(obj)->registry; + }}; + return AeCtx{ + const_cast(this), // NOLINT + &table, + }; + } + + template + decltype(auto) Update(A&&... a) { + return sched.Update(std::forward(a)...); + } + + TaskScheduler sched; + IndexRegistry registry; +}; +} // namespace ae + +#endif // TESTS_TEST_STREAM_STREAM_TEST_CTX_H diff --git a/tests/test-safe-stream/test_circular_buffer.cpp b/tests/test-safe-stream/test_circular_buffer.cpp new file mode 100644 index 00000000..f5a7d381 --- /dev/null +++ b/tests/test-safe-stream/test_circular_buffer.cpp @@ -0,0 +1,152 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include "aether/safe_stream/details/circular_buffer.h" + +#include "tests/test-stream/to_data_buffer.h" + +namespace ae::test_circular_buffer { + +static constexpr std::string_view test_str = "Let it circle"; + +void test_PushData() { + static constexpr std::size_t kCapacity = 30; + using Buffer = CircularBuffer; + + Buffer buffer; + + auto res1 = buffer.Push(ToSpan(test_str)); + TEST_ASSERT_TRUE(res1.IsOk()); + TEST_ASSERT_EQUAL(0, res1.value().left); + TEST_ASSERT_EQUAL(test_str.size() - 1, res1.value().right); + + auto res2 = buffer.Push(ToSpan(test_str)); + TEST_ASSERT_TRUE(res2.IsOk()); + TEST_ASSERT_EQUAL(test_str.size(), res2.value().left); + TEST_ASSERT_EQUAL(2 * test_str.size() - 1, res2.value().right); + + // must be overflow + auto res3 = buffer.Push(ToSpan(test_str)); + TEST_ASSERT_FALSE(res3.IsOk()); + TEST_ASSERT_EQUAL(CircularBufferError::kDataOverflow, res3.error()); +} + +void test_ReadData() { + static constexpr std::size_t kCapacity = 30; + using Buffer = CircularBuffer; + using IndexType = Buffer::index_type; + using IndexRangeType = Buffer::index_range_type; + + Buffer buffer; + + auto read_res1 = buffer.Read(IndexType{0}, 10); + TEST_ASSERT_FALSE(read_res1.IsOk()); + TEST_ASSERT_EQUAL(CircularBufferError::kEmptyBuffer, read_res1.error()); + + auto push_res1 = buffer.Push(ToSpan(test_str)); + TEST_ASSERT_TRUE(push_res1.IsOk()); + + auto read_res2 = buffer.Read(IndexType{0}, 10); + TEST_ASSERT_TRUE(read_res2.IsOk()); + TEST_ASSERT_EQUAL(10, read_res2.value().first.size()); + TEST_ASSERT_TRUE(read_res2.value().second.empty()); + TEST_ASSERT_EQUAL_STRING_LEN(test_str.data(), read_res2.value().first.data(), + 10); + + auto read_res3 = buffer.Read(IndexType{10}, 3); + TEST_ASSERT_TRUE(read_res3.IsOk()); + TEST_ASSERT_EQUAL(3, read_res3.value().first.size()); + TEST_ASSERT_TRUE(read_res3.value().second.empty()); + TEST_ASSERT_EQUAL_STRING_LEN(test_str.data() + 10, + read_res3.value().first.data(), 3); + + auto read_res4 = buffer.Read(IndexType{0}, 20); + TEST_ASSERT_TRUE(read_res4.IsOk()); + TEST_ASSERT_EQUAL(13, read_res4.value().first.size()); + TEST_ASSERT_TRUE(read_res4.value().second.empty()); + TEST_ASSERT_EQUAL_STRING_LEN(test_str.data(), read_res4.value().first.data(), + 13); + + auto read_res5 = buffer.Read(IndexType{20}, 13); + TEST_ASSERT_FALSE(read_res5.IsOk()); + TEST_ASSERT_EQUAL(CircularBufferError::kIndexOutOfRange, read_res5.error()); +} + +void test_Erase() { + static constexpr std::size_t kCapacity = 30; + using Buffer = CircularBuffer; + using IndexType = Buffer::index_type; + using IndexRangeType = Buffer::index_range_type; + Buffer buffer; + + auto res1 = buffer.Push(ToSpan(test_str)); + TEST_ASSERT_TRUE(res1.IsOk()); + auto read_res1 = buffer.Read(IndexType{0}, 3); + TEST_ASSERT_TRUE(read_res1.IsOk()); + + buffer.Erase(IndexType{5}); + auto read_res2 = buffer.Read(IndexType{0}, 13); + TEST_ASSERT_FALSE(read_res2.IsOk()); + TEST_ASSERT_EQUAL(CircularBufferError::kIndexOutOfRange, read_res2.error()); + + auto read_res3 = buffer.Read(IndexType{5}, 8); + TEST_ASSERT_TRUE(read_res3.IsOk()); + TEST_ASSERT_EQUAL(8, read_res3.value().first.size()); + TEST_ASSERT_EQUAL_STRING_LEN(test_str.data() + 5, + read_res3.value().first.data(), 8); + + buffer.Erase(res1.value().right + 1); + auto read_res4 = buffer.Read(res1.value().right + 1, 3); + TEST_ASSERT_FALSE(read_res4.IsOk()); + TEST_ASSERT_EQUAL(CircularBufferError::kEmptyBuffer, read_res4.error()); +} + +void test_PushOverTheBourder() { + static constexpr std::size_t kCapacity = 30; + using Buffer = CircularBuffer; + using IndexType = Buffer::index_type; + using IndexRangeType = Buffer::index_range_type; + + // Push some data near the full of the buffer + // Erase data to make room for more + // Push more data to wrap buffer around + + Buffer buffer; + auto res1 = buffer.Push(ToSpan(test_str)); + TEST_ASSERT_TRUE(res1.IsOk()); + auto res2 = buffer.Push(ToSpan(test_str)); + TEST_ASSERT_TRUE(res2.IsOk()); + buffer.Erase(res1.value().right); + auto res3 = buffer.Push(ToSpan(test_str)); + TEST_ASSERT_TRUE(res3.IsOk()); + TEST_ASSERT_EQUAL(26, res3.value().left); + TEST_ASSERT_EQUAL(8, res3.value().right); +} + +} // namespace ae::test_circular_buffer + +int test_circular_buffer() { + UNITY_BEGIN(); + RUN_TEST(ae::test_circular_buffer::test_PushData); + RUN_TEST(ae::test_circular_buffer::test_ReadData); + RUN_TEST(ae::test_circular_buffer::test_Erase); + RUN_TEST(ae::test_circular_buffer::test_PushOverTheBourder); + return UNITY_END(); +} diff --git a/tests/test-safe-stream/test_receiving_chunks.cpp b/tests/test-safe-stream/test_receiving_chunks.cpp new file mode 100644 index 00000000..d61a847d --- /dev/null +++ b/tests/test-safe-stream/test_receiving_chunks.cpp @@ -0,0 +1,195 @@ +/* + * Copyright 2025 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "aether/safe_stream/details/receiving_chunk_list.h" + +namespace ae::test_receiving_chunks { +static constexpr std::size_t kCapacity = 1024; +using IndexType = RingIndex; +using IndexRangeType = RingIndexRange; +using ChunkList = ReceiveChunkList; + +void test_AddChunks() { + static constexpr IndexType buffer_begin = IndexType{0}; + auto chunk_list = ChunkList{buffer_begin}; + + auto res = + chunk_list.AddChunk(IndexRangeType{IndexType{0}, IndexType{69}}, 0); + TEST_ASSERT_EQUAL(ChunkAddResult::kAdded, res); + + // add the same chunk + res = chunk_list.AddChunk(IndexRangeType{IndexType{0}, IndexType{69}}, 1); + TEST_ASSERT_EQUAL(ChunkAddResult::kAddRepeated, res); + + // add duplicate chunk + res = chunk_list.AddChunk(IndexRangeType{IndexType{0}, IndexType{69}}, 1); + TEST_ASSERT_EQUAL(ChunkAddResult::kDuplicate, res); + + // add overlapped chunk + res = chunk_list.AddChunk(IndexRangeType{IndexType{20}, IndexType{89}}, 0); + TEST_ASSERT_EQUAL(ChunkAddResult::kAdded, res); + + // add chunk unorder + res = chunk_list.AddChunk(IndexRangeType{IndexType{140}, IndexType{209}}, 0); + TEST_ASSERT_EQUAL(ChunkAddResult::kAdded, res); + + // return order + res = chunk_list.AddChunk(IndexRangeType{IndexType{70}, IndexType{139}}, 0); + TEST_ASSERT_EQUAL(ChunkAddResult::kAdded, res); +} + +void test_ReceiveChunks() { + static constexpr IndexType buffer_begin = IndexType{0}; + auto chunk_list = ChunkList{buffer_begin}; + + auto chunk0 = chunk_list.ReceiveChunk(); + TEST_ASSERT_TRUE(chunk0.IsEmpty()); + + // add 3 chunks one after another + chunk_list.AddChunk(IndexRangeType{IndexType{0}, IndexType{49}}, 0); + chunk_list.AddChunk(IndexRangeType{IndexType{50}, IndexType{99}}, 0); + chunk_list.AddChunk(IndexRangeType{IndexType{100}, IndexType{149}}, 0); + + auto chunk1 = chunk_list.ReceiveChunk(); + TEST_ASSERT_FALSE(chunk1.IsEmpty()); + TEST_ASSERT_EQUAL(0, chunk1.left); + TEST_ASSERT_EQUAL(149, chunk1.right); + + chunk_list.Acknowledge(chunk1.right); + chunk_list.set_buffer_begin(chunk1.right + 1); + + // no more chunks to receive + auto chunk2 = chunk_list.ReceiveChunk(); + TEST_ASSERT_TRUE(chunk2.IsEmpty()); + + // add 2 chunks with skip + chunk_list.AddChunk(IndexRangeType{IndexType{150}, IndexType{199}}, 0); + chunk_list.AddChunk(IndexRangeType{IndexType{250}, IndexType{299}}, 0); + + // only the first chunk was popped + auto chunk3 = chunk_list.ReceiveChunk(); + TEST_ASSERT_FALSE(chunk3.IsEmpty()); + TEST_ASSERT_EQUAL(150, chunk3.left); + TEST_ASSERT_EQUAL(199, chunk3.right); + + chunk_list.Acknowledge(chunk3.right); + chunk_list.set_buffer_begin(chunk1.right + 1); + + auto chunk4 = chunk_list.ReceiveChunk(); + TEST_ASSERT_TRUE(chunk4.IsEmpty()); +} + +void test_ReceiveChunksInOrder() { + static constexpr IndexType buffer_begin = IndexType{0}; + auto chunk_list = ChunkList{buffer_begin}; + + // second + chunk_list.AddChunk(IndexRangeType{IndexType{100}, IndexType{149}}, 0); + // first + chunk_list.AddChunk(IndexRangeType{IndexType{0}, IndexType{99}}, 0); + + auto chunk1 = chunk_list.ReceiveChunk(); + TEST_ASSERT_FALSE(chunk1.IsEmpty()); + TEST_ASSERT_EQUAL(0, chunk1.left); + TEST_ASSERT_EQUAL(149, chunk1.right); +} + +void test_ReceiveChunksOverlap() { + static constexpr IndexType buffer_begin = IndexType{0}; + auto chunk_list = ChunkList{buffer_begin}; + + // first + chunk_list.AddChunk(IndexRangeType{IndexType{0}, IndexType{99}}, 0); + // second + chunk_list.AddChunk(IndexRangeType{IndexType{20}, IndexType{119}}, 0); + + auto chunk1 = chunk_list.ReceiveChunk(); + TEST_ASSERT_FALSE(chunk1.IsEmpty()); + TEST_ASSERT_EQUAL(0, chunk1.left); + TEST_ASSERT_EQUAL(119, chunk1.right); + + chunk_list.Acknowledge(chunk1.right); + chunk_list.set_buffer_begin(chunk1.right + 1); + + // full overlap + auto acknowledged = IndexType{chunk1.right + 1}; + // add 20 bytes + chunk_list.AddChunk( + IndexRangeType{acknowledged, IndexType{acknowledged + 20}}, 0); + + // overlap from the beginning + chunk_list.AddChunk( + IndexRangeType{acknowledged, IndexType{acknowledged + 50}}, 0); + + auto chunk2 = chunk_list.ReceiveChunk(); + TEST_ASSERT_FALSE(chunk2.IsEmpty()); + + TEST_ASSERT_EQUAL(acknowledged, chunk2.left); + TEST_ASSERT_EQUAL(acknowledged + 50, chunk2.right); + + chunk_list.Acknowledge(chunk2.right); + chunk_list.set_buffer_begin(chunk2.right + 1); + acknowledged = chunk2.right + 1; + + // receive with gap and overoverlap with next chunk + // first 20 bytes with 20 offset + chunk_list.AddChunk(IndexRangeType{IndexType{acknowledged + 20}, + IndexType{acknowledged + 49}}, + 0); + // full overlap + chunk_list.AddChunk( + IndexRangeType{IndexType{acknowledged}, IndexType{acknowledged + 49}}, 0); + + auto chunk5 = chunk_list.ReceiveChunk(); + TEST_ASSERT_FALSE(chunk5.IsEmpty()); + TEST_ASSERT_EQUAL(acknowledged, chunk5.left); + TEST_ASSERT_EQUAL(acknowledged + 49, chunk5.right); +} + +void test_FindMissedChunks() { + static constexpr IndexType buffer_begin = IndexType{0}; + auto chunk_list = ChunkList{buffer_begin}; + + auto missed0 = chunk_list.FindMissedChunk(); + TEST_ASSERT_FALSE(missed0.has_value()); + + chunk_list.AddChunk(IndexRangeType{IndexType{0}, IndexType{10}}, 0); + chunk_list.AddChunk(IndexRangeType{IndexType{11}, IndexType{30}}, 0); + + auto missed1 = chunk_list.FindMissedChunk(); + TEST_ASSERT_FALSE(missed1.has_value()); + + // add chunks with skip + chunk_list.AddChunk(IndexRangeType{IndexType{50}, IndexType{79}}, 0); + + auto missed2 = chunk_list.FindMissedChunk(); + TEST_ASSERT_TRUE(missed2.has_value()); + TEST_ASSERT_EQUAL(31, missed2->left); + TEST_ASSERT_EQUAL(49, missed2->right); +} +} // namespace ae::test_receiving_chunks + +int test_receiving_chunks() { + UNITY_BEGIN(); + RUN_TEST(ae::test_receiving_chunks::test_AddChunks); + RUN_TEST(ae::test_receiving_chunks::test_ReceiveChunks); + RUN_TEST(ae::test_receiving_chunks::test_ReceiveChunksInOrder); + RUN_TEST(ae::test_receiving_chunks::test_ReceiveChunksOverlap); + RUN_TEST(ae::test_receiving_chunks::test_FindMissedChunks); + return UNITY_END(); +} diff --git a/tests/test-stream/safe-stream/test_safe_stream.cpp b/tests/test-safe-stream/test_safe_stream.cpp similarity index 96% rename from tests/test-stream/safe-stream/test_safe_stream.cpp rename to tests/test-safe-stream/test_safe_stream.cpp index b241bc13..4a32a9bb 100644 --- a/tests/test-stream/safe-stream/test_safe_stream.cpp +++ b/tests/test-safe-stream/test_safe_stream.cpp @@ -21,7 +21,7 @@ #include "aether/ae_context.h" #include "aether/api_protocol/protocol_context.h" -#include "aether/stream_api/safe_stream.h" +#include "aether/safe_stream/safe_stream.h" #include "tests/test-stream/to_data_buffer.h" #include "tests/test-stream/stream-test-ctx.h" @@ -30,7 +30,6 @@ namespace ae::test_safe_stream { constexpr auto config = SafeStreamConfig{ - 20 * 1024, 10 * 1024, 200, 3, @@ -58,7 +57,7 @@ void test_SafeStreamWriteFewData() { auto read_stream = MockReadStream{}; auto write_stream = MockWriteStream{ctx, std::size_t{120}}; - auto safe_stream = SafeStream{ctx, config}; + auto safe_stream = SafeStream<1024>{ctx, config}; Tie(read_stream, safe_stream, write_stream); @@ -101,7 +100,7 @@ void test_SafeStreamPacketLoss() { auto read_stream = MockReadStream{}; auto write_stream = MockWriteStream{ctx, std::size_t{120}}; - auto safe_stream = SafeStream{ctx, config}; + auto safe_stream = SafeStream<1024>{ctx, config}; Tie(read_stream, safe_stream, write_stream); // loop data to itself diff --git a/tests/test-stream/safe-stream/test_safe_stream_recv.cpp b/tests/test-safe-stream/test_safe_stream_recv.cpp similarity index 64% rename from tests/test-stream/safe-stream/test_safe_stream_recv.cpp rename to tests/test-safe-stream/test_safe_stream_recv.cpp index 6a179578..12165e76 100644 --- a/tests/test-stream/safe-stream/test_safe_stream_recv.cpp +++ b/tests/test-safe-stream/test_safe_stream_recv.cpp @@ -16,13 +16,12 @@ #include -#include #include +#include #include "aether/config.h" -#include "aether/actions/action_ptr.h" -#include "aether/stream_api/safe_stream/safe_stream_config.h" -#include "aether/stream_api/safe_stream/safe_stream_recv_action.h" +#include "aether/safe_stream/safe_stream_config.h" +#include "aether/safe_stream/details/safe_stream_recv_action.h" #include "tests/test-stream/to_data_buffer.h" #include "tests/test-stream/stream-test-ctx.h" @@ -32,7 +31,6 @@ constexpr auto kTick = std::chrono::milliseconds{1}; // Configuration for most tests constexpr auto config = SafeStreamConfig{ - 20 * 1024, 2048, // larger window for recv tests 150, 3, @@ -43,7 +41,6 @@ constexpr auto config = SafeStreamConfig{ // Configuration for window size tests constexpr auto window_config = SafeStreamConfig{ - 20 * 1024, 512, // smaller window for testing limits 128, 3, @@ -52,30 +49,33 @@ constexpr auto window_config = SafeStreamConfig{ std::chrono::milliseconds{80}, }; +static constexpr std::size_t kCapacity = 20 * 1024; +using Receiver = SafeStreamRecvAction; +using IndexType = RingIndex; +using IndexRangeType = RingIndexRange; + class MockSendConfirmRepeat final : public ISendAckRepeat { public: - struct ConfirmData { - SSRingIndex offset; + struct AckData { + std::uint16_t index; }; struct RepeatRequestData { - SSRingIndex offset; + std::uint16_t index; }; - void SendAck(SSRingIndex offset) override { - confirm_data = ConfirmData{offset}; - } + void SendAck(std::uint16_t index) override { ack_data = AckData{index}; } - void SendRepeatRequest(SSRingIndex offset) override { - repeat_request_data = RepeatRequestData{offset}; + void SendRepeatRequest(std::uint16_t index) override { + repeat_request_data = RepeatRequestData{index}; } void Reset() { - confirm_data.reset(); + ack_data.reset(); repeat_request_data.reset(); } - std::optional confirm_data; + std::optional ack_data; std::optional repeat_request_data; }; @@ -110,27 +110,27 @@ static constexpr std::string_view bug_report = "Severity: critical to sanity."; // Helper function to create DataChunk -static auto CreateDataMessage(std::string_view data, std::uint16_t offset) { - return DataMessage{0, false, offset, ToDataBuffer(data)}; +static auto CreateDataMessage(std::string_view data, std::uint16_t index) { + return DataMessage{false, 0, index, ToDataBuffer(data)}; } void test_RecvActionCreateAndReceive() { TestContext ctx; - auto begin_offset = SSRingIndex{1337}; + auto begin_offset = std::uint16_t{1337}; auto mock_sender = MockSendConfirmRepeat{}; - auto recv_action = ActionPtr{ctx, mock_sender, config}; + auto receiver = Receiver{ctx, mock_sender, config}; // Track received data std::vector received_data; - auto recv_sub = recv_action->receive_event().Subscribe( + auto recv_sub = receiver.receive_event().Subscribe( [&](DataBuffer&& data) { received_data.push_back(std::move(data)); }); auto start_time = Now(); auto message = CreateDataMessage(quantum_data, 0); - recv_action->PushData(begin_offset, message); + receiver.PushData(begin_offset, message); ctx.Update(start_time); // Should immediately emit the data since it's the expected next chunk @@ -139,31 +139,30 @@ void test_RecvActionCreateAndReceive() { quantum_data.size()); // Should not send confirmation immediately (waits for timeout) - TEST_ASSERT_FALSE(mock_sender.confirm_data.has_value()); + TEST_ASSERT_FALSE(mock_sender.ack_data.has_value()); // Wait for confirmation timeout and verify confirmation is sent auto timeout_time = start_time + config.send_ack_timeout + kTick; ctx.Update(timeout_time); // Should send confirmation after timeout - TEST_ASSERT(mock_sender.confirm_data.has_value()); - auto expected_confirm_offset = - begin_offset + static_cast(quantum_data.size()); - TEST_ASSERT_EQUAL( - static_cast(expected_confirm_offset), - static_cast(mock_sender.confirm_data->offset)); + TEST_ASSERT(mock_sender.ack_data.has_value()); + auto expected_confirm_index = + begin_offset + static_cast(quantum_data.size()) - 1; + TEST_ASSERT_EQUAL(static_cast(expected_confirm_index), + static_cast(mock_sender.ack_data->index)); } void test_RecvActionInOrderDataChain() { TestContext ctx; - auto begin_offset = SSRingIndex{2048}; + auto begin_offset = std::uint16_t{2048}; auto mock_sender = MockSendConfirmRepeat{}; - auto recv_action = ActionPtr{ctx, mock_sender, config}; + auto receiver = Receiver{ctx, mock_sender, config}; std::vector received_data; - auto recv_sub = recv_action->receive_event().Subscribe( + auto recv_sub = receiver.receive_event().Subscribe( [&](DataBuffer&& data) { received_data.push_back(std::move(data)); }); // Create data chunks using DataChunk structure @@ -173,15 +172,15 @@ void test_RecvActionInOrderDataChain() { async_philosophy, message1.data.size() + message2.data.size()); // Send chunks in order - recv_action->PushData(begin_offset, message1); + receiver.PushData(begin_offset, message1); ctx.Update(Now()); TEST_ASSERT_EQUAL(1, received_data.size()); - recv_action->PushData(begin_offset, message2); + receiver.PushData(begin_offset, message2); ctx.Update(Now()); TEST_ASSERT_EQUAL(2, received_data.size()); - recv_action->PushData(begin_offset, message3); + receiver.PushData(begin_offset, message3); ctx.Update(Now()); TEST_ASSERT_EQUAL(3, received_data.size()); @@ -197,13 +196,13 @@ void test_RecvActionInOrderDataChain() { void test_RecvActionOutOfOrderDataChain() { TestContext ctx; - auto begin_offset = SSRingIndex{4096}; + auto begin_offset = std::uint16_t{4096}; auto mock_sender = MockSendConfirmRepeat{}; - auto recv_action = ActionPtr{ctx, mock_sender, config}; + auto receiver = Receiver{ctx, mock_sender, config}; std::vector received_data; - auto recv_sub = recv_action->receive_event().Subscribe( + auto recv_sub = receiver.receive_event().Subscribe( [&](DataBuffer&& data) { received_data.push_back(std::move(data)); }); // Create data chunks using DataChunk structure @@ -213,15 +212,15 @@ void test_RecvActionOutOfOrderDataChain() { quantum_data, message1.data.size() + message2.data.size()); // Send chunks out of order: 1, 3, 2 - recv_action->PushData(begin_offset, message1); + receiver.PushData(begin_offset, message1); ctx.Update(Now()); TEST_ASSERT_EQUAL(1, received_data.size()); // Chunk 1 emitted immediately - recv_action->PushData(begin_offset, message3); + receiver.PushData(begin_offset, message3); ctx.Update(Now()); TEST_ASSERT_EQUAL(1, received_data.size()); // Chunk 3 buffered (gap exists) - recv_action->PushData(begin_offset, message2); + receiver.PushData(begin_offset, message2); ctx.Update(Now()); TEST_ASSERT_EQUAL( 2, received_data.size()); // Chunks 2+3 emitted as single combined data @@ -248,51 +247,50 @@ void test_RecvActionOutOfOrderDataChain() { void test_RecvActionSendConfirmOnTimeout() { TestContext ctx; - auto begin_offset = SSRingIndex{5555}; + auto begin_offset = std::uint16_t{5555}; auto mock_sender = MockSendConfirmRepeat{}; - auto recv_action = ActionPtr{ctx, mock_sender, config}; + auto receiver = Receiver{ctx, mock_sender, config}; std::vector received_data; - auto recv_sub = recv_action->receive_event().Subscribe( + auto recv_sub = receiver.receive_event().Subscribe( [&](DataBuffer&& data) { received_data.push_back(std::move(data)); }); auto start_time = Now(); // Send some data using DataChunk auto message = CreateDataMessage(network_poetry, 0); - recv_action->PushData(begin_offset, message); + receiver.PushData(begin_offset, message); ctx.Update(start_time); // Data should be emitted TEST_ASSERT_EQUAL(1, received_data.size()); // Should not send confirmation immediately - TEST_ASSERT_FALSE(mock_sender.confirm_data.has_value()); + TEST_ASSERT_FALSE(mock_sender.ack_data.has_value()); // Wait for confirmation timeout auto timeout_time = start_time + config.send_ack_timeout + kTick; ctx.Update(timeout_time); // Should send confirmation after timeout - TEST_ASSERT(mock_sender.confirm_data.has_value()); - auto expected_confirm_offset = - begin_offset + static_cast(network_poetry.size()); - TEST_ASSERT_EQUAL( - static_cast(expected_confirm_offset), - static_cast(mock_sender.confirm_data->offset)); + TEST_ASSERT(mock_sender.ack_data.has_value()); + auto expected_confirm_index = + begin_offset + static_cast(network_poetry.size()) - 1; + TEST_ASSERT_EQUAL(static_cast(expected_confirm_index), + static_cast(mock_sender.ack_data->index)); } void test_RecvActionRequestRepeatOnMissing() { TestContext ctx; - auto begin_offset = SSRingIndex{6789}; + auto begin_offset = std::uint16_t{6789}; auto mock_sender = MockSendConfirmRepeat{}; - auto recv_action = ActionPtr{ctx, mock_sender, config}; + auto receiver = Receiver{ctx, mock_sender, config}; std::vector received_data; - auto recv_sub = recv_action->receive_event().Subscribe( + auto recv_sub = receiver.receive_event().Subscribe( [&](DataBuffer&& data) { received_data.push_back(std::move(data)); }); // Create chunks with a gap @@ -304,12 +302,12 @@ void test_RecvActionRequestRepeatOnMissing() { auto start_time = Now(); // Send chunk 1 (will be emitted immediately) - recv_action->PushData(begin_offset, message1); + receiver.PushData(begin_offset, message1); ctx.Update(start_time); TEST_ASSERT_EQUAL(1, received_data.size()); // Send chunk 3 (will be buffered due to gap) - recv_action->PushData(begin_offset, message3); + receiver.PushData(begin_offset, message3); ctx.Update(start_time); TEST_ASSERT_EQUAL(1, received_data.size()); // Still only chunk 1 emitted @@ -323,20 +321,20 @@ void test_RecvActionRequestRepeatOnMissing() { // Should request repeat for missing chunk 2 TEST_ASSERT(mock_sender.repeat_request_data.has_value()); TEST_ASSERT_EQUAL( - static_cast(begin_offset + message1.data.size()), - static_cast(mock_sender.repeat_request_data->offset)); + static_cast(begin_offset + message1.data.size()), + static_cast(mock_sender.repeat_request_data->index)); } -void test_RecvActionDuplicateDataHandling() { +void test_RecvActionRepeatDataHandling() { TestContext ctx; - auto begin_offset = SSRingIndex{8192}; + auto begin_offset = std::uint16_t{8192}; auto mock_sender = MockSendConfirmRepeat{}; - auto recv_action = ActionPtr{ctx, mock_sender, config}; + auto receiver = Receiver{ctx, mock_sender, config}; std::vector received_data; - auto recv_sub = recv_action->receive_event().Subscribe( + auto recv_sub = receiver.receive_event().Subscribe( [&](DataBuffer&& data) { received_data.push_back(std::move(data)); }); auto message = CreateDataMessage(protocol_haiku, 0); @@ -344,41 +342,117 @@ void test_RecvActionDuplicateDataHandling() { auto start_time = Now(); // Send original data - recv_action->PushData(begin_offset, message); + receiver.PushData(begin_offset, message); + // Send repeat request + message.repeat_count += 1; + receiver.PushData(begin_offset, message); + ctx.Update(start_time); + // should receive only one message TEST_ASSERT_EQUAL(1, received_data.size()); // Reset mock to check for immediate confirmation mock_sender.Reset(); - - // Send duplicate with higher repeat count - message.control.repeat_count += 1; - recv_action->PushData(begin_offset, message); auto timeout_time = start_time + config.send_ack_timeout + kTick; ctx.Update(timeout_time); - // Should still have only 1 emitted data (no duplicate emission) + // Should have only 1 emitted data (no duplicate emission) TEST_ASSERT_EQUAL(1, received_data.size()); - // Should send immediate confirmation for duplicate - TEST_ASSERT(mock_sender.confirm_data.has_value()); - auto expected_confirm_offset = - begin_offset + static_cast(protocol_haiku.size()); - TEST_ASSERT_EQUAL( - static_cast(expected_confirm_offset), - static_cast(mock_sender.confirm_data->offset)); + // Should have one acknowledgement for repeated data + TEST_ASSERT(mock_sender.ack_data.has_value()); + auto expected_confirm_index = + begin_offset + static_cast(protocol_haiku.size()) - 1; + TEST_ASSERT_EQUAL(static_cast(expected_confirm_index), + static_cast(mock_sender.ack_data->index)); +} + +void test_RecvActionDuplicateDataHandling() { + TestContext ctx; + + auto begin_offset = std::uint16_t{8192}; + auto mock_sender = MockSendConfirmRepeat{}; + + auto receiver = Receiver{ctx, mock_sender, config}; + + std::vector received_data; + auto recv_sub = receiver.receive_event().Subscribe( + [&](DataBuffer&& data) { received_data.push_back(std::move(data)); }); + + auto message = CreateDataMessage(protocol_haiku, 0); + + auto start_time = Now(); + + // Send original and duplicate messages + receiver.PushData(begin_offset, message); + receiver.PushData(begin_offset, message); + ctx.Update(start_time); + // should receive only one message + TEST_ASSERT_EQUAL(1, received_data.size()); + + // wait for ack timeout + auto timeout_time = start_time + config.send_ack_timeout + 2 * kTick; + ctx.Update(timeout_time); + + // Should send anknowledgement only for the original message + TEST_ASSERT_TRUE(mock_sender.ack_data.has_value()); + TEST_ASSERT_EQUAL(begin_offset + protocol_haiku.size() - 1, + mock_sender.ack_data->index); +} + +void test_RecvActionRepeatOverlappingDataHandling() { + TestContext ctx; + + auto begin_offset = std::uint16_t{8192}; + auto mock_sender = MockSendConfirmRepeat{}; + + auto receiver = Receiver{ctx, mock_sender, config}; + + std::vector received_data; + auto recv_sub = receiver.receive_event().Subscribe( + [&](DataBuffer&& data) { received_data.push_back(std::move(data)); }); + + // two messages: part of bug_report and full bug_report + auto message1 = DataMessage{ + false, 0, 0, + ToDataBuffer(std::begin(bug_report), std::begin(bug_report) + 20)}; + + auto message2 = DataMessage{ + false, 0, 0, ToDataBuffer(std::begin(bug_report), std::end(bug_report))}; + + auto start_time = Now(); + + // Send original data + receiver.PushData(begin_offset, message1); + ctx.Update(start_time); + // received the first part of the bug_report + TEST_ASSERT_EQUAL(1, received_data.size()); + TEST_ASSERT_EQUAL(20, received_data[0].size()); + + mock_sender.Reset(); + + // Send repeated request with full bug_report + receiver.PushData(begin_offset, message2); + ctx.Update(start_time + kTick); + // received the second part of the bug_report + TEST_ASSERT_EQUAL(2, received_data.size()); + TEST_ASSERT_EQUAL(bug_report.size() - 20, received_data[1].size()); + + TEST_ASSERT_EQUAL_STRING_LEN(bug_report.data(), received_data[0].data(), 20); + TEST_ASSERT_EQUAL_STRING_LEN(bug_report.data() + 20, received_data[1].data(), + bug_report.size() - 20); } void test_RecvActionInOrderCombinedDataChain() { TestContext ctx; - auto begin_offset = SSRingIndex{3141}; + auto begin_offset = std::uint16_t{3141}; auto mock_sender = MockSendConfirmRepeat{}; - auto recv_action = ActionPtr{ctx, mock_sender, config}; + auto receiver = Receiver{ctx, mock_sender, config}; std::vector received_data; - auto recv_sub = recv_action->receive_event().Subscribe( + auto recv_sub = receiver.receive_event().Subscribe( [&](DataBuffer&& data) { received_data.push_back(std::move(data)); }); // Create multiple small contiguous chunks that should be combined @@ -388,9 +462,9 @@ void test_RecvActionInOrderCombinedDataChain() { "Part3-Final", message1.data.size() + message2.data.size()); // Send all chunks rapidly - they should be combined into single emission - recv_action->PushData(begin_offset, message1); - recv_action->PushData(begin_offset, message2); - recv_action->PushData(begin_offset, message3); + receiver.PushData(begin_offset, message1); + receiver.PushData(begin_offset, message2); + receiver.PushData(begin_offset, message3); ctx.Update(Now()); // Should emit all data as single combined chunk @@ -412,18 +486,18 @@ void test_RecvActionInOrderCombinedDataChain() { void test_RecvActionWindowSizeLimit() { TestContext ctx; - auto begin_offset = SSRingIndex{7777}; + auto begin_offset = std::uint16_t{7777}; auto mock_sender = MockSendConfirmRepeat{}; - auto recv_action = ActionPtr{ctx, mock_sender, config}; + auto receiver = Receiver{ctx, mock_sender, config}; std::vector received_data; - auto recv_sub = recv_action->receive_event().Subscribe( + auto recv_sub = receiver.receive_event().Subscribe( [&](DataBuffer&& data) { received_data.push_back(std::move(data)); }); // Send first chunk auto message1 = CreateDataMessage("First chunk: ", 0); - recv_action->PushData(begin_offset, message1); + receiver.PushData(begin_offset, message1); ctx.Update(Now()); TEST_ASSERT_EQUAL(1, received_data.size()); // First chunk emitted @@ -434,12 +508,12 @@ void test_RecvActionWindowSizeLimit() { auto invalid_message3 = CreateDataMessage(quantum_data, 600); // Send valid second chunk (within window and contiguous) - recv_action->PushData(begin_offset, message2); + receiver.PushData(begin_offset, message2); ctx.Update(Now()); TEST_ASSERT_EQUAL(2, received_data.size()); // Second chunk emitted // Send invalid data (outside window) - should be rejected - recv_action->PushData(begin_offset, invalid_message3); + receiver.PushData(begin_offset, invalid_message3); ctx.Update(Now()); // Should still have only 2 emitted chunks (invalid data rejected) @@ -451,11 +525,14 @@ int test_safe_stream_recv() { UNITY_BEGIN(); RUN_TEST(ae::test_safe_stream_recv::test_RecvActionCreateAndReceive); RUN_TEST(ae::test_safe_stream_recv::test_RecvActionInOrderDataChain); - RUN_TEST(ae::test_safe_stream_recv::test_RecvActionInOrderCombinedDataChain); RUN_TEST(ae::test_safe_stream_recv::test_RecvActionOutOfOrderDataChain); RUN_TEST(ae::test_safe_stream_recv::test_RecvActionSendConfirmOnTimeout); RUN_TEST(ae::test_safe_stream_recv::test_RecvActionRequestRepeatOnMissing); + RUN_TEST(ae::test_safe_stream_recv::test_RecvActionRepeatDataHandling); RUN_TEST(ae::test_safe_stream_recv::test_RecvActionDuplicateDataHandling); + RUN_TEST( + ae::test_safe_stream_recv::test_RecvActionRepeatOverlappingDataHandling); + RUN_TEST(ae::test_safe_stream_recv::test_RecvActionInOrderCombinedDataChain); RUN_TEST(ae::test_safe_stream_recv::test_RecvActionWindowSizeLimit); return UNITY_END(); } diff --git a/tests/test-stream/safe-stream/test_safe_stream_reliability.cpp b/tests/test-safe-stream/test_safe_stream_reliability.cpp similarity index 81% rename from tests/test-stream/safe-stream/test_safe_stream_reliability.cpp rename to tests/test-safe-stream/test_safe_stream_reliability.cpp index ac87a56d..e7ff0f0c 100644 --- a/tests/test-stream/safe-stream/test_safe_stream_reliability.cpp +++ b/tests/test-safe-stream/test_safe_stream_reliability.cpp @@ -18,31 +18,31 @@ #include -#include "aether/actions/action_ptr.h" #include "aether/actions/action_context.h" #include "aether/actions/repeatable_task.h" -#include "aether/actions/action_processor.h" -#include "aether/stream_api/safe_stream.h" +#include "aether/safe_stream/safe_stream.h" #include "aether/tele/tele.h" #include "tests/test-stream/to_data_buffer.h" -#include "tests/test-stream/stream-test-ctx.h" -#include "tests/test-stream/mock_bad_streams.h" #include "tests/test-stream/mock_write_stream.h" +#include "stream-test-ctx.h" +#include "mock_bad_streams.h" + namespace ae::test_safe_stream_reliability { constexpr auto config = SafeStreamConfig{ - 20 * 1024, 4096, 200, - 10, + 100, std::chrono::milliseconds{50}, std::chrono::milliseconds{0}, std::chrono::milliseconds{10}, }; +static constexpr std::size_t kCapacity = 20 * 1024; + static constexpr std::string_view test_data = "If I was in World War Two, they'd call me \"Spitfire\""; @@ -51,9 +51,10 @@ TimePoint WaitUntil(TimePoint epoch, TimePoint till_time) { return epoch; } -void TestSendPackets(TestContext& ctx, SafeStream& sender, SafeStream& receiver, - int wait_messages) { +void TestSendPackets(TestContext& ctx, SafeStream& sender, + SafeStream& receiver, int wait_messages) { int sent_messages = 0; + int failed_messages = 0; std::size_t received_size = 0; // send messages periodically @@ -62,7 +63,9 @@ void TestSendPackets(TestContext& ctx, SafeStream& sender, SafeStream& receiver, [&]() { auto& sent_action = sender.Write(ToDataBuffer(test_data)); sent_action.status_event().Subscribe([&](auto status) { - TEST_ASSERT_NOT_EQUAL(WriteAction::Status::kFail, status); + if (status != WriteAction::Status::kSuccess) { + failed_messages++; + } wait_messages--; }); sent_messages++; @@ -84,8 +87,9 @@ void TestSendPackets(TestContext& ctx, SafeStream& sender, SafeStream& receiver, } TEST_ASSERT_EQUAL(0, wait_messages); - TEST_ASSERT_EQUAL(sent_messages, - static_cast(received_size / test_data.size())); + TEST_ASSERT_EQUAL(0, failed_messages); + auto expected_size = sent_messages * test_data.size(); + TEST_ASSERT_EQUAL(expected_size, received_size); } void test_SafeStreamLostPackets() { @@ -102,8 +106,8 @@ void test_SafeStreamLostPackets() { r_mock_stream.on_write_event().Subscribe( [&](auto&& data) { s_mock_stream.WriteOut(data); }); - auto sender = SafeStream{ctx, config}; - auto receiver = SafeStream{ctx, config}; + auto sender = SafeStream{ctx, config}; + auto receiver = SafeStream{ctx, config}; // Tie streams forward and back Tie(sender, s_packet_loss, s_mock_stream); @@ -128,8 +132,8 @@ void test_SafeStreamPacketsReordered() { r_mock_stream.on_write_event().Subscribe( [&](auto&& data) { s_mock_stream.WriteOut(data); }); - auto sender = SafeStream{ctx, config}; - auto receiver = SafeStream{ctx, config}; + auto sender = SafeStream{ctx, config}; + auto receiver = SafeStream{ctx, config}; // Tie streams forward and back Tie(sender, s_packet_delay, s_mock_stream); @@ -158,8 +162,8 @@ void test_SafeStreamPacketsLostAndReordered() { r_mock_stream.on_write_event().Subscribe( [&](auto&& data) { s_mock_stream.WriteOut(data); }); - auto sender = SafeStream{ctx, config}; - auto receiver = SafeStream{ctx, config}; + auto sender = SafeStream{ctx, config}; + auto receiver = SafeStream{ctx, config}; // Tie streams forward and back Tie(sender, s_packet_loss, s_packet_delay, s_mock_stream); @@ -172,8 +176,8 @@ void test_SafeStreamPacketsLostAndReordered() { int test_safe_stream_reliability() { UNITY_BEGIN(); - // RUN_TEST(ae::test_safe_stream_reliability::test_SafeStreamLostPackets); - // RUN_TEST(ae::test_safe_stream_reliability::test_SafeStreamPacketsReordered); + RUN_TEST(ae::test_safe_stream_reliability::test_SafeStreamLostPackets); + RUN_TEST(ae::test_safe_stream_reliability::test_SafeStreamPacketsReordered); RUN_TEST( ae::test_safe_stream_reliability::test_SafeStreamPacketsLostAndReordered); return UNITY_END(); diff --git a/tests/test-stream/safe-stream/test_safe_stream_send.cpp b/tests/test-safe-stream/test_safe_stream_send.cpp similarity index 65% rename from tests/test-stream/safe-stream/test_safe_stream_send.cpp rename to tests/test-safe-stream/test_safe_stream_send.cpp index 18c60b0c..1764328a 100644 --- a/tests/test-stream/safe-stream/test_safe_stream_send.cpp +++ b/tests/test-safe-stream/test_safe_stream_send.cpp @@ -19,10 +19,9 @@ #include #include "aether/config.h" -#include "aether/actions/action_context.h" -#include "aether/actions/action_processor.h" -#include "aether/stream_api/safe_stream/safe_stream_config.h" -#include "aether/stream_api/safe_stream/safe_stream_send_action.h" +#include "aether/ae_context.h" +#include "aether/safe_stream/safe_stream_config.h" +#include "aether/safe_stream/details/safe_stream_send_action.h" #include "tests/test-stream/to_data_buffer.h" #include "tests/test-stream/stream-test-ctx.h" @@ -31,7 +30,6 @@ namespace ae::test_safe_stream_send { constexpr auto kTick = std::chrono::milliseconds{1}; constexpr auto config = SafeStreamConfig{ - 20 * 1024, 1056, 200, 3, @@ -40,10 +38,15 @@ constexpr auto config = SafeStreamConfig{ std::chrono::milliseconds{80}, }; +static constexpr std::size_t kCapacity = 20 * 1024; +using Sender = SafeStreamSendAction; +using IndexType = RingIndex; +using IndexRangeType = RingIndexRange; + class MockSendDataPush final : public ISendDataPush { public: struct SendData { - SSRingIndex begin; + std::uint16_t index; DataMessage data_message; }; @@ -57,9 +60,9 @@ class MockSendDataPush final : public ISendDataPush { explicit MockSendDataPush(AeContext const& context) : context_{context} {} - WriteAction& PushData(SSRingIndex begin, + WriteAction& PushData(std::uint16_t index, DataMessage&& data_message) override { - send_data = SendData{begin, std::move(data_message)}; + send_data = SendData{index, std::move(data_message)}; if (!wa_ || wa_->is_finished()) { wa_.emplace(context_); } @@ -117,17 +120,26 @@ void test_SendActionCreateAndSend() { auto send_data_push = MockSendDataPush{ctx}; - auto send_action = - ActionPtr{ctx, send_data_push, config}; - send_action->SetMaxPayload(config.max_packet_size); - - auto data_action = send_action->SendData(ToDataBuffer(packet_poetry)); - bool is_sent = false; bool is_error = false; - data_action->StatusEvent().Subscribe( - ActionHandler{OnResult{[&]() { is_sent = true; }}, - OnError{[&]() { is_error = true; }}}); + IndexRangeType expected_range{}; + + auto sender = Sender{ctx, send_data_push, config}; + sender.acknowledged_event().Subscribe([&](auto, auto index) { + if (expected_range.right == index) { + is_sent = true; + } + }); + sender.send_failed_event().Subscribe([&](auto, auto index) { + if (expected_range.right == index) { + is_error = true; + } + }); + sender.SetMaxPayload(config.max_packet_size); + + auto send_data = sender.SendData(ToSpan(packet_poetry)); + TEST_ASSERT_TRUE(send_data.IsOk()); + expected_range = send_data.value(); // Verify initial state before update TEST_ASSERT_FALSE(send_data_push.send_data.has_value()); @@ -137,22 +149,22 @@ void test_SendActionCreateAndSend() { ctx.Update(Now()); // Verify data was sent with correct content and offset - TEST_ASSERT(send_data_push.send_data.has_value()); + TEST_ASSERT_TRUE(send_data_push.send_data.has_value()); TEST_ASSERT_EQUAL_STRING_LEN( packet_poetry.data(), send_data_push.send_data->data_message.data.data(), packet_poetry.size()); TEST_ASSERT_EQUAL(0, send_data_push.send_data->data_message.delta_offset); - TEST_ASSERT_EQUAL(true, send_data_push.send_data->data_message.reset()); + TEST_ASSERT_EQUAL(true, send_data_push.send_data->data_message.reset); // Verify intermediate state: data sent but not yet confirmed TEST_ASSERT_FALSE(is_sent); TEST_ASSERT_FALSE(is_error); - TEST_ASSERT_EQUAL(0, send_data_push.send_data->data_message.repeat_count()); + TEST_ASSERT_EQUAL(0, send_data_push.send_data->data_message.repeat_count); - send_action->Acknowledge( - send_data_push.send_data->begin + - send_data_push.send_data->data_message.delta_offset + - static_cast(packet_poetry.size())); + // acknowledge up to last byte in the packet size() - 1; + sender.Acknowledge(send_data_push.send_data->index + + send_data_push.send_data->data_message.delta_offset + + static_cast(packet_poetry.size()) - 1); ctx.Update(Now()); // Verify final state after confirmation @@ -165,24 +177,33 @@ void test_SendActionRepeatOnTimeout() { auto send_data_push = MockSendDataPush{ctx}; - auto send_action = - ActionPtr{ctx, send_data_push, config}; - send_action->SetMaxPayload(config.max_packet_size); - - auto data_action = send_action->SendData(ToDataBuffer(retry_humor)); - bool is_sent = false; bool is_error = false; - data_action->StatusEvent().Subscribe( - ActionHandler{OnResult{[&]() { is_sent = true; }}, - OnError{[&]() { is_error = true; }}}); + IndexRangeType expected_range{}; + + auto sender = Sender{ctx, send_data_push, config}; + sender.acknowledged_event().Subscribe([&](auto, auto index) { + if (expected_range.right == index) { + is_sent = true; + } + }); + sender.send_failed_event().Subscribe([&](auto, auto index) { + if (expected_range.right == index) { + is_error = true; + } + }); + sender.SetMaxPayload(config.max_packet_size); + + auto send_data = sender.SendData(ToSpan(retry_humor)); + TEST_ASSERT_TRUE(send_data.IsOk()); + expected_range = send_data.value(); auto start_time = Now(); // Initial send ctx.Update(start_time); TEST_ASSERT(send_data_push.send_data.has_value()); - TEST_ASSERT_EQUAL(0, send_data_push.send_data->data_message.repeat_count()); + TEST_ASSERT_EQUAL(0, send_data_push.send_data->data_message.repeat_count); send_data_push.send_data.reset(); // Wait for timeout and trigger repeat - need two updates: one to detect @@ -193,16 +214,16 @@ void test_SendActionRepeatOnTimeout() { // Check that repeat was triggered TEST_ASSERT(send_data_push.send_data.has_value()); - TEST_ASSERT_EQUAL(1, send_data_push.send_data->data_message.repeat_count()); + TEST_ASSERT_EQUAL(1, send_data_push.send_data->data_message.repeat_count); TEST_ASSERT_EQUAL(0, send_data_push.send_data->data_message.delta_offset); TEST_ASSERT_EQUAL_STRING_LEN( retry_humor.data(), send_data_push.send_data->data_message.data.data(), retry_humor.size()); // Confirm after first repeat - send_action->Acknowledge(send_data_push.send_data->begin + - send_data_push.send_data->data_message.delta_offset + - +static_cast(retry_humor.size())); + sender.Acknowledge(send_data_push.send_data->index + + send_data_push.send_data->data_message.delta_offset + + +static_cast(retry_humor.size() - 1)); ctx.Update(timeout_time + kTick + kTick); TEST_ASSERT(is_sent); @@ -214,24 +235,33 @@ void test_SendActionErrorOnMaxRepeatExceeded() { auto send_data_push = MockSendDataPush{ctx}; - auto send_action = - ActionPtr{ctx, send_data_push, config}; - send_action->SetMaxPayload(config.max_packet_size); - - auto data_action = send_action->SendData(ToDataBuffer(confirmation_comedy)); - bool is_sent = false; bool is_error = false; - data_action->StatusEvent().Subscribe( - ActionHandler{OnResult{[&]() { is_sent = true; }}, - OnError{[&]() { is_error = true; }}}); + IndexRangeType expected_range{}; + + auto sender = Sender{ctx, send_data_push, config}; + sender.acknowledged_event().Subscribe([&](auto, auto index) { + if (expected_range.right == index) { + is_sent = true; + } + }); + sender.send_failed_event().Subscribe([&](auto, auto index) { + if (expected_range.right == index) { + is_error = true; + } + }); + sender.SetMaxPayload(config.max_packet_size); + + auto send_data = sender.SendData(ToSpan(confirmation_comedy)); + TEST_ASSERT_TRUE(send_data.IsOk()); + expected_range = send_data.value(); auto current_time = Now(); // Initial send (repeat_count = 0) ctx.Update(current_time); TEST_ASSERT(send_data_push.send_data.has_value()); - TEST_ASSERT_EQUAL(0, send_data_push.send_data->data_message.repeat_count()); + TEST_ASSERT_EQUAL(0, send_data_push.send_data->data_message.repeat_count); send_data_push.send_data.reset(); // Helper lambda for timeout calculation with exponential backoff @@ -249,7 +279,7 @@ void test_SendActionErrorOnMaxRepeatExceeded() { ctx.Update(current_time); ctx.Update(current_time + kTick); TEST_ASSERT(send_data_push.send_data.has_value()); - TEST_ASSERT_EQUAL(1, send_data_push.send_data->data_message.repeat_count()); + TEST_ASSERT_EQUAL(1, send_data_push.send_data->data_message.repeat_count); send_data_push.send_data.reset(); // Second repeat (repeat_count = 2) @@ -257,11 +287,12 @@ void test_SendActionErrorOnMaxRepeatExceeded() { ctx.Update(current_time); ctx.Update(current_time + kTick); TEST_ASSERT(send_data_push.send_data.has_value()); - TEST_ASSERT_EQUAL(2, send_data_push.send_data->data_message.repeat_count()); + TEST_ASSERT_EQUAL(2, send_data_push.send_data->data_message.repeat_count); send_data_push.send_data.reset(); - // Third repeat attempt should exceed max_repeat_count (3) and trigger error - // repeat_count = 3, then incremented to 4, which exceeds max_repeat_count (3) + // Third repeat attempt should exceed max_repeat_count (3) and trigger + // error + // repeat_count = 3, then incremented to 4, which exceeds max_repeat_count(3) current_time += wait_confirm_timeout(2) + kTick; ctx.Update(current_time); ctx.Update(current_time + kTick); @@ -269,7 +300,7 @@ void test_SendActionErrorOnMaxRepeatExceeded() { // Should not send anymore and trigger error TEST_ASSERT_FALSE(send_data_push.send_data.has_value()); TEST_ASSERT_FALSE(is_sent); - TEST_ASSERT(is_error); + TEST_ASSERT_TRUE(is_error); } void test_SendActionRequestRepeat() { @@ -277,13 +308,12 @@ void test_SendActionRequestRepeat() { auto send_data_push = MockSendDataPush{ctx}; - auto send_action = - ActionPtr{ctx, send_data_push, config}; - send_action->SetMaxPayload(config.max_packet_size); + auto sender = Sender{ctx, send_data_push, config}; + sender.SetMaxPayload(config.max_packet_size); - auto data_action1 = send_action->SendData(ToDataBuffer(network_philosophy)); + auto send_data1 = sender.SendData(ToSpan(network_philosophy)); - SSRingIndex begin_offset; + std::uint16_t begin_offset{}; // Initial send ctx.Update(Now()); @@ -292,22 +322,22 @@ void test_SendActionRequestRepeat() { send_data_push.send_data.reset(); // Add second data and let it send - auto data_action2 = send_action->SendData(ToDataBuffer(buffer_ballad)); + auto send_data2 = sender.SendData(ToSpan(buffer_ballad)); ctx.Update(Now()); TEST_ASSERT(send_data_push.send_data.has_value()); TEST_ASSERT_EQUAL(network_philosophy.size(), send_data_push.send_data->data_message.delta_offset); - begin_offset = send_data_push.send_data->begin; + begin_offset = send_data_push.send_data->index; send_data_push.send_data.reset(); // Request repeat of first chunk - send_action->RequestRepeat(begin_offset); + sender.RequestRepeat(begin_offset); ctx.Update(Now()); // Should resend from the requested offset TEST_ASSERT(send_data_push.send_data.has_value()); TEST_ASSERT_EQUAL(0, send_data_push.send_data->data_message.delta_offset); - TEST_ASSERT_EQUAL(1, send_data_push.send_data->data_message.repeat_count()); + TEST_ASSERT_EQUAL(1, send_data_push.send_data->data_message.repeat_count); } void test_SendActionWindowSizeLimit() { @@ -317,19 +347,18 @@ void test_SendActionWindowSizeLimit() { // Use larger packet size for cleaner window size testing constexpr std::size_t large_packet_size = 352; - auto send_action = - ActionPtr{ctx, send_data_push, config}; - send_action->SetMaxPayload(large_packet_size); + auto sender = Sender{ctx, send_data_push, config}; + sender.SetMaxPayload(large_packet_size); // Window check is: begin_.Distance(last_sent_ + max_packet_size_) > // window_size_ window_size = 1056, max_packet_size = 352 We can send 3 // packets (1056 bytes) The 4th packet would exceed window auto packet_data = CreateTestData(large_packet_size); - SSRingIndex begin_offset; + std::uint16_t begin_offset{}; // Send 3 packets to fill window exactly (3 * 352 = 1056 bytes) for (int i = 0; i < 3; ++i) { - send_action->SendData(DataBuffer{packet_data}); + sender.SendData(DataBuffer{packet_data}); ctx.Update(Now()); // Should send successfully @@ -337,7 +366,7 @@ void test_SendActionWindowSizeLimit() { auto expected_delta = i * large_packet_size; TEST_ASSERT_EQUAL(expected_delta, send_data_push.send_data->data_message.delta_offset); - begin_offset = send_data_push.send_data->begin; + begin_offset = send_data_push.send_data->index; send_data_push.send_data.reset(); } @@ -345,7 +374,7 @@ void test_SendActionWindowSizeLimit() { // last_sent_ is now at begin_ + 1056, adding max_packet_size (352) = begin_ // + 1408 begin_.Distance(begin_ + 1408) = 1408 > 1056 (window_size), so // should be blocked - send_action->SendData(DataBuffer{packet_data}); + sender.SendData(packet_data); ctx.Update(Now()); // This should NOT send because it would exceed window size @@ -354,8 +383,8 @@ void test_SendActionWindowSizeLimit() { // Confirm some data to free up window space // Confirm first packet (352 bytes) auto confirm_offset = - begin_offset + static_cast(large_packet_size); - send_action->Acknowledge(confirm_offset); + begin_offset + static_cast(large_packet_size - 1); + sender.Acknowledge(confirm_offset); ctx.Update(Now()); // Now the blocked data should send @@ -374,26 +403,25 @@ void test_SendActionWindowSizeWithMultipleWaitingPackets() { // Use larger packet size for cleaner testing constexpr std::size_t large_packet_size = 352; - auto send_action = - ActionPtr{ctx, send_data_push, config}; - send_action->SetMaxPayload(large_packet_size); + auto sender = Sender{ctx, send_data_push, config}; + sender.SetMaxPayload(large_packet_size); // Fill the window to its limit // window_size = 1056, max_packet_size = 352 // Send 3 packets to reach exactly 1056 bytes auto packet_data = CreateTestData(large_packet_size); - SSRingIndex begin_offset; + std::uint16_t begin_offset{}; // Send 3 packets of 352 bytes = 1056 bytes (full window) for (int i = 0; i < 3; ++i) { - send_action->SendData(DataBuffer{packet_data}); + sender.SendData(packet_data); ctx.Update(Now()); TEST_ASSERT(send_data_push.send_data.has_value()); auto expected_delta = i * large_packet_size; TEST_ASSERT_EQUAL(expected_delta, send_data_push.send_data->data_message.delta_offset); - begin_offset = send_data_push.send_data->begin; + begin_offset = send_data_push.send_data->index; send_data_push.send_data.reset(); } @@ -401,7 +429,7 @@ void test_SendActionWindowSizeWithMultipleWaitingPackets() { // Each attempt to send would make last_sent_ + max_packet_size > begin_ + // window_size for (int i = 0; i < 2; ++i) { - send_action->SendData(DataBuffer{packet_data}); + sender.SendData(DataBuffer{packet_data}); ctx.Update(Now()); // None of these should send @@ -410,13 +438,13 @@ void test_SendActionWindowSizeWithMultipleWaitingPackets() { // Confirm some data to free up space (confirm first packet = 352 bytes) auto confirm_offset = - begin_offset + static_cast(large_packet_size); - send_action->Acknowledge(confirm_offset); + begin_offset + static_cast(large_packet_size - 1); + sender.Acknowledge(confirm_offset); ctx.Update(Now()); // Now first waiting packet should send - // begin moved forward by 352, so the next packet delta should be 1056 - 352 = - // 704 + // begin moved forward by 352, so the next packet delta should be 1056 - + // 352 = 704 TEST_ASSERT(send_data_push.send_data.has_value()); auto expected_delta = 2 * large_packet_size; TEST_ASSERT_EQUAL(expected_delta, @@ -424,7 +452,8 @@ void test_SendActionWindowSizeWithMultipleWaitingPackets() { send_data_push.send_data.reset(); // Second waiting packet should not send (window limit reached again) - // Window now has 2 packets (704 bytes) + new packet (352) = 1056 bytes (full) + // Window now has 2 packets (704 bytes) + new packet (352) = 1056 bytes + // (full) ctx.Update(Now()); TEST_ASSERT_FALSE(send_data_push.send_data.has_value()); } @@ -434,31 +463,49 @@ void test_SendActionMultipleDataQueueing() { auto send_data_push = MockSendDataPush{ctx}; - auto send_action = - ActionPtr{ctx, send_data_push, config}; - send_action->SetMaxPayload(config.max_packet_size); - - // Send multiple data chunks rapidly - auto data_action1 = send_action->SendData(ToDataBuffer(packet_poetry)); - auto data_action2 = send_action->SendData(ToDataBuffer(window_wisdom)); - - SSRingIndex begin_offset; + std::uint16_t begin_offset{}; bool sent1 = false; bool error1 = false; + IndexRangeType expected_range1{}; bool sent2 = false; bool error2 = false; + IndexRangeType expected_range2{}; - data_action1->StatusEvent().Subscribe(ActionHandler{ - OnResult{[&]() { sent1 = true; }}, OnError{[&]() { error1 = true; }}}); + auto sender = Sender{ctx, send_data_push, config}; + sender.acknowledged_event().Subscribe([&](auto buffer_begin, auto index) { + if (IndexComparable{expected_range1.right, buffer_begin} <= index) { + sent1 = true; + } + if (IndexComparable{expected_range2.right, buffer_begin} <= index) { + sent2 = true; + } + }); + sender.send_failed_event().Subscribe([&](auto buffer_begin, auto index) { + if (IndexComparable{expected_range1.right, buffer_begin} <= index) { + error1 = true; + } + if (IndexComparable{expected_range2.right, buffer_begin} <= index) { + error2 = true; + } + }); + sender.SetMaxPayload(config.max_packet_size); - data_action2->StatusEvent().Subscribe(ActionHandler{ - OnResult{[&]() { sent2 = true; }}, OnError{[&]() { error2 = true; }}}); + // Send multiple data chunks rapidly + auto send_data1 = sender.SendData(ToSpan(packet_poetry)); + auto send_data2 = sender.SendData(ToSpan(window_wisdom)); + + TEST_ASSERT_TRUE(send_data1.IsOk()); + TEST_ASSERT_TRUE(send_data2.IsOk()); + expected_range1 = send_data1.value(); + expected_range2 = send_data2.value(); // Process queued data ctx.Update(Now()); TEST_ASSERT(send_data_push.send_data.has_value()); TEST_ASSERT_EQUAL(0, send_data_push.send_data->data_message.delta_offset); - begin_offset = send_data_push.send_data->begin; + TEST_ASSERT_EQUAL(config.max_packet_size, + send_data_push.send_data->data_message.data.size()); + begin_offset = send_data_push.send_data->index; send_data_push.send_data.reset(); ctx.Update(Now()); @@ -467,12 +514,13 @@ void test_SendActionMultipleDataQueueing() { send_data_push.send_data.reset(); // Verify proper sequencing (begin_offset should be less than second_offset) - TEST_ASSERT_GREATER_THAN(0, static_cast(second_delta)); + TEST_ASSERT_GREATER_THAN(0, static_cast(second_delta)); // Confirm all data to complete actions - auto final_offset = begin_offset + second_delta + - static_cast(window_wisdom.size()); - send_action->Acknowledge(final_offset); + auto final_offset = begin_offset + + static_cast(packet_poetry.size()) + + static_cast(window_wisdom.size()) - 1; + sender.Acknowledge(final_offset); ctx.Update(Now()); // Verify actions completed successfully @@ -488,20 +536,29 @@ void test_SendDataBiggerThanMaxPacketSize() { constexpr std::uint16_t max_packet_size = 60; auto send_data_push = MockSendDataPush{ctx}; - auto send_action = - ActionPtr{ctx, send_data_push, config}; - send_action->SetMaxPayload(max_packet_size); - - // Send data bigger than max packet size - auto data_action = send_action->SendData(ToDataBuffer(packet_poetry)); - constexpr auto packets_count = packet_poetry.size() / max_packet_size + 1; bool sent = false; bool error = false; + IndexRangeType expected_range{}; - data_action->StatusEvent().Subscribe( - ActionHandler{OnResult{[&]() { sent = true; }}, - OnError{[&](auto const&) { error = true; }}}); + auto sender = Sender{ctx, send_data_push, config}; + sender.acknowledged_event().Subscribe([&](auto, auto index) { + if (expected_range.right == index) { + sent = true; + } + }); + sender.send_failed_event().Subscribe([&](auto, auto index) { + if (expected_range.right == index) { + error = true; + } + }); + sender.SetMaxPayload(max_packet_size); + + // Send data bigger than max packet size + auto send_data = sender.SendData(ToSpan(packet_poetry)); + TEST_ASSERT_TRUE(send_data.IsOk()); + expected_range = send_data.value(); + constexpr auto packets_count = (packet_poetry.size() / max_packet_size) + 1; // Process packets for (std::uint16_t i = 0; i < packets_count - 1; ++i) { auto expected_packet = @@ -546,8 +603,8 @@ void test_SendDataBiggerThanMaxPacketSize() { // Confirm all data to complete actions auto final_offset = - send_data_push.send_data->begin + expected_offset + expected_size; - send_action->Acknowledge(final_offset); + send_data_push.send_data->index + expected_offset + expected_size - 1; + sender.Acknowledge(final_offset); ctx.Update(Now()); // Verify actions completed successfully @@ -555,18 +612,91 @@ void test_SendDataBiggerThanMaxPacketSize() { TEST_ASSERT_FALSE(error); } +void test_SendDataAndStop() { + TestContext ctx; + + constexpr std::uint16_t max_packet_size = 450; + auto send_data_push = MockSendDataPush{ctx}; + + bool sent = false; + bool error = false; + bool stopped = false; + IndexRangeType expected_range{}; + + auto sender = Sender{ctx, send_data_push, config}; + sender.acknowledged_event().Subscribe([&](auto, auto index) { + if (expected_range.right == index) { + sent = true; + } + }); + sender.send_failed_event().Subscribe([&](auto, auto index) { + if (expected_range.right == index) { + error = true; + } + }); + sender.stopped_event().Subscribe([&](auto, auto index) { + if (expected_range.right == index) { + stopped = true; + } + }); + sender.SetMaxPayload(max_packet_size); + + auto send_range0 = sender.SendData(ToSpan(packet_poetry)); + TEST_ASSERT(send_range0.IsOk()); + expected_range = send_range0.value(); + + // stop sending till send + sender.Stop(send_range0.value()); + + ctx.Update(Now()); + TEST_ASSERT_FALSE(sent); + TEST_ASSERT_FALSE(error); + TEST_ASSERT_TRUE(stopped); + TEST_ASSERT_FALSE(send_data_push.send_data.has_value()); + + stopped = false; + + // send more data + auto send_range1 = sender.SendData(ToSpan(packet_poetry)); + TEST_ASSERT(send_range1.IsOk()); + expected_range = send_range1.value(); + + ctx.Update(Now()); + + TEST_ASSERT_FALSE(sent); + TEST_ASSERT_FALSE(error); + TEST_ASSERT_FALSE(stopped); + TEST_ASSERT_TRUE(send_data_push.send_data.has_value()); + TEST_ASSERT_EQUAL(expected_range.left, send_data_push.send_data->index); + send_data_push.send_data.reset(); + + // stop sending after send should have no effect + sender.Stop(send_range1.value()); + ctx.Update(Now()); + // no new data + TEST_ASSERT_FALSE(sent); + TEST_ASSERT_FALSE(error); + TEST_ASSERT_FALSE(stopped); + TEST_ASSERT_FALSE(send_data_push.send_data.has_value()); + + sender.Acknowledge(static_cast(send_range1.value().right)); + ctx.Update(Now()); + TEST_ASSERT_TRUE(sent); +} + } // namespace ae::test_safe_stream_send int test_safe_stream_send() { UNITY_BEGIN(); RUN_TEST(ae::test_safe_stream_send::test_SendActionCreateAndSend); - RUN_TEST(ae::test_safe_stream_send::test_SendActionMultipleDataQueueing); + RUN_TEST(ae::test_safe_stream_send:: + test_SendActionWindowSizeWithMultipleWaitingPackets); RUN_TEST(ae::test_safe_stream_send::test_SendActionRepeatOnTimeout); RUN_TEST(ae::test_safe_stream_send::test_SendActionErrorOnMaxRepeatExceeded); - RUN_TEST(ae::test_safe_stream_send::test_SendActionRequestRepeat); RUN_TEST(ae::test_safe_stream_send::test_SendActionWindowSizeLimit); - RUN_TEST(ae::test_safe_stream_send:: - test_SendActionWindowSizeWithMultipleWaitingPackets); + RUN_TEST(ae::test_safe_stream_send::test_SendActionRequestRepeat); + RUN_TEST(ae::test_safe_stream_send::test_SendActionMultipleDataQueueing); RUN_TEST(ae::test_safe_stream_send::test_SendDataBiggerThanMaxPacketSize); + RUN_TEST(ae::test_safe_stream_send::test_SendDataAndStop); return UNITY_END(); } diff --git a/tests/test-stream/safe-stream/test_safe_stream_send_recv.cpp b/tests/test-safe-stream/test_safe_stream_send_recv.cpp similarity index 64% rename from tests/test-stream/safe-stream/test_safe_stream_send_recv.cpp rename to tests/test-safe-stream/test_safe_stream_send_recv.cpp index a037cb83..2d275a75 100644 --- a/tests/test-stream/safe-stream/test_safe_stream_send_recv.cpp +++ b/tests/test-safe-stream/test_safe_stream_send_recv.cpp @@ -16,17 +16,17 @@ #include -#include "aether/actions/action_ptr.h" -#include "aether/stream_api/safe_stream/safe_stream_config.h" -#include "aether/stream_api/safe_stream/safe_stream_recv_action.h" -#include "aether/stream_api/safe_stream/safe_stream_send_action.h" +#include + +#include "aether/safe_stream/safe_stream_config.h" +#include "aether/safe_stream/details/safe_stream_recv_action.h" +#include "aether/safe_stream/details/safe_stream_send_action.h" #include "tests/test-stream/to_data_buffer.h" #include "tests/test-stream/stream-test-ctx.h" namespace ae::test_safe_stream_send_recv { constexpr auto config = SafeStreamConfig{ - 20 * 1024, 10 * 1024, 100, 3, @@ -34,51 +34,56 @@ constexpr auto config = SafeStreamConfig{ std::chrono::milliseconds{25}, std::chrono::milliseconds{80}, }; - +constexpr auto kCapacity = 20 * 1024; constexpr auto kTick = std::chrono::milliseconds{1}; +using Sender = SafeStreamSendAction; +using Receiver = SafeStreamRecvAction; +using IndexType = RingIndex; +using IndexRangeType = RingIndexRange; + class TestSafeStreamActionsTransport { public: - explicit TestSafeStreamActionsTransport(SafeStreamSendAction& send_action, - SafeStreamRecvAction& recv_action) - : send_action_{&send_action}, recv_action_{&recv_action} {} + explicit TestSafeStreamActionsTransport(Sender& send_action, + Receiver& recv_action) + : sender_{&send_action}, recveiver_{&recv_action} {} virtual ~TestSafeStreamActionsTransport() = default; - virtual void PushData(SSRingIndex begin, DataMessage data_message) { + virtual void PushData(std::uint16_t begin, DataMessage data_message) { MakePushData(begin, std::move(data_message)); } - virtual void SendAck(SSRingIndex offset) { MakeSendAck(offset); } + virtual void SendAck(std::uint16_t offset) { MakeSendAck(offset); } - virtual void SendRepeatRequest(SSRingIndex offset) { + virtual void SendRepeatRequest(std::uint16_t offset) { MakeSendRepeatRequest(offset); } - void MakePushData(SSRingIndex begin, DataMessage data_message) { - recv_action_->PushData(begin, std::move(data_message)); + void MakePushData(std::uint16_t begin, DataMessage data_message) { + recveiver_->PushData(begin, std::move(data_message)); } - void MakeSendAck(SSRingIndex offset) { send_action_->Acknowledge(offset); } + void MakeSendAck(std::uint16_t offset) { sender_->Acknowledge(offset); } - void MakeSendRepeatRequest(SSRingIndex offset) { - send_action_->RequestRepeat(offset); + void MakeSendRepeatRequest(std::uint16_t offset) { + sender_->RequestRepeat(offset); } - SafeStreamSendAction* send_action_; - SafeStreamRecvAction* recv_action_; + Sender* sender_; + Receiver* recveiver_; }; class DelayPushDataImpl : public TestSafeStreamActionsTransport { public: struct PushMessageT { - SSRingIndex begin; + std::uint16_t begin; DataMessage message; }; using TestSafeStreamActionsTransport::TestSafeStreamActionsTransport; - void PushData(SSRingIndex begin, DataMessage data_message) override { + void PushData(std::uint16_t begin, DataMessage data_message) override { push_message = PushMessageT{begin, std::move(data_message)}; } @@ -107,7 +112,7 @@ class MockSendDataPush : public ISendDataPush { transport_ = &transport; } - WriteAction& PushData(SSRingIndex begin, + WriteAction& PushData(std::uint16_t begin, DataMessage&& data_message) override { transport_->PushData(begin, std::move(data_message)); if (!dswa_ || dswa_->is_finished()) { @@ -129,8 +134,8 @@ class MockSendAckRepeat : public ISendAckRepeat { transport_ = &transport; } - void SendAck(SSRingIndex offset) override { transport_->SendAck(offset); } - void SendRepeatRequest(SSRingIndex offset) override { + void SendAck(std::uint16_t offset) override { transport_->SendAck(offset); } + void SendRepeatRequest(std::uint16_t offset) override { transport_->SendRepeatRequest(offset); } @@ -152,25 +157,31 @@ void test_SafeStreamInitHandshake() { bool acked{}; DataBuffer received{}; + IndexRangeType expected_range{}; auto send_transport = MockSendDataPush{ctx}; auto recv_transport = MockSendAckRepeat{}; - auto sender = ActionPtr{ctx, send_transport, config}; - sender->SetMaxPayload(config.max_packet_size); - auto receiver = ActionPtr{ctx, recv_transport, config}; + auto sender = Sender{ctx, send_transport, config}; + sender.acknowledged_event().Subscribe([&](auto buffer_begin, auto end) { + if (IndexComparable{expected_range.right, buffer_begin} <= end) { + acked = true; + } + }); - auto sender_to_receiver = TestSafeStreamActionsTransport{*sender, *receiver}; + sender.SetMaxPayload(config.max_packet_size); + auto receiver = Receiver{ctx, recv_transport, config}; + + auto sender_to_receiver = TestSafeStreamActionsTransport{sender, receiver}; send_transport.Link(sender_to_receiver); recv_transport.Link(sender_to_receiver); - receiver->receive_event().Subscribe( + receiver.receive_event().Subscribe( [&](auto const& data) { received = data; }); - auto send_data = sender->SendData(ToDataBuffer(test_data)); - - send_data->StatusEvent().Subscribe( - OnResult{[&](auto const&) { acked = true; }}); + auto send_data = sender.SendData(ToSpan(test_data)); + TEST_ASSERT_TRUE(send_data.IsOk()); + expected_range = send_data.value(); ctx.Update(epoch); ctx.Update(epoch += config.send_ack_timeout + kTick); @@ -187,34 +198,41 @@ void test_SafeStreamInitHandshake() { * data to SafeStreamRecvAction. */ void test_SafeStreamReInitSender() { - auto epoch = Now(); TestContext ctx; bool acked{}; DataBuffer received{}; + IndexRangeType expected_range; auto send_transport = MockSendDataPush{ctx}; auto recv_transport = MockSendAckRepeat{}; // sender is optional and will be replaced - auto sender = ActionPtr{ctx, send_transport, config}; + auto sender = + std::optional{std::in_place, ctx, send_transport, config}; + sender->acknowledged_event().Subscribe([&](auto buffer_begin, auto end) { + if (IndexComparable{expected_range.right, buffer_begin} <= end) { + acked = true; + } + }); sender->SetMaxPayload(config.max_packet_size); - auto receiver = ActionPtr{ctx, recv_transport, config}; + auto receiver = Receiver{ctx, recv_transport, config}; - auto sender_to_receiver = TestSafeStreamActionsTransport{*sender, *receiver}; + auto sender_to_receiver = TestSafeStreamActionsTransport{*sender, receiver}; send_transport.Link(sender_to_receiver); recv_transport.Link(sender_to_receiver); - receiver->receive_event().Subscribe( + receiver.receive_event().Subscribe( [&](auto const& data) { received = data; }); - auto send_action1 = sender->SendData(ToDataBuffer(test_data)); - send_action1->StatusEvent().Subscribe( - OnResult{[&](auto const&) { acked = true; }}); + auto send_data1 = sender->SendData(ToSpan(test_data)); + TEST_ASSERT_TRUE(send_data1.IsOk()); + expected_range = send_data1.value(); + auto epoch = Now(); ctx.Update(epoch); ctx.Update(epoch += kTick); - ctx.Update(epoch += config.send_ack_timeout + kTick); + ctx.Update(epoch += (config.send_ack_timeout + kTick)); // test received data TEST_ASSERT_TRUE(acked); @@ -226,16 +244,21 @@ void test_SafeStreamReInitSender() { received.clear(); // create new sender - sender = ActionPtr{ctx, send_transport, config}; + sender.emplace(ctx, send_transport, config); + sender->acknowledged_event().Subscribe([&](auto buffer_begin, auto end) { + if (IndexComparable{expected_range.right, buffer_begin} <= end) { + acked = true; + } + }); sender->SetMaxPayload(config.max_packet_size); - sender_to_receiver = TestSafeStreamActionsTransport{*sender, *receiver}; + sender_to_receiver = TestSafeStreamActionsTransport{*sender, receiver}; send_transport.Link(sender_to_receiver); recv_transport.Link(sender_to_receiver); - auto send_action2 = sender->SendData(ToDataBuffer(test_data)); - send_action2->StatusEvent().Subscribe( - OnResult{[&](auto const&) { acked = true; }}); + auto send_data2 = sender->SendData(ToSpan(test_data)); + TEST_ASSERT_TRUE(send_data2.IsOk()); + expected_range = send_data2.value(); ctx.Update(epoch); ctx.Update(epoch += kTick); @@ -257,25 +280,32 @@ void test_SafeStreamReInitReceiver() { bool acked{}; DataBuffer received{}; + IndexRangeType expected_range{}; auto send_transport = MockSendDataPush{ctx}; auto recv_transport = MockSendAckRepeat{}; // sender is optional and will be replaced - auto sender = ActionPtr{ctx, send_transport, config}; - sender->SetMaxPayload(config.max_packet_size); - auto receiver = ActionPtr{ctx, recv_transport, config}; + auto sender = Sender{ctx, send_transport, config}; + sender.acknowledged_event().Subscribe([&](auto buffer_begin, auto end) { + if (IndexComparable{expected_range.right, buffer_begin} <= end) { + acked = true; + } + }); + sender.SetMaxPayload(config.max_packet_size); + auto receiver = + std::optional{std::in_place, ctx, recv_transport, config}; - auto sender_to_receiver = TestSafeStreamActionsTransport{*sender, *receiver}; + auto sender_to_receiver = TestSafeStreamActionsTransport{sender, *receiver}; send_transport.Link(sender_to_receiver); recv_transport.Link(sender_to_receiver); receiver->receive_event().Subscribe( [&](auto const& data) { received = data; }); - auto send_action1 = sender->SendData(ToDataBuffer(test_data)); - send_action1->StatusEvent().Subscribe( - OnResult{[&](auto const&) { acked = true; }}); + auto send_data1 = sender.SendData(ToSpan(test_data)); + TEST_ASSERT_TRUE(send_data1.IsOk()); + expected_range = send_data1.value(); ctx.Update(epoch); ctx.Update(epoch += config.send_ack_timeout + kTick); @@ -289,18 +319,18 @@ void test_SafeStreamReInitReceiver() { acked = false; received.clear(); - // create new sender - receiver = ActionPtr{ctx, recv_transport, config}; + // create new receiver + receiver.emplace(ctx, recv_transport, config); - sender_to_receiver = TestSafeStreamActionsTransport{*sender, *receiver}; + sender_to_receiver = TestSafeStreamActionsTransport{sender, *receiver}; send_transport.Link(sender_to_receiver); recv_transport.Link(sender_to_receiver); receiver->receive_event().Subscribe( [&](auto const& data) { received = data; }); - auto send_action2 = sender->SendData(ToDataBuffer(test_data)); - send_action2->StatusEvent().Subscribe( - OnResult{[&](auto const&) { acked = true; }}); + auto send_data2 = sender.SendData(ToSpan(test_data)); + TEST_ASSERT_TRUE(send_data2.IsOk()); + expected_range = send_data2.value(); ctx.Update(epoch); ctx.Update(epoch += config.send_ack_timeout + kTick); diff --git a/tests/test-safe-stream/test_sending_chunk_list.cpp b/tests/test-safe-stream/test_sending_chunk_list.cpp new file mode 100644 index 00000000..2fea02ae --- /dev/null +++ b/tests/test-safe-stream/test_sending_chunk_list.cpp @@ -0,0 +1,121 @@ +/* + * Copyright 2025 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "aether/safe_stream/details/sending_chunk_list.h" + +namespace ae::test_sending_chunk_list { + +static constexpr std::size_t kCapacity = 1024; +using IndexType = RingIndex; +using IndexRangeType = RingIndexRange; +using ChunkList = SendingChunkList; + +void test_SendingChunkList() { + constexpr auto begin = IndexType{0}; + + ChunkList chunk_list{begin}; + + // add three chunks + chunk_list.Register(IndexRangeType{IndexType{0}, IndexType{5}}, Now()); + chunk_list.Register(IndexRangeType{IndexType{6}, IndexType{10}}, Now()); + chunk_list.Register(IndexRangeType{IndexType{11}, IndexType{20}}, Now()); + TEST_ASSERT_FALSE(chunk_list.empty()); + + auto& ch1 = chunk_list.front(); + TEST_ASSERT_EQUAL(0, ch1.range.left); + + // repeat add first chunk + chunk_list.Register(IndexRangeType{IndexType{0}, IndexType{5}}, Now()); + + // now in front must be the second chunk + auto& ch2 = chunk_list.front(); + TEST_ASSERT_EQUAL(6, ch2.range.left); + + // add second and third chunks + chunk_list.Register(IndexRangeType{IndexType{6}, IndexType{10}}, Now()); + chunk_list.Register(IndexRangeType{IndexType{11}, IndexType{20}}, Now()); + + // now in front must be the first chunk again + auto& ch3 = chunk_list.front(); + TEST_ASSERT_EQUAL(0, ch3.range.left); +} + +void test_SendingChunkListSelect() { + constexpr auto begin = IndexType{0}; + + ChunkList chunk_list{begin}; + + auto* s0 = chunk_list.Select(IndexRangeType{IndexType{6}, IndexType{10}}); + TEST_ASSERT_NULL(s0); + + // add three chunks + chunk_list.Register(IndexRangeType{IndexType{0}, IndexType{5}}, Now()); + chunk_list.Register(IndexRangeType{IndexType{6}, IndexType{10}}, Now()); + chunk_list.Register(IndexRangeType{IndexType{11}, IndexType{20}}, Now()); + + auto* s1 = chunk_list.Select(IndexRangeType{IndexType{6}, IndexType{10}}); + TEST_ASSERT_NOT_NULL(s1); + TEST_ASSERT_EQUAL(6, s1->range.left); + + auto* s2 = chunk_list.Select(IndexRangeType{IndexType{21}, IndexType{25}}); + TEST_ASSERT_NULL(s2); + + chunk_list.RemoveUpTo(IndexType{21}); + auto* s3 = chunk_list.Select(IndexRangeType{IndexType{6}, IndexType{10}}); + TEST_ASSERT_NULL(s3); +} + +void test_SendingChunkListRemoving() { + constexpr auto begin = IndexType{0}; + + ChunkList chunk_list{begin}; + + chunk_list.Register(IndexRangeType{IndexType{0}, IndexType{10}}, Now()); + TEST_ASSERT_FALSE(chunk_list.empty()); + + // remove it + chunk_list.RemoveUpTo(IndexType{10}); + TEST_ASSERT_TRUE(chunk_list.empty()); + + // add far away chunk + chunk_list.Register(IndexRangeType{IndexType{100}, IndexType{200}}, Now()); + auto& ch2 = chunk_list.front(); + TEST_ASSERT_EQUAL(100, ch2.range.left); + + // try to remove + chunk_list.RemoveUpTo(IndexType{10}); + TEST_ASSERT_FALSE(chunk_list.empty()); + { + auto& front_chunk = chunk_list.front(); + TEST_ASSERT_EQUAL(100, front_chunk.range.left); + } + + // confirm all + chunk_list.RemoveUpTo(IndexType{1000}); + TEST_ASSERT_TRUE(chunk_list.empty()); +} + +} // namespace ae::test_sending_chunk_list + +int test_sending_chunk_list() { + UNITY_BEGIN(); + RUN_TEST(ae::test_sending_chunk_list::test_SendingChunkList); + RUN_TEST(ae::test_sending_chunk_list::test_SendingChunkListSelect); + RUN_TEST(ae::test_sending_chunk_list::test_SendingChunkListRemoving); + return UNITY_END(); +} diff --git a/tests/test-stream/CMakeLists.txt b/tests/test-stream/CMakeLists.txt index 9e02218e..1cee746c 100644 --- a/tests/test-stream/CMakeLists.txt +++ b/tests/test-stream/CMakeLists.txt @@ -16,19 +16,9 @@ cmake_minimum_required( VERSION 3.18 ) list(APPEND test_srcs main.cpp - mock_bad_streams.cpp - safe-stream/test_safe_stream_types.cpp - safe-stream/test_sending_chunk_list.cpp - safe-stream/test_send_data_buffer.cpp - safe-stream/test_receiving_chunks.cpp - safe-stream/test_safe_stream_send.cpp - safe-stream/test_safe_stream_recv.cpp - safe-stream/test_safe_stream_send_recv.cpp - safe-stream/test_safe_stream.cpp - safe-stream/test_safe_stream_reliability.cpp templated-streams/test_templated_streams.cpp test-tied-gates.cpp - ${CMAKE_CURRENT_LIST_DIR}/../test-object-system/map_domain_storage.cpp + # ${CMAKE_CURRENT_LIST_DIR}/../test-object-system/map_domain_storage.cpp ) if(NOT CM_PLATFORM) diff --git a/tests/test-stream/main.cpp b/tests/test-stream/main.cpp index 17f7ece0..0d4a4151 100644 --- a/tests/test-stream/main.cpp +++ b/tests/test-stream/main.cpp @@ -16,34 +16,14 @@ #include -#include "aether/tele/tele_init.h" - -void setUp() { ae::tele::TeleInit::Init(); } +void setUp() {} void tearDown() {} -extern int test_safe_stream_types(); -extern int test_sending_chunk_list(); -extern int test_send_data_buffer(); -extern int test_receiving_chunks(); -extern int test_safe_stream_send(); -extern int test_safe_stream_recv(); -extern int test_safe_stream_send_recv(); -extern int test_safe_stream(); -extern int test_safe_stream_reliability(); extern int test_templated_streams(); extern int test_tied_gates(); int main() { int res = 0; - res += test_safe_stream_types(); - res += test_sending_chunk_list(); - res += test_send_data_buffer(); - res += test_receiving_chunks(); - res += test_safe_stream_send(); - res += test_safe_stream_recv(); - res += test_safe_stream_send_recv(); - res += test_safe_stream(); - res += test_safe_stream_reliability(); res += test_templated_streams(); res += test_tied_gates(); return res; diff --git a/tests/test-stream/safe-stream/test_receiving_chunks.cpp b/tests/test-stream/safe-stream/test_receiving_chunks.cpp deleted file mode 100644 index 6e1a74f3..00000000 --- a/tests/test-stream/safe-stream/test_receiving_chunks.cpp +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright 2025 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include - -#include "aether/stream_api/safe_stream/receiving_chunk_list.h" - -#include "tests/test-stream/to_data_buffer.h" - -namespace ae::test_receiving_chunks { -static constexpr std::string_view test_data = - "The Taste That Goes EXTREME! Radical Refreshment For Maximum Coolness"; - -void test_AddChunks() { - auto chunk_list = ReceiveChunkList{}; - auto res = chunk_list.AddChunk( - ReceivingChunk{SSRingIndex{0}, ToDataBuffer(test_data), {}}); - TEST_ASSERT_EQUAL(ReceiveChunkList::AddResult::kAdded, res); - TEST_ASSERT_EQUAL(1, chunk_list.size()); - - // add the same chunk - res = chunk_list.AddChunk( - ReceivingChunk{SSRingIndex{0}, ToDataBuffer(test_data), 1}); - TEST_ASSERT_EQUAL(ReceiveChunkList::AddResult::kAdded, res); - // must not add the new chunk - TEST_ASSERT_EQUAL(1, chunk_list.size()); - - // add duplicate chunk - res = chunk_list.AddChunk( - ReceivingChunk{SSRingIndex{0}, ToDataBuffer(test_data), 1}); - TEST_ASSERT_EQUAL(ReceiveChunkList::AddResult::kDuplicate, res); - // must not add the new chunk - TEST_ASSERT_EQUAL(1, chunk_list.size()); - - // add overlapped chunk - res = chunk_list.AddChunk( - ReceivingChunk{SSRingIndex{20}, ToDataBuffer(test_data), {}}); - TEST_ASSERT_EQUAL(ReceiveChunkList::AddResult::kAdded, res); - // split the chunk on two - TEST_ASSERT_EQUAL(2, chunk_list.size()); - - // add chunk unorder - res = chunk_list.AddChunk(ReceivingChunk{ - SSRingIndex{test_data.size() * 2}, ToDataBuffer(test_data), {}}); - TEST_ASSERT_EQUAL(ReceiveChunkList::AddResult::kAdded, res); - TEST_ASSERT_EQUAL(3, chunk_list.size()); - - // return order - res = chunk_list.AddChunk(ReceivingChunk{ - SSRingIndex{test_data.size()}, ToDataBuffer(test_data), {}}); - TEST_ASSERT_EQUAL(ReceiveChunkList::AddResult::kAdded, res); - // though last chunk is overlapped with 3rd and 4th there still only 3 chunks - TEST_ASSERT_EQUAL(4, chunk_list.size()); -} - -void test_ReceiveChunks() { - auto chunk_list = ReceiveChunkList{}; - - auto chunk0 = chunk_list.ReceiveChunk(SSRingIndex{0}); - TEST_ASSERT_FALSE(chunk0.has_value()); - - // add 3 chunks one after another - chunk_list.AddChunk( - ReceivingChunk{SSRingIndex{0}, ToDataBuffer(test_data), {}}); - chunk_list.AddChunk(ReceivingChunk{ - SSRingIndex{static_cast(test_data.size())}, - ToDataBuffer(test_data), - {}}); - chunk_list.AddChunk(ReceivingChunk{ - SSRingIndex{static_cast(test_data.size() * 2)}, - ToDataBuffer(test_data), - {}}); - - auto chunk1 = chunk_list.ReceiveChunk(SSRingIndex{0}); - TEST_ASSERT_TRUE(chunk1.has_value()); - TEST_ASSERT_EQUAL(test_data.size() * 3, chunk1->data.size()); - TEST_ASSERT_EQUAL_STRING_LEN(test_data.data(), chunk1->data.data(), - test_data.size()); - TEST_ASSERT_EQUAL_STRING_LEN(test_data.data(), - chunk1->data.data() + test_data.size(), - test_data.size()); - TEST_ASSERT_EQUAL_STRING_LEN(test_data.data(), - chunk1->data.data() + test_data.size() * 2, - test_data.size()); - - chunk_list.Acknowledge(SSRingIndex{0}, chunk1->offset_range().right); - - // no more chunks to receive - auto chunk2 = chunk_list.ReceiveChunk(SSRingIndex{0}); - TEST_ASSERT_FALSE(chunk2.has_value()); - - // add 2 chunks with skip - chunk_list.AddChunk(ReceivingChunk{ - SSRingIndex{static_cast(test_data.size() * 3)}, - ToDataBuffer(test_data), - {}}); - chunk_list.AddChunk(ReceivingChunk{ - SSRingIndex{static_cast(test_data.size() * 5)}, - ToDataBuffer(test_data), - {}}); - - auto start_offset = - SSRingIndex{static_cast(chunk1->data.size())}; - // only the first chunk was popped - auto chunk3 = chunk_list.ReceiveChunk(start_offset); - TEST_ASSERT_TRUE(chunk3.has_value()); - TEST_ASSERT_EQUAL_STRING_LEN(test_data.data(), chunk3->data.data(), - test_data.size()); - - chunk_list.Acknowledge(SSRingIndex{0}, chunk3->offset_range().right); - - auto chunk4 = chunk_list.ReceiveChunk(start_offset); - TEST_ASSERT_FALSE(chunk4.has_value()); -} - -void test_ReceiveChunksInOrder() { - auto chunk_list = ReceiveChunkList{}; - - // second - chunk_list.AddChunk(ReceivingChunk{ - SSRingIndex{static_cast(test_data.size())}, - ToDataBuffer(test_data), - {}}); - // first - chunk_list.AddChunk( - ReceivingChunk{SSRingIndex{0}, ToDataBuffer(test_data), {}}); - - auto chunk1 = chunk_list.ReceiveChunk(SSRingIndex{0}); - TEST_ASSERT_TRUE(chunk1.has_value()); - TEST_ASSERT_EQUAL(0, static_cast(chunk1->offset)); - TEST_ASSERT_EQUAL(test_data.size() * 2, chunk1->data.size()); - TEST_ASSERT_EQUAL_STRING_LEN(test_data.data(), chunk1->data.data(), - test_data.size()); - TEST_ASSERT_EQUAL_STRING_LEN(test_data.data(), - chunk1->data.data() + test_data.size(), - test_data.size()); -} - -void test_ReceiveChunksOverlap() { - auto chunk_list = ReceiveChunkList{}; - - // first - chunk_list.AddChunk( - ReceivingChunk{SSRingIndex{0}, ToDataBuffer(test_data), {}}); - // second - chunk_list.AddChunk( - ReceivingChunk{SSRingIndex{20}, ToDataBuffer(test_data), {}}); - - auto chunk1 = chunk_list.ReceiveChunk(SSRingIndex{0}); - TEST_ASSERT_TRUE(chunk1.has_value()); - TEST_ASSERT_EQUAL(0, static_cast(chunk1->offset)); - TEST_ASSERT_EQUAL(test_data.size() + 20, chunk1->data.size()); - TEST_ASSERT_EQUAL_STRING_LEN(test_data.data(), chunk1->data.data(), 20); - TEST_ASSERT_EQUAL_STRING_LEN(test_data.data(), chunk1->data.data() + 20, - test_data.size()); - - chunk_list.Acknowledge(SSRingIndex{0}, chunk1->offset_range().right); - - // full overlap - auto confirmed = SSRingIndex{chunk1->offset_range().right + 1}; - // add 20 bytes - chunk_list.AddChunk(ReceivingChunk{ - confirmed, - ToDataBuffer(std::begin(test_data), std::begin(test_data) + 20), - {}}); - - // overlap from the beginning - chunk_list.AddChunk(ReceivingChunk{confirmed, ToDataBuffer(test_data), {}}); - - auto chunk2 = chunk_list.ReceiveChunk(confirmed); - TEST_ASSERT_TRUE(chunk2.has_value()); - TEST_ASSERT_EQUAL(static_cast(confirmed), - static_cast(chunk2->offset)); - TEST_ASSERT_EQUAL(test_data.size(), chunk2->data.size()); - TEST_ASSERT_EQUAL_STRING_LEN(test_data.data(), chunk2->data.data(), - test_data.size()); - - chunk_list.Acknowledge(SSRingIndex{0}, chunk2->offset_range().right); - - // overlap with received - confirmed = chunk2->offset_range().right + 1; - // add data with overlap 20 bytes to confirmed offset - chunk_list.AddChunk( - ReceivingChunk{confirmed - 20, ToDataBuffer(test_data), {}}); - chunk_list.AddChunk( - ReceivingChunk{confirmed + 49, ToDataBuffer(test_data), {}}); - auto chunk3 = chunk_list.ReceiveChunk(confirmed); - TEST_ASSERT_TRUE(chunk3.has_value()); - TEST_ASSERT_EQUAL(static_cast(confirmed), - static_cast(chunk3->offset)); - TEST_ASSERT_EQUAL(test_data.size() - 20 + test_data.size(), - chunk3->data.size()); - TEST_ASSERT_EQUAL_STRING_LEN(test_data.data() + 20, chunk3->data.data(), - test_data.size() - 20); - TEST_ASSERT_EQUAL_STRING_LEN(test_data.data(), chunk3->data.data() + 49, - test_data.size()); - - chunk_list.Acknowledge(SSRingIndex{0}, chunk3->offset_range().right); - - // overlap with confirmed - confirmed = chunk3->offset_range().right + 1; - chunk_list.AddChunk( - ReceivingChunk{SSRingIndex{0}, ToDataBuffer(test_data), {}}); - auto chunk4 = chunk_list.ReceiveChunk(confirmed); - TEST_ASSERT_FALSE(chunk4.has_value()); - - // receive with gap and overoverlap with next chunk - // first 20 bytes with 20 offset - chunk_list.AddChunk(ReceivingChunk{ - confirmed + 20, - ToDataBuffer(std::begin(test_data), std::begin(test_data) + 20), - {}}); - // full overlap - chunk_list.AddChunk(ReceivingChunk{confirmed, ToDataBuffer(test_data), {}}); - - auto chunk5 = chunk_list.ReceiveChunk(confirmed); - TEST_ASSERT_TRUE(chunk5.has_value()); - TEST_ASSERT_EQUAL(static_cast(confirmed), - static_cast(chunk5->offset)); - TEST_ASSERT_EQUAL(test_data.size(), chunk5->data.size()); - TEST_ASSERT_EQUAL_STRING_LEN(test_data.data(), chunk5->data.data(), - test_data.size()); -} - -void test_FindMissedChunks() { - constexpr auto begin = SSRingIndex{0}; - auto chunk_list = ReceiveChunkList{}; - - auto missed0 = chunk_list.FindMissedChunks(begin); - TEST_ASSERT_TRUE(missed0.empty()); - - chunk_list.AddChunk(ReceivingChunk{begin, ToDataBuffer(test_data), {}}); - chunk_list.AddChunk( - ReceivingChunk{begin + test_data.size(), ToDataBuffer(test_data), {}}); - - auto missed1 = chunk_list.FindMissedChunks(begin); - TEST_ASSERT_TRUE(missed1.empty()); - - // add chunks with skip - auto new_begin = begin + test_data.size() * 2; - chunk_list.AddChunk(ReceivingChunk{ - new_begin + test_data.size(), ToDataBuffer(test_data), {}}); - - auto missed2 = chunk_list.FindMissedChunks(begin); - TEST_ASSERT_FALSE(missed2.empty()); - TEST_ASSERT_EQUAL(static_cast(new_begin), - static_cast(missed2[0].expected_offset)); - - // check missed chunks from arbitrary offset - auto missed3 = - chunk_list.FindMissedChunks(SSRingIndex{begin + test_data.size()}); - TEST_ASSERT_FALSE(missed3.empty()); - TEST_ASSERT_EQUAL(static_cast(new_begin), - static_cast(missed3[0].expected_offset)); -} -} // namespace ae::test_receiving_chunks - -int test_receiving_chunks() { - UNITY_BEGIN(); - RUN_TEST(ae::test_receiving_chunks::test_AddChunks); - RUN_TEST(ae::test_receiving_chunks::test_ReceiveChunks); - RUN_TEST(ae::test_receiving_chunks::test_ReceiveChunksInOrder); - RUN_TEST(ae::test_receiving_chunks::test_ReceiveChunksOverlap); - RUN_TEST(ae::test_receiving_chunks::test_FindMissedChunks); - return UNITY_END(); -} diff --git a/tests/test-stream/safe-stream/test_safe_stream_types.cpp b/tests/test-stream/safe-stream/test_safe_stream_types.cpp deleted file mode 100644 index e79962be..00000000 --- a/tests/test-stream/safe-stream/test_safe_stream_types.cpp +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2024 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include - -#include "aether/stream_api/safe_stream/safe_stream_types.h" - -namespace ae::test_safe_stream_types { -void test_OffsetRange() { - constexpr auto half_window_range = - OffsetRange{SSRingIndex{25}, SSRingIndex{75}}; - - TEST_ASSERT_TRUE(half_window_range.IsAfter(SSRingIndex{24})); - TEST_ASSERT_FALSE(half_window_range.IsAfter(SSRingIndex{25})); - TEST_ASSERT_FALSE(half_window_range.IsAfter(SSRingIndex{26})); - - TEST_ASSERT_TRUE(half_window_range.IsBefore(SSRingIndex{76})); - TEST_ASSERT_FALSE(half_window_range.IsBefore(SSRingIndex{75})); - TEST_ASSERT_FALSE(half_window_range.IsBefore(SSRingIndex{74})); - - TEST_ASSERT_TRUE(half_window_range.InRange(SSRingIndex{25})); - TEST_ASSERT_TRUE(half_window_range.InRange(SSRingIndex{75})); - TEST_ASSERT_TRUE(half_window_range.InRange(SSRingIndex{50})); - TEST_ASSERT_FALSE(half_window_range.InRange(SSRingIndex{20})); - TEST_ASSERT_FALSE(half_window_range.InRange(SSRingIndex{80})); - - constexpr auto window_range = OffsetRange{SSRingIndex{0}, SSRingIndex{100}}; - TEST_ASSERT_TRUE(window_range.InRange(SSRingIndex{0})); - TEST_ASSERT_TRUE(window_range.InRange(SSRingIndex{100})); - TEST_ASSERT_TRUE(window_range.InRange(SSRingIndex{10})); - TEST_ASSERT_FALSE(window_range.InRange(SSRingIndex{110})); - TEST_ASSERT_TRUE(window_range.IsBefore(SSRingIndex{110})); - TEST_ASSERT_FALSE(window_range.IsAfter(SSRingIndex{110})); -} - -} // namespace ae::test_safe_stream_types - -int test_safe_stream_types() { - UNITY_BEGIN(); - RUN_TEST(ae::test_safe_stream_types::test_OffsetRange); - return UNITY_END(); -} diff --git a/tests/test-stream/safe-stream/test_send_data_buffer.cpp b/tests/test-stream/safe-stream/test_send_data_buffer.cpp deleted file mode 100644 index 773ce452..00000000 --- a/tests/test-stream/safe-stream/test_send_data_buffer.cpp +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright 2025 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include - -#include - -#include "aether/actions/action_context.h" -#include "aether/actions/action_processor.h" -#include "aether/events/multi_subscription.h" -#include "aether/stream_api/safe_stream/send_data_buffer.h" - -#include "tests/test-stream/to_data_buffer.h" - -namespace ae::test_send_data_buffer { -constexpr std::string_view test_data = "Pure refreshment in every drop"; - -struct UpdateStatus { - template - void Subscribe(Action& action) { - subscriptions.Push(action.StatusEvent().Subscribe(ActionHandler{ - OnResult{[&]() { ack = true; }}, - OnError{[&]() { rejected = true; }}, - OnStop{[&]() { stopped = true; }}, - })); - } - - bool ack; - bool rejected; - bool stopped; - - MultiSubscription subscriptions; -}; - -void test_SendDataBufferGetSlice() { - constexpr auto begin = SSRingIndex{0}; - - ActionProcessor action_processor; - ActionContext action_context{action_processor}; - - SendDataBuffer send_data_buffer{action_context}; - - // add some data - send_data_buffer.AddData( - SendingData{SSRingIndex{0}, ToDataBuffer(test_data)}); - - send_data_buffer.AddData( - SendingData{SSRingIndex{test_data.size()}, ToDataBuffer(test_data)}); - send_data_buffer.AddData( - SendingData{SSRingIndex{2 * test_data.size()}, ToDataBuffer(test_data)}); - - TEST_ASSERT_EQUAL(test_data.size() * 3, send_data_buffer.size()); - // get a slice - { - auto data_slice = - send_data_buffer.GetSlice(SSRingIndex{0}, test_data.size()); - TEST_ASSERT(SSRingIndex{0} == data_slice.offset); - TEST_ASSERT_EQUAL_CHAR_ARRAY(test_data.data(), data_slice.data.data(), - test_data.size()); - } - // get a slice of data part - { - auto data_slice = - send_data_buffer.GetSlice(SSRingIndex{5}, test_data.size() - 5); - TEST_ASSERT(SSRingIndex{5} == data_slice.offset); - TEST_ASSERT_EQUAL_CHAR_ARRAY(test_data.data() + 5, data_slice.data.data(), - test_data.size() - 5); - } - // get a slice other two data parts - { - auto data_slice = - send_data_buffer.GetSlice(SSRingIndex{0}, test_data.size() * 2); - TEST_ASSERT(SSRingIndex{0} == data_slice.offset); - TEST_ASSERT_EQUAL_CHAR_ARRAY(test_data.data(), data_slice.data.data(), - test_data.size()); - TEST_ASSERT_EQUAL_CHAR_ARRAY(test_data.data(), - data_slice.data.data() + test_data.size(), - test_data.size()); - } -} - -void test_SendDataBufferConfirmStopReject() { - constexpr auto begin = SSRingIndex{0}; - - ActionProcessor action_processor; - ActionContext action_context{action_processor}; - - SendDataBuffer send_data_buffer{action_context}; - - auto a1_res = UpdateStatus{}; - auto a2_res = UpdateStatus{}; - auto a3_res = UpdateStatus{}; - - // add some data - auto a1 = send_data_buffer.AddData( - SendingData{SSRingIndex{0}, ToDataBuffer(test_data)}); - - a1_res.Subscribe(*a1); - - auto a2 = send_data_buffer.AddData( - SendingData{SSRingIndex{test_data.size()}, ToDataBuffer(test_data)}); - a2_res.Subscribe(*a2); - - auto a3 = send_data_buffer.AddData( - SendingData{SSRingIndex{2 * test_data.size()}, ToDataBuffer(test_data)}); - a3_res.Subscribe(*a3); - - action_processor.Update(Now()); - - // confirm some - auto ack_size_0 = send_data_buffer.Acknowledge(SSRingIndex{5}); - action_processor.Update(Now()); - TEST_ASSERT_EQUAL(5, ack_size_0); - TEST_ASSERT_FALSE(a1_res.ack); - - auto ack_size_1 = send_data_buffer.Acknowledge(SSRingIndex{test_data.size()}); - action_processor.Update(Now()); - TEST_ASSERT_EQUAL(test_data.size() - 5, ack_size_1); - TEST_ASSERT_TRUE(a1_res.ack); - - // reject some - auto rejected_0 = send_data_buffer.Reject(SSRingIndex{test_data.size()}); - action_processor.Update(Now()); - TEST_ASSERT_EQUAL(0, rejected_0); - TEST_ASSERT_FALSE(a1_res.rejected); - TEST_ASSERT_FALSE(a2_res.rejected); - - auto rejected_1 = send_data_buffer.Reject(SSRingIndex{test_data.size() + 1}); - action_processor.Update(Now()); - TEST_ASSERT_EQUAL(test_data.size(), rejected_1); - TEST_ASSERT_TRUE(a2_res.rejected); - - // stop some - auto stopped_0 = - send_data_buffer.Stop(SSRingIndex{(2 * test_data.size() - 1)}); - action_processor.Update(Now()); - TEST_ASSERT_EQUAL(0, stopped_0); - TEST_ASSERT_FALSE(a3_res.stopped); - - auto stopped_1 = send_data_buffer.Stop(SSRingIndex{2 * test_data.size()}); - action_processor.Update(Now()); - TEST_ASSERT_EQUAL(test_data.size(), stopped_1); - TEST_ASSERT_TRUE(a3_res.stopped); -} - -void test_SendDataBufferConfirmation() { - constexpr auto begin = SSRingIndex{0}; - - ActionProcessor action_processor; - ActionContext action_context{action_processor}; - - SendDataBuffer send_data_buffer{action_context}; - - std::array arr_res = {UpdateStatus{}, UpdateStatus{}, UpdateStatus{}}; - - for (std::size_t i = 0; i < arr_res.size(); ++i) { - auto action = send_data_buffer.AddData(SendingData{ - SSRingIndex{static_cast(test_data.size() * i)}, - ToDataBuffer(test_data)}); - action->StatusEvent().Subscribe( - OnResult{[&arr_res, i]() { arr_res[i].ack = true; }}); - } - - // confirm some data at the beginning - send_data_buffer.Acknowledge(SSRingIndex{5}); - action_processor.Update(Now()); - // non data should be confirmed - TEST_ASSERT_FALSE(arr_res[0].ack); - - // confirm almost first data - send_data_buffer.Acknowledge(SSRingIndex{test_data.size() - 2}); - action_processor.Update(Now()); - // non data should be confirmed - TEST_ASSERT_FALSE(arr_res[0].ack); - // confirm whole first data - send_data_buffer.Acknowledge(SSRingIndex{test_data.size()}); - action_processor.Update(Now()); - // now the data is confirmed - TEST_ASSERT_TRUE(arr_res[0].ack); - // confirm up to the half of third data - send_data_buffer.Acknowledge( - SSRingIndex{(test_data.size() * 2) + (test_data.size() / 2)}); - action_processor.Update(Now()); - // second is confirmed but the third is not - TEST_ASSERT_TRUE(arr_res[1].ack); - TEST_ASSERT_FALSE(arr_res[2].ack); - - // confirm all - send_data_buffer.Acknowledge(SSRingIndex{(test_data.size() * 3)}); - action_processor.Update(Now()); - TEST_ASSERT_TRUE(arr_res[2].ack); - - // add more data - for (std::size_t i = 0; i < arr_res.size(); ++i) { - arr_res[i] = {}; - auto action = send_data_buffer.AddData(SendingData{ - SSRingIndex{static_cast(test_data.size() * (3 + i))}, - ToDataBuffer(test_data)}); - action->StatusEvent().Subscribe( - OnResult{[&arr_res, i]() { arr_res[i].ack = true; }}); - } - - // confirm it all - send_data_buffer.Acknowledge(SSRingIndex{(test_data.size() * (3 + 3))}); - action_processor.Update(Now()); - TEST_ASSERT_TRUE(arr_res[0].ack); - TEST_ASSERT_TRUE(arr_res[1].ack); - TEST_ASSERT_TRUE(arr_res[2].ack); -} - -} // namespace ae::test_send_data_buffer - -int test_send_data_buffer() { - UNITY_BEGIN(); - RUN_TEST(ae::test_send_data_buffer::test_SendDataBufferGetSlice); - RUN_TEST(ae::test_send_data_buffer::test_SendDataBufferConfirmStopReject); - RUN_TEST(ae::test_send_data_buffer::test_SendDataBufferConfirmation); - return UNITY_END(); -} diff --git a/tests/test-stream/safe-stream/test_sending_chunk_list.cpp b/tests/test-stream/safe-stream/test_sending_chunk_list.cpp deleted file mode 100644 index b33bde8c..00000000 --- a/tests/test-stream/safe-stream/test_sending_chunk_list.cpp +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2025 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include - -#include "aether/stream_api/safe_stream/sending_chunk_list.h" - -namespace ae::test_sending_chunk_list { - -void test_SendingChunkList() { - constexpr auto begin = SSRingIndex{0}; - - SendingChunkList chunk_list{}; - - // add three chunks - chunk_list.Register(SSRingIndex{0}, SSRingIndex{5}, Now()); - chunk_list.Register(SSRingIndex{6}, SSRingIndex{10}, Now()); - chunk_list.Register(SSRingIndex{11}, SSRingIndex{20}, Now()); - - TEST_ASSERT_EQUAL(3, chunk_list.size()); - // merge two chunks - chunk_list.Register(SSRingIndex{0}, SSRingIndex{10}, Now()); - TEST_ASSERT_EQUAL(2, chunk_list.size()); - // merge again - chunk_list.Register(SSRingIndex{0}, SSRingIndex{20}, Now()); - TEST_ASSERT_EQUAL(1, chunk_list.size()); - // register a smaller chunk - chunk_list.Register(SSRingIndex{0}, SSRingIndex{10}, Now()); - TEST_ASSERT_EQUAL(2, chunk_list.size()); - // register a smaller between two chunks - chunk_list.Register(SSRingIndex{8}, SSRingIndex{14}, Now()); - TEST_ASSERT_EQUAL(3, chunk_list.size()); - auto& chunk = chunk_list.front(); - TEST_ASSERT(SSRingIndex{15} == chunk.offset_range.left); - TEST_ASSERT(SSRingIndex{20} == chunk.offset_range.right); - chunk_list.RemoveUpTo(SSRingIndex{7}); - TEST_ASSERT_EQUAL(2, chunk_list.size()); - chunk_list.RemoveUpTo(SSRingIndex{20}); - TEST_ASSERT(chunk_list.empty()); -} - -void test_SendingChunkListRepeatCount() { - constexpr auto begin = SSRingIndex{0}; - - SendingChunkList chunk_list{}; - { - auto& chunk1 = chunk_list.Register(SSRingIndex{0}, SSRingIndex{50}, Now()); - chunk1.repeat_count = 1; - auto& chunk2 = chunk_list.Register(SSRingIndex{51}, SSRingIndex{60}, Now()); - chunk2.repeat_count = 2; - auto& chunk3 = chunk_list.Register(SSRingIndex{61}, SSRingIndex{90}, Now()); - chunk3.repeat_count = 3; - } - // re register chunk - { - auto& chunk = chunk_list.front(); - TEST_ASSERT_EQUAL(1, chunk.repeat_count); - auto& chunk1 = chunk_list.Register(SSRingIndex{0}, SSRingIndex{50}, Now()); - TEST_ASSERT_EQUAL(1, chunk1.repeat_count); - auto& front_chunk = chunk_list.front(); - TEST_ASSERT_EQUAL(2, front_chunk.repeat_count); - } - // merge chunks - { - auto& chunk1 = chunk_list.Register(SSRingIndex{0}, SSRingIndex{60}, Now()); - TEST_ASSERT_EQUAL(1, chunk1.repeat_count); - auto& front_chunk = chunk_list.front(); - TEST_ASSERT_EQUAL(3, front_chunk.repeat_count); - } - // split chunks - { - auto& chunk1 = chunk_list.Register(SSRingIndex{0}, SSRingIndex{30}, Now()); - TEST_ASSERT_EQUAL(1, chunk1.repeat_count); - auto& front_chunk = chunk_list.front(); - TEST_ASSERT_EQUAL(3, front_chunk.repeat_count); - auto& chunk2 = chunk_list.Register(SSRingIndex{31}, SSRingIndex{60}, Now()); - TEST_ASSERT_EQUAL(1, chunk2.repeat_count); - chunk2.repeat_count = 2; - } -} - -void test_SendingChunkConfirmPartial() { - constexpr auto begin = SSRingIndex{0}; - - SendingChunkList chunk_list{}; - chunk_list.Register(SSRingIndex{0}, SSRingIndex{1000}, Now()); - // confirm part - chunk_list.RemoveUpTo(SSRingIndex{100}); - { - TEST_ASSERT_FALSE(chunk_list.empty()); - auto& front_chunk = chunk_list.front(); - TEST_ASSERT_EQUAL( - 100, static_cast(front_chunk.offset_range.left)); - } - // confirm all - chunk_list.RemoveUpTo(SSRingIndex{1000}); - { - TEST_ASSERT_TRUE(chunk_list.empty()); - } -} - -} // namespace ae::test_sending_chunk_list - -int test_sending_chunk_list() { - UNITY_BEGIN(); - RUN_TEST(ae::test_sending_chunk_list::test_SendingChunkList); - RUN_TEST(ae::test_sending_chunk_list::test_SendingChunkListRepeatCount); - RUN_TEST(ae::test_sending_chunk_list::test_SendingChunkConfirmPartial); - return UNITY_END(); -} diff --git a/tests/test-stream/stream-test-ctx.h b/tests/test-stream/stream-test-ctx.h index 0e0b883c..736333fc 100644 --- a/tests/test-stream/stream-test-ctx.h +++ b/tests/test-stream/stream-test-ctx.h @@ -23,8 +23,12 @@ namespace ae { struct TestContext { AeCtx ToAeContext() const { static constexpr auto table = - AeCtxTable{nullptr, [](void* obj) -> TaskScheduler& { + AeCtxTable{nullptr, + [](void* obj) -> TaskScheduler& { return static_cast(obj)->sched; + }, + [](void* obj) -> IndexRegistry& { + return static_cast(obj)->registry; }}; return AeCtx{ const_cast(this), // NOLINT @@ -38,6 +42,7 @@ struct TestContext { } TaskScheduler sched; + IndexRegistry registry; }; } // namespace ae diff --git a/tests/test-stream/to_data_buffer.h b/tests/test-stream/to_data_buffer.h index 8f7dcf7c..8e0c6d37 100644 --- a/tests/test-stream/to_data_buffer.h +++ b/tests/test-stream/to_data_buffer.h @@ -17,6 +17,7 @@ #ifndef TESTS_TEST_STREAM_TO_DATA_BUFFER_H_ #define TESTS_TEST_STREAM_TO_DATA_BUFFER_H_ +#include #include #include #include @@ -43,6 +44,11 @@ static auto ToDataBuffer(Iterator begin, Iterator end) { return ToVector(begin, end); } +static auto ToSpan(std::string_view str) { + return std::span( + reinterpret_cast(str.data()), str.size()); +} + } // namespace ae #endif // TESTS_TEST_STREAM_TO_DATA_BUFFER_H_