diff --git a/.github/workflows/diff.yaml b/.github/workflows/diff.yaml index bf6a39147..ce2caea75 100644 --- a/.github/workflows/diff.yaml +++ b/.github/workflows/diff.yaml @@ -171,7 +171,7 @@ jobs: # Run leftover CTest tests (all except unit and benchmark tests). cd build - ctest -E "(memgraph__unit|memgraph__benchmark)" --output-on-failure + ctest -E "(memgraph__unit|memgraph__benchmark|memgraph__simulation)" --output-on-failure - name: Run drivers tests run: | @@ -262,6 +262,15 @@ jobs: cd build ctest -R memgraph__unit --output-on-failure -j$THREADS + - name: Run simulation tests + run: | + # Activate toolchain. + source /opt/toolchain-v4/activate + + # Run unit tests. + cd build + ctest -R memgraph__simulation --output-on-failure -j$THREADS + - name: Run e2e tests run: | # TODO(gitbuda): Setup mgclient and pymgclient properly. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index efc653b9a..f4c303daf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,6 +5,7 @@ add_subdirectory(lisp) add_subdirectory(utils) add_subdirectory(requests) add_subdirectory(io) +add_subdirectory(io/simulator) add_subdirectory(kvstore) add_subdirectory(telemetry) add_subdirectory(communication) diff --git a/src/io/address.hpp b/src/io/address.hpp new file mode 100644 index 000000000..94a231e07 --- /dev/null +++ b/src/io/address.hpp @@ -0,0 +1,60 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#pragma once + +#include + +#include +#include +#include +#include + +namespace memgraph::io { +struct Address { + // It's important for all participants to have a + // unique identifier - IP and port alone are not + // enough, and may change over the lifecycle of + // the nodes. Particularly storage nodes may change + // their IP addresses over time, and the system + // should gracefully update its information + // about them. + boost::uuids::uuid unique_id; + boost::asio::ip::address last_known_ip; + uint16_t last_known_port; + + static Address TestAddress(uint16_t port) { + Address ret; + ret.last_known_port = port; + return ret; + } + + friend bool operator==(const Address &lhs, const Address &rhs) = default; + + /// unique_id is most dominant for ordering, then last_known_ip, then last_known_port + friend bool operator<(const Address &lhs, const Address &rhs) { + if (lhs.unique_id != rhs.unique_id) { + return lhs.unique_id < rhs.unique_id; + } + + if (lhs.last_known_ip != rhs.last_known_ip) { + return lhs.last_known_ip < rhs.last_known_ip; + } + + return lhs.last_known_port < rhs.last_known_port; + } + + std::string ToString() const { + return fmt::format("Address {{ unique_id: {}, last_known_ip: {}, last_known_port: {} }}", + boost::uuids::to_string(unique_id), last_known_ip.to_string(), last_known_port); + } +}; +}; // namespace memgraph::io diff --git a/src/io/errors.hpp b/src/io/errors.hpp new file mode 100644 index 000000000..7df2171d9 --- /dev/null +++ b/src/io/errors.hpp @@ -0,0 +1,26 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#pragma once + +namespace memgraph::io { +// Signifies that a retriable operation was unable to +// complete after a configured number of retries. +struct RetriesExhausted {}; + +// Signifies that a request was unable to receive a response +// within some configured timeout duration. It is important +// to remember that in distributed systems, a timeout does +// not signify that a request was not received or processed. +// It may be the case that the request was fully processed +// but that the response was not received. +struct TimedOut {}; +}; // namespace memgraph::io diff --git a/src/io/future.hpp b/src/io/future.hpp new file mode 100644 index 000000000..7b9a4461c --- /dev/null +++ b/src/io/future.hpp @@ -0,0 +1,262 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "io/errors.hpp" +#include "utils/logging.hpp" + +namespace memgraph::io { + +// Shared is in an anonymous namespace, and the only way to +// construct a Promise or Future is to pass a Shared in. This +// ensures that Promises and Futures can only be constructed +// in this translation unit. +namespace details { +template +class Shared { + mutable std::condition_variable cv_; + mutable std::mutex mu_; + std::optional item_; + bool consumed_ = false; + bool waiting_ = false; + std::function simulator_notifier_ = nullptr; + + public: + explicit Shared(std::function simulator_notifier) : simulator_notifier_(simulator_notifier) {} + Shared() = default; + Shared(Shared &&) = delete; + Shared &operator=(Shared &&) = delete; + Shared(const Shared &) = delete; + Shared &operator=(const Shared &) = delete; + ~Shared() = default; + + /// Takes the item out of our optional item_ and returns it. + T Take() { + MG_ASSERT(item_, "Take called without item_ being present"); + MG_ASSERT(!consumed_, "Take called on already-consumed Future"); + + T ret = std::move(item_).value(); + item_.reset(); + + consumed_ = true; + + return ret; + } + + T Wait() { + std::unique_lock lock(mu_); + waiting_ = true; + + while (!item_) { + bool simulator_progressed = false; + if (simulator_notifier_) [[unlikely]] { + // We can't hold our own lock while notifying + // the simulator because notifying the simulator + // involves acquiring the simulator's mutex + // to guarantee that our notification linearizes + // with the simulator's condition variable. + // However, the simulator may acquire our + // mutex to check if we are being awaited, + // while determining system quiescence, + // so we have to get out of its way to avoid + // a cyclical deadlock. + lock.unlock(); + simulator_progressed = std::invoke(simulator_notifier_); + lock.lock(); + if (item_) { + // item may have been filled while we + // had dropped our mutex while notifying + // the simulator of our waiting_ status. + break; + } + } + if (!simulator_progressed) [[likely]] { + cv_.wait(lock); + } + MG_ASSERT(!consumed_, "Future consumed twice!"); + } + + waiting_ = false; + + return Take(); + } + + bool IsReady() const { + std::unique_lock lock(mu_); + return item_; + } + + std::optional TryGet() { + std::unique_lock lock(mu_); + + if (item_) { + return Take(); + } + + return std::nullopt; + } + + void Fill(T item) { + { + std::unique_lock lock(mu_); + + MG_ASSERT(!consumed_, "Promise filled after it was already consumed!"); + MG_ASSERT(!item_, "Promise filled twice!"); + + item_ = item; + } // lock released before condition variable notification + + cv_.notify_all(); + } + + bool IsAwaited() const { + std::unique_lock lock(mu_); + return waiting_; + } +}; +} // namespace details + +template +class Future { + bool consumed_or_moved_ = false; + std::shared_ptr> shared_; + + public: + explicit Future(std::shared_ptr> shared) : shared_(shared) {} + + Future() = delete; + Future(Future &&old) noexcept { + MG_ASSERT(!old.consumed_or_moved_, "Future moved from after already being moved from or consumed."); + shared_ = std::move(old.shared_); + consumed_or_moved_ = old.consumed_or_moved_; + old.consumed_or_moved_ = true; + } + + Future &operator=(Future &&old) noexcept { + MG_ASSERT(!old.consumed_or_moved_, "Future moved from after already being moved from or consumed."); + shared_ = std::move(old.shared_); + old.consumed_or_moved_ = true; + } + + Future(const Future &) = delete; + Future &operator=(const Future &) = delete; + ~Future() = default; + + /// Returns true if the Future is ready to + /// be consumed using TryGet or Wait (prefer Wait + /// if you know it's ready, because it doesn't + /// return an optional. + bool IsReady() { + MG_ASSERT(!consumed_or_moved_, "Called IsReady after Future already consumed!"); + return shared_->IsReady(); + } + + /// Non-blocking method that returns the inner + /// item if it's already ready, or std::nullopt + /// if it is not ready yet. + std::optional TryGet() { + MG_ASSERT(!consumed_or_moved_, "Called TryGet after Future already consumed!"); + std::optional ret = shared_->TryGet(); + if (ret) { + consumed_or_moved_ = true; + } + return ret; + } + + /// Block on the corresponding promise to be filled, + /// returning the inner item when ready. + T Wait() && { + MG_ASSERT(!consumed_or_moved_, "Future should only be consumed with Wait once!"); + T ret = shared_->Wait(); + consumed_or_moved_ = true; + return ret; + } + + /// Marks this Future as canceled. + void Cancel() { + MG_ASSERT(!consumed_or_moved_, "Future::Cancel called on a future that was already moved or consumed!"); + consumed_or_moved_ = true; + } +}; + +template +class Promise { + std::shared_ptr> shared_; + bool filled_or_moved_ = false; + + public: + explicit Promise(std::shared_ptr> shared) : shared_(shared) {} + + Promise() = delete; + Promise(Promise &&old) noexcept { + MG_ASSERT(!old.filled_or_moved_, "Promise moved from after already being moved from or filled."); + shared_ = std::move(old.shared_); + old.filled_or_moved_ = true; + } + + Promise &operator=(Promise &&old) noexcept { + MG_ASSERT(!old.filled_or_moved_, "Promise moved from after already being moved from or filled."); + shared_ = std::move(old.shared_); + old.filled_or_moved_ = true; + } + Promise(const Promise &) = delete; + Promise &operator=(const Promise &) = delete; + + ~Promise() { MG_ASSERT(filled_or_moved_, "Promise destroyed before its associated Future was filled!"); } + + // Fill the expected item into the Future. + void Fill(T item) { + MG_ASSERT(!filled_or_moved_, "Promise::Fill called on a promise that is already filled or moved!"); + shared_->Fill(item); + filled_or_moved_ = true; + } + + bool IsAwaited() { return shared_->IsAwaited(); } + + /// Moves this Promise into a unique_ptr. + std::unique_ptr> ToUnique() && { + std::unique_ptr> up = std::make_unique>(std::move(shared_)); + + filled_or_moved_ = true; + + return up; + } +}; + +template +std::pair, Promise> FuturePromisePair() { + std::shared_ptr> shared = std::make_shared>(); + + Future future = Future(shared); + Promise promise = Promise(shared); + + return std::make_pair(std::move(future), std::move(promise)); +} + +template +std::pair, Promise> FuturePromisePairWithNotifier(std::function simulator_notifier) { + std::shared_ptr> shared = std::make_shared>(simulator_notifier); + + Future future = Future(shared); + Promise promise = Promise(shared); + + return std::make_pair(std::move(future), std::move(promise)); +} + +}; // namespace memgraph::io diff --git a/src/io/simulator/CMakeLists.txt b/src/io/simulator/CMakeLists.txt new file mode 100644 index 000000000..1cb61d8d9 --- /dev/null +++ b/src/io/simulator/CMakeLists.txt @@ -0,0 +1,8 @@ +set(io_simulator_sources + simulator_handle.cpp) + +find_package(fmt REQUIRED) +find_package(Threads REQUIRED) + +add_library(mg-io-simulator STATIC ${io_simulator_sources}) +target_link_libraries(mg-io-simulator stdc++fs Threads::Threads fmt::fmt mg-utils) diff --git a/src/io/simulator/message_conversion.hpp b/src/io/simulator/message_conversion.hpp new file mode 100644 index 000000000..f16c60f65 --- /dev/null +++ b/src/io/simulator/message_conversion.hpp @@ -0,0 +1,177 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#pragma once + +#include "io/transport.hpp" + +namespace memgraph::io::simulator { + +using memgraph::io::Duration; +using memgraph::io::Message; +using memgraph::io::Time; + +struct OpaqueMessage { + Address from_address; + uint64_t request_id; + std::any message; + + /// Recursively tries to match a specific type from the outer + /// variant's parameter pack against the type of the std::any, + /// and if it matches, make it concrete and return it. Otherwise, + /// move on and compare the any with the next type from the + /// parameter pack. + /// + /// Return is the full std::variant type that holds the + /// full parameter pack without interfering with recursive + /// narrowing expansion. + template + std::optional Unpack(std::any &&a) { + if (typeid(Head) == a.type()) { + Head concrete = std::any_cast(std::move(a)); + return concrete; + } + + if constexpr (sizeof...(Rest) > 0) { + return Unpack(std::move(a)); + } else { + return std::nullopt; + } + } + + /// High level "user-facing" conversion function that lets + /// people interested in conversion only supply a single + /// parameter pack for the types that they want to compare + /// with the any and potentially include in the returned + /// variant. + template + requires(sizeof...(Ms) > 0) std::optional> VariantFromAny(std::any &&a) { + return Unpack, Ms...>(std::move(a)); + } + + template + requires(sizeof...(Ms) > 0) std::optional> Take() && { + std::optional> m_opt = VariantFromAny(std::move(message)); + + if (m_opt) { + return RequestEnvelope{ + .message = std::move(*m_opt), + .request_id = request_id, + .from_address = from_address, + }; + } + + return std::nullopt; + } +}; + +class OpaquePromiseTraitBase { + public: + virtual const std::type_info *TypeInfo() const = 0; + virtual bool IsAwaited(void *ptr) const = 0; + virtual void Fill(void *ptr, OpaqueMessage &&) const = 0; + virtual void TimeOut(void *ptr) const = 0; + + virtual ~OpaquePromiseTraitBase() = default; + OpaquePromiseTraitBase() = default; + OpaquePromiseTraitBase(const OpaquePromiseTraitBase &) = delete; + OpaquePromiseTraitBase &operator=(const OpaquePromiseTraitBase &) = delete; + OpaquePromiseTraitBase(OpaquePromiseTraitBase &&old) = delete; + OpaquePromiseTraitBase &operator=(OpaquePromiseTraitBase &&) = delete; +}; + +template +class OpaquePromiseTrait : public OpaquePromiseTraitBase { + public: + const std::type_info *TypeInfo() const override { return &typeid(T); }; + + bool IsAwaited(void *ptr) const override { return static_cast *>(ptr)->IsAwaited(); }; + + void Fill(void *ptr, OpaqueMessage &&opaque_message) const override { + T message = std::any_cast(std::move(opaque_message.message)); + auto response_envelope = ResponseEnvelope{.message = std::move(message), + .request_id = opaque_message.request_id, + .from_address = opaque_message.from_address}; + auto promise = static_cast *>(ptr); + auto unique_promise = std::unique_ptr>(promise); + unique_promise->Fill(std::move(response_envelope)); + }; + + void TimeOut(void *ptr) const override { + auto promise = static_cast *>(ptr); + auto unique_promise = std::unique_ptr>(promise); + ResponseResult result = TimedOut{}; + unique_promise->Fill(std::move(result)); + } +}; + +class OpaquePromise { + void *ptr_; + std::unique_ptr trait_; + + public: + OpaquePromise(OpaquePromise &&old) noexcept : ptr_(old.ptr_), trait_(std::move(old.trait_)) { + MG_ASSERT(old.ptr_ != nullptr); + old.ptr_ = nullptr; + } + + OpaquePromise &operator=(OpaquePromise &&old) noexcept { + MG_ASSERT(ptr_ == nullptr); + MG_ASSERT(old.ptr_ != nullptr); + MG_ASSERT(this != &old); + ptr_ = old.ptr_; + trait_ = std::move(old.trait_); + old.ptr_ = nullptr; + return *this; + } + + OpaquePromise(const OpaquePromise &) = delete; + OpaquePromise &operator=(const OpaquePromise &) = delete; + + template + std::unique_ptr> Take() && { + MG_ASSERT(typeid(T) == *trait_->TypeInfo()); + MG_ASSERT(ptr_ != nullptr); + + auto ptr = static_cast *>(ptr_); + + ptr_ = nullptr; + + return std::unique_ptr(ptr); + } + + template + explicit OpaquePromise(std::unique_ptr> promise) + : ptr_(static_cast(promise.release())), trait_(std::make_unique>()) {} + + bool IsAwaited() { + MG_ASSERT(ptr_ != nullptr); + return trait_->IsAwaited(ptr_); + } + + void TimeOut() { + MG_ASSERT(ptr_ != nullptr); + trait_->TimeOut(ptr_); + ptr_ = nullptr; + } + + void Fill(OpaqueMessage &&opaque_message) { + MG_ASSERT(ptr_ != nullptr); + trait_->Fill(ptr_, std::move(opaque_message)); + ptr_ = nullptr; + } + + ~OpaquePromise() { + MG_ASSERT(ptr_ == nullptr, "OpaquePromise destroyed without being explicitly timed out or filled"); + } +}; + +} // namespace memgraph::io::simulator diff --git a/src/io/simulator/simulator.hpp b/src/io/simulator/simulator.hpp new file mode 100644 index 000000000..354aae6ac --- /dev/null +++ b/src/io/simulator/simulator.hpp @@ -0,0 +1,45 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#pragma once + +#include +#include + +#include "io/address.hpp" +#include "io/simulator/simulator_config.hpp" +#include "io/simulator/simulator_handle.hpp" +#include "io/simulator/simulator_transport.hpp" + +namespace memgraph::io::simulator { +class Simulator { + std::mt19937 rng_; + std::shared_ptr simulator_handle_; + + public: + explicit Simulator(SimulatorConfig config) + : rng_(std::mt19937{config.rng_seed}), simulator_handle_{std::make_shared(config)} {} + + void ShutDown() { simulator_handle_->ShutDown(); } + + Io Register(Address address) { + std::uniform_int_distribution seed_distrib; + uint64_t seed = seed_distrib(rng_); + return Io{SimulatorTransport{simulator_handle_, address, seed}, address}; + } + + void IncrementServerCountAndWaitForQuiescentState(Address address) { + simulator_handle_->IncrementServerCountAndWaitForQuiescentState(address); + } + + SimulatorStats Stats() { return simulator_handle_->Stats(); } +}; +}; // namespace memgraph::io::simulator diff --git a/src/io/simulator/simulator_config.hpp b/src/io/simulator/simulator_config.hpp new file mode 100644 index 000000000..4719488d2 --- /dev/null +++ b/src/io/simulator/simulator_config.hpp @@ -0,0 +1,30 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#pragma once + +#include + +#include "io/time.hpp" + +namespace memgraph::io::simulator { + +using memgraph::io::Time; + +struct SimulatorConfig { + uint8_t drop_percent = 0; + bool perform_timeouts = false; + bool scramble_messages = true; + uint64_t rng_seed = 0; + Time start_time = Time::min(); + Time abort_time = Time::max(); +}; +}; // namespace memgraph::io::simulator diff --git a/src/io/simulator/simulator_handle.cpp b/src/io/simulator/simulator_handle.cpp new file mode 100644 index 000000000..05585f551 --- /dev/null +++ b/src/io/simulator/simulator_handle.cpp @@ -0,0 +1,154 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#include "io/simulator/simulator_handle.hpp" +#include "io/address.hpp" +#include "io/errors.hpp" +#include "io/simulator/simulator_config.hpp" +#include "io/simulator/simulator_stats.hpp" +#include "io/time.hpp" +#include "io/transport.hpp" + +namespace memgraph::io::simulator { + +using memgraph::io::Duration; +using memgraph::io::Time; + +void SimulatorHandle::ShutDown() { + std::unique_lock lock(mu_); + should_shut_down_ = true; + cv_.notify_all(); +} + +bool SimulatorHandle::ShouldShutDown() const { + std::unique_lock lock(mu_); + return should_shut_down_; +} + +void SimulatorHandle::IncrementServerCountAndWaitForQuiescentState(Address address) { + std::unique_lock lock(mu_); + server_addresses_.insert(address); + + while (true) { + const size_t blocked_servers = BlockedServers(); + + const bool all_servers_blocked = blocked_servers == server_addresses_.size(); + + if (all_servers_blocked) { + return; + } + + cv_.wait(lock); + } +} + +size_t SimulatorHandle::BlockedServers() { + size_t blocked_servers = blocked_on_receive_; + + for (auto &[promise_key, opaque_promise] : promises_) { + if (opaque_promise.promise.IsAwaited() && server_addresses_.contains(promise_key.requester_address)) { + blocked_servers++; + } + } + + return blocked_servers; +} + +bool SimulatorHandle::MaybeTickSimulator() { + std::unique_lock lock(mu_); + + const size_t blocked_servers = BlockedServers(); + + if (blocked_servers < server_addresses_.size()) { + // we only need to advance the simulator when all + // servers have reached a quiescent state, blocked + // on their own futures or receive methods. + return false; + } + + stats_.simulator_ticks++; + + cv_.notify_all(); + + TimeoutPromisesPastDeadline(); + + if (in_flight_.empty()) { + // return early here because there are no messages to schedule + + // We tick the clock forward when all servers are blocked but + // there are no in-flight messages to schedule delivery of. + std::poisson_distribution<> time_distrib(50); + Duration clock_advance = std::chrono::microseconds{time_distrib(rng_)}; + cluster_wide_time_microseconds_ += clock_advance; + + MG_ASSERT(cluster_wide_time_microseconds_ < config_.abort_time, + "Cluster has executed beyond its configured abort_time, and something may be failing to make progress " + "in an expected amount of time."); + return true; + } + + if (config_.scramble_messages) { + // scramble messages + std::uniform_int_distribution swap_distrib(0, in_flight_.size() - 1); + const size_t swap_index = swap_distrib(rng_); + std::swap(in_flight_[swap_index], in_flight_.back()); + } + + auto [to_address, opaque_message] = std::move(in_flight_.back()); + in_flight_.pop_back(); + + std::uniform_int_distribution drop_distrib(0, 99); + const int drop_threshold = drop_distrib(rng_); + const bool should_drop = drop_threshold < config_.drop_percent; + + if (should_drop) { + stats_.dropped_messages++; + } + + PromiseKey promise_key{.requester_address = to_address, + .request_id = opaque_message.request_id, + .replier_address = opaque_message.from_address}; + + if (promises_.contains(promise_key)) { + // complete waiting promise if it's there + DeadlineAndOpaquePromise dop = std::move(promises_.at(promise_key)); + promises_.erase(promise_key); + + const bool normal_timeout = config_.perform_timeouts && (dop.deadline < cluster_wide_time_microseconds_); + + if (should_drop || normal_timeout) { + stats_.timed_out_requests++; + dop.promise.TimeOut(); + } else { + stats_.total_responses++; + dop.promise.Fill(std::move(opaque_message)); + } + } else if (should_drop) { + // don't add it anywhere, let it drop + } else { + // add to can_receive_ if not + const auto &[om_vec, inserted] = can_receive_.try_emplace(to_address, std::vector()); + om_vec->second.emplace_back(std::move(opaque_message)); + } + + return true; +} + +Time SimulatorHandle::Now() const { + std::unique_lock lock(mu_); + return cluster_wide_time_microseconds_; +} + +SimulatorStats SimulatorHandle::Stats() { + std::unique_lock lock(mu_); + return stats_; +} +} // namespace memgraph::io::simulator diff --git a/src/io/simulator/simulator_handle.hpp b/src/io/simulator/simulator_handle.hpp new file mode 100644 index 000000000..6abaa129d --- /dev/null +++ b/src/io/simulator/simulator_handle.hpp @@ -0,0 +1,206 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "io/address.hpp" +#include "io/errors.hpp" +#include "io/simulator/message_conversion.hpp" +#include "io/simulator/simulator_config.hpp" +#include "io/simulator/simulator_stats.hpp" +#include "io/time.hpp" +#include "io/transport.hpp" + +namespace memgraph::io::simulator { + +using memgraph::io::Duration; +using memgraph::io::Time; + +struct PromiseKey { + Address requester_address; + uint64_t request_id; + // TODO(tyler) possibly remove replier_address from promise key + // once we want to support DSR. + Address replier_address; + + public: + friend bool operator<(const PromiseKey &lhs, const PromiseKey &rhs) { + if (lhs.requester_address != rhs.requester_address) { + return lhs.requester_address < rhs.requester_address; + } + + if (lhs.request_id != rhs.request_id) { + return lhs.request_id < rhs.request_id; + } + + return lhs.replier_address < rhs.replier_address; + } +}; + +struct DeadlineAndOpaquePromise { + Time deadline; + OpaquePromise promise; +}; + +class SimulatorHandle { + mutable std::mutex mu_{}; + mutable std::condition_variable cv_; + + // messages that have not yet been scheduled or dropped + std::vector> in_flight_; + + // the responses to requests that are being waited on + std::map promises_; + + // messages that are sent to servers that may later receive them + std::map> can_receive_; + + Time cluster_wide_time_microseconds_; + bool should_shut_down_ = false; + SimulatorStats stats_; + size_t blocked_on_receive_ = 0; + std::set
server_addresses_; + std::mt19937 rng_; + SimulatorConfig config_; + + /// Returns the number of servers currently blocked on Receive, plus + /// the servers that are blocked on Futures that were created through + /// SimulatorTransport::Request. + /// + /// TODO(tyler) investigate whether avoiding consideration of Futures + /// increases determinism. + size_t BlockedServers(); + + void TimeoutPromisesPastDeadline() { + const Time now = cluster_wide_time_microseconds_; + + for (auto &[promise_key, dop] : promises_) { + if (dop.deadline < now) { + spdlog::debug("timing out request from requester {} to replier {}.", promise_key.requester_address.ToString(), + promise_key.replier_address.ToString()); + std::move(dop).promise.TimeOut(); + promises_.erase(promise_key); + + stats_.timed_out_requests++; + } + } + } + + public: + explicit SimulatorHandle(SimulatorConfig config) + : cluster_wide_time_microseconds_(config.start_time), rng_(config.rng_seed), config_(config) {} + + void IncrementServerCountAndWaitForQuiescentState(Address address); + + /// This method causes most of the interesting simulation logic to happen, wrt network behavior. + /// It checks to see if all background "server" threads are blocked on new messages, and if so, + /// it will decide whether to drop, reorder, or deliver in-flight messages based on the SimulatorConfig + /// that was used to create the Simulator. + bool MaybeTickSimulator(); + + void ShutDown(); + + bool ShouldShutDown() const; + + template + void SubmitRequest(Address to_address, Address from_address, uint64_t request_id, Request &&request, Duration timeout, + ResponsePromise &&promise) { + std::unique_lock lock(mu_); + + const Time deadline = cluster_wide_time_microseconds_ + timeout; + + std::any message(request); + OpaqueMessage om{.from_address = from_address, .request_id = request_id, .message = std::move(message)}; + in_flight_.emplace_back(std::make_pair(to_address, std::move(om))); + + PromiseKey promise_key{.requester_address = from_address, .request_id = request_id, .replier_address = to_address}; + OpaquePromise opaque_promise(std::move(promise).ToUnique()); + DeadlineAndOpaquePromise dop{.deadline = deadline, .promise = std::move(opaque_promise)}; + promises_.emplace(std::move(promise_key), std::move(dop)); + + stats_.total_messages++; + stats_.total_requests++; + + cv_.notify_all(); + } + + template + requires(sizeof...(Ms) > 0) RequestResult Receive(const Address &receiver, Duration timeout) { + std::unique_lock lock(mu_); + + blocked_on_receive_ += 1; + + const Time deadline = cluster_wide_time_microseconds_ + timeout; + + while (!should_shut_down_ && (cluster_wide_time_microseconds_ < deadline)) { + if (can_receive_.contains(receiver)) { + std::vector &can_rx = can_receive_.at(receiver); + if (!can_rx.empty()) { + OpaqueMessage message = std::move(can_rx.back()); + can_rx.pop_back(); + + // TODO(tyler) search for item in can_receive_ that matches the desired types, rather + // than asserting that the last item in can_rx matches. + auto m_opt = std::move(message).Take(); + + blocked_on_receive_ -= 1; + + return std::move(m_opt).value(); + } + } + + lock.unlock(); + bool made_progress = MaybeTickSimulator(); + lock.lock(); + if (!should_shut_down_ && !made_progress) { + cv_.wait(lock); + } + } + + blocked_on_receive_ -= 1; + + return TimedOut{}; + } + + template + void Send(Address to_address, Address from_address, uint64_t request_id, M message) { + std::unique_lock lock(mu_); + std::any message_any(std::move(message)); + OpaqueMessage om{.from_address = from_address, .request_id = request_id, .message = std::move(message_any)}; + in_flight_.emplace_back(std::make_pair(std::move(to_address), std::move(om))); + + stats_.total_messages++; + + cv_.notify_all(); + } + + Time Now() const; + + template , class Return = uint64_t> + Return Rand(D distrib) { + std::unique_lock lock(mu_); + return distrib(rng_); + } + + SimulatorStats Stats(); +}; +}; // namespace memgraph::io::simulator diff --git a/src/io/simulator/simulator_stats.hpp b/src/io/simulator/simulator_stats.hpp new file mode 100644 index 000000000..7f529a456 --- /dev/null +++ b/src/io/simulator/simulator_stats.hpp @@ -0,0 +1,25 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#pragma once + +#include + +namespace memgraph::io::simulator { +struct SimulatorStats { + uint64_t total_messages = 0; + uint64_t dropped_messages = 0; + uint64_t timed_out_requests = 0; + uint64_t total_requests = 0; + uint64_t total_responses = 0; + uint64_t simulator_ticks = 0; +}; +}; // namespace memgraph::io::simulator diff --git a/src/io/simulator/simulator_transport.hpp b/src/io/simulator/simulator_transport.hpp new file mode 100644 index 000000000..b67371ff0 --- /dev/null +++ b/src/io/simulator/simulator_transport.hpp @@ -0,0 +1,65 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#pragma once + +#include +#include + +#include "io/address.hpp" +#include "io/simulator/simulator_handle.hpp" +#include "io/time.hpp" + +namespace memgraph::io::simulator { + +using memgraph::io::Duration; +using memgraph::io::Time; + +class SimulatorTransport { + std::shared_ptr simulator_handle_; + const Address address_; + std::mt19937 rng_; + + public: + SimulatorTransport(std::shared_ptr simulator_handle, Address address, uint64_t seed) + : simulator_handle_(simulator_handle), address_(address), rng_(std::mt19937{seed}) {} + + template + ResponseFuture Request(Address address, uint64_t request_id, Request request, Duration timeout) { + std::function maybe_tick_simulator = [this] { return simulator_handle_->MaybeTickSimulator(); }; + auto [future, promise] = + memgraph::io::FuturePromisePairWithNotifier>(maybe_tick_simulator); + + simulator_handle_->SubmitRequest(address, address_, request_id, std::move(request), timeout, std::move(promise)); + + return std::move(future); + } + + template + requires(sizeof...(Ms) > 0) RequestResult Receive(Duration timeout) { + return simulator_handle_->template Receive(address_, timeout); + } + + template + void Send(Address address, uint64_t request_id, M message) { + return simulator_handle_->template Send(address, address_, request_id, message); + } + + Time Now() const { return simulator_handle_->Now(); } + + bool ShouldShutDown() const { return simulator_handle_->ShouldShutDown(); } + + template , class Return = uint64_t> + Return Rand(D distrib) { + return distrib(rng_); + } +}; +}; // namespace memgraph::io::simulator diff --git a/src/io/time.hpp b/src/io/time.hpp new file mode 100644 index 000000000..57f58cab1 --- /dev/null +++ b/src/io/time.hpp @@ -0,0 +1,21 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#pragma once + +#include + +namespace memgraph::io { + +using Duration = std::chrono::microseconds; +using Time = std::chrono::time_point; + +} // namespace memgraph::io diff --git a/src/io/transport.hpp b/src/io/transport.hpp new file mode 100644 index 000000000..a9e550434 --- /dev/null +++ b/src/io/transport.hpp @@ -0,0 +1,130 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#pragma once + +#include +#include +#include +#include + +#include "io/address.hpp" +#include "io/errors.hpp" +#include "io/future.hpp" +#include "io/time.hpp" +#include "utils/result.hpp" + +namespace memgraph::io { + +using memgraph::utils::BasicResult; + +// TODO(tyler) ensure that Message continues to represent +// reasonable constraints around message types over time, +// as we adapt things to use Thrift-generated message types. +template +concept Message = std::same_as>; + +template +struct ResponseEnvelope { + M message; + uint64_t request_id; + Address from_address; +}; + +template +using ResponseResult = BasicResult>; + +template +using ResponseFuture = memgraph::io::Future>; + +template +using ResponsePromise = memgraph::io::Promise>; + +template +struct RequestEnvelope { + std::variant message; + uint64_t request_id; + Address from_address; +}; + +template +using RequestResult = BasicResult>; + +template +class Io { + I implementation_; + Address address_; + uint64_t request_id_counter_ = 0; + Duration default_timeout_ = std::chrono::microseconds{50000}; + + public: + Io(I io, Address address) : implementation_(io), address_(address) {} + + /// Set the default timeout for all requests that are issued + /// without an explicit timeout set. + void SetDefaultTimeout(Duration timeout) { default_timeout_ = timeout; } + + /// Issue a request with an explicit timeout in microseconds provided. This tends to be used by clients. + template + ResponseFuture RequestWithTimeout(Address address, Request request, Duration timeout) { + const uint64_t request_id = ++request_id_counter_; + return implementation_.template Request(address, request_id, request, timeout); + } + + /// Issue a request that times out after the default timeout. This tends + /// to be used by clients. + template + ResponseFuture Request(Address address, Request request) { + const uint64_t request_id = ++request_id_counter_; + const Duration timeout = default_timeout_; + return implementation_.template Request(address, request_id, std::move(request), timeout); + } + + /// Wait for an explicit number of microseconds for a request of one of the + /// provided types to arrive. This tends to be used by servers. + template + RequestResult ReceiveWithTimeout(Duration timeout) { + return implementation_.template Receive(timeout); + } + + /// Wait the default number of microseconds for a request of one of the + /// provided types to arrive. This tends to be used by servers. + template + requires(sizeof...(Ms) > 0) RequestResult Receive() { + const Duration timeout = default_timeout_; + return implementation_.template Receive(timeout); + } + + /// Send a message in a best-effort fashion. This is used for messaging where + /// responses are not necessarily expected, and for servers to respond to requests. + /// If you need reliable delivery, this must be built on-top. TCP is not enough for most use cases. + template + void Send(Address address, uint64_t request_id, M message) { + return implementation_.template Send(address, request_id, std::move(message)); + } + + /// The current system time. This time source should be preferred over any other, + /// because it lets us deterministically control clocks from tests for making + /// things like timeouts deterministic. + Time Now() const { return implementation_.Now(); } + + /// Returns true if the system should shut-down. + bool ShouldShutDown() const { return implementation_.ShouldShutDown(); } + + /// Returns a random number within the specified distribution. + template , class Return = uint64_t> + Return Rand(D distrib) { + return implementation_.template Rand(distrib); + } + + Address GetAddress() { return address_; } +}; +}; // namespace memgraph::io diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 02535dcc7..664c010c8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -10,6 +10,9 @@ add_subdirectory(stress) # concurrent test binaries add_subdirectory(concurrent) +# simulation test binaries +add_subdirectory(simulation) + # manual test binaries add_subdirectory(manual) diff --git a/tests/benchmark/CMakeLists.txt b/tests/benchmark/CMakeLists.txt index 4bf8374b0..31f0eebc0 100644 --- a/tests/benchmark/CMakeLists.txt +++ b/tests/benchmark/CMakeLists.txt @@ -62,3 +62,6 @@ target_link_libraries(${test_prefix}storage_v2_gc mg-storage-v2) add_benchmark(storage_v2_property_store.cpp) target_link_libraries(${test_prefix}storage_v2_property_store mg-storage-v2) + +add_benchmark(future.cpp) +target_link_libraries(${test_prefix}future mg-io) diff --git a/tests/benchmark/future.cpp b/tests/benchmark/future.cpp new file mode 100644 index 000000000..abbe3fb98 --- /dev/null +++ b/tests/benchmark/future.cpp @@ -0,0 +1,30 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#include + +#include "io/future.hpp" + +static void FuturePairFillWait(benchmark::State &state) { + uint64_t counter = 0; + while (state.KeepRunning()) { + auto [future, promise] = memgraph::io::FuturePromisePair(); + promise.Fill(1); + std::move(future).Wait(); + + ++counter; + } + state.SetItemsProcessed(counter); +} + +BENCHMARK(FuturePairFillWait)->Unit(benchmark::kNanosecond)->UseRealTime(); + +BENCHMARK_MAIN(); diff --git a/tests/simulation/CMakeLists.txt b/tests/simulation/CMakeLists.txt new file mode 100644 index 000000000..142657401 --- /dev/null +++ b/tests/simulation/CMakeLists.txt @@ -0,0 +1,30 @@ +set(test_prefix memgraph__simulation__) + +find_package(gflags) + +add_custom_target(memgraph__simulation) + +function(add_simulation_test test_cpp san) + # get exec name (remove extension from the abs path) + get_filename_component(exec_name ${test_cpp} NAME_WE) + set(target_name ${test_prefix}${exec_name}) + add_executable(${target_name} ${test_cpp}) + + # OUTPUT_NAME sets the real name of a target when it is built and can be + # used to help create two targets of the same name even though CMake + # requires unique logical target names + set_target_properties(${target_name} PROPERTIES OUTPUT_NAME ${exec_name}) + target_link_libraries(${target_name} gtest gmock mg-utils mg-io mg-io-simulator) + + # sanitize + target_compile_options(${target_name} PRIVATE -fsanitize=${san}) + target_link_options(${target_name} PRIVATE -fsanitize=${san}) + + # register test + add_test(${target_name} ${exec_name}) + add_dependencies(memgraph__simulation ${target_name}) +endfunction(add_simulation_test) + +add_simulation_test(basic_request.cpp address) + +add_simulation_test(trial_query_storage/query_storage_test.cpp address) diff --git a/tests/simulation/basic_request.cpp b/tests/simulation/basic_request.cpp new file mode 100644 index 000000000..ac3190ad7 --- /dev/null +++ b/tests/simulation/basic_request.cpp @@ -0,0 +1,87 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#include + +#include "io/simulator/simulator.hpp" + +using memgraph::io::Address; +using memgraph::io::Io; +using memgraph::io::ResponseFuture; +using memgraph::io::ResponseResult; +using memgraph::io::simulator::Simulator; +using memgraph::io::simulator::SimulatorConfig; +using memgraph::io::simulator::SimulatorTransport; + +struct CounterRequest { + uint64_t proposal; +}; + +struct CounterResponse { + uint64_t highest_seen; +}; + +void run_server(Io io) { + uint64_t highest_seen = 0; + + while (!io.ShouldShutDown()) { + std::cout << "[SERVER] Is receiving..." << std::endl; + auto request_result = io.Receive(); + if (request_result.HasError()) { + std::cout << "[SERVER] Error, continue" << std::endl; + continue; + } + auto request_envelope = request_result.GetValue(); + auto req = std::get(request_envelope.message); + + highest_seen = std::max(highest_seen, req.proposal); + auto srv_res = CounterResponse{highest_seen}; + + io.Send(request_envelope.from_address, request_envelope.request_id, srv_res); + } +} + +int main() { + auto config = SimulatorConfig{ + .drop_percent = 0, + .perform_timeouts = true, + .scramble_messages = true, + .rng_seed = 0, + }; + auto simulator = Simulator(config); + + auto cli_addr = Address::TestAddress(1); + auto srv_addr = Address::TestAddress(2); + + Io cli_io = simulator.Register(cli_addr); + Io srv_io = simulator.Register(srv_addr); + + auto srv_thread = std::jthread(run_server, std::move(srv_io)); + simulator.IncrementServerCountAndWaitForQuiescentState(srv_addr); + + for (int i = 1; i < 3; ++i) { + // send request + CounterRequest cli_req; + cli_req.proposal = i; + auto res_f = cli_io.Request(srv_addr, cli_req); + auto res_rez = std::move(res_f).Wait(); + if (!res_rez.HasError()) { + std::cout << "[CLIENT] Got a valid response" << std::endl; + auto env = res_rez.GetValue(); + MG_ASSERT(env.message.highest_seen == i); + } else { + std::cout << "[CLIENT] Got an error" << std::endl; + } + } + + simulator.ShutDown(); + return 0; +} diff --git a/tests/simulation/trial_query_storage/messages.hpp b/tests/simulation/trial_query_storage/messages.hpp new file mode 100644 index 000000000..8db78a54c --- /dev/null +++ b/tests/simulation/trial_query_storage/messages.hpp @@ -0,0 +1,34 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#pragma once + +#include +#include +#include + +namespace memgraph::tests::simulation { + +struct Vertex { + std::string key; +}; + +struct ScanVerticesRequest { + int64_t count; + std::optional continuation; +}; + +struct VerticesResponse { + std::vector vertices; + std::optional continuation; +}; + +} // namespace memgraph::tests::simulation diff --git a/tests/simulation/trial_query_storage/query_storage_test.cpp b/tests/simulation/trial_query_storage/query_storage_test.cpp new file mode 100644 index 000000000..9cdff4ee6 --- /dev/null +++ b/tests/simulation/trial_query_storage/query_storage_test.cpp @@ -0,0 +1,85 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#include + +#include "io/address.hpp" +#include "io/simulator/simulator.hpp" +#include "io/simulator/simulator_config.hpp" +#include "io/simulator/simulator_transport.hpp" +#include "io/transport.hpp" + +#include "messages.hpp" + +namespace memgraph::tests::simulation { +using memgraph::io::Io; +using memgraph::io::simulator::SimulatorTransport; + +void run_server(Io io) { + while (!io.ShouldShutDown()) { + std::cout << "[STORAGE] Is receiving..." << std::endl; + auto request_result = io.Receive(); + if (request_result.HasError()) { + std::cout << "[STORAGE] Error, continue" << std::endl; + continue; + } + auto request_envelope = request_result.GetValue(); + auto req = std::get(request_envelope.message); + + VerticesResponse response{}; + const int64_t start_index = std::invoke([&req] { + if (req.continuation.has_value()) { + return *req.continuation; + } + return 0L; + }); + for (auto index = start_index; index < start_index + req.count; ++index) { + response.vertices.push_back({std::string("Vertex_") + std::to_string(index)}); + } + io.Send(request_envelope.from_address, request_envelope.request_id, response); + } +} + +} // namespace memgraph::tests::simulation + +int main() { + using memgraph::io::Address; + using memgraph::io::Io; + using memgraph::io::simulator::Simulator; + using memgraph::io::simulator::SimulatorConfig; + using memgraph::io::simulator::SimulatorTransport; + using memgraph::tests::simulation::run_server; + using memgraph::tests::simulation::ScanVerticesRequest; + using memgraph::tests::simulation::VerticesResponse; + auto config = SimulatorConfig{ + .drop_percent = 0, + .perform_timeouts = true, + .scramble_messages = true, + .rng_seed = 0, + }; + auto simulator = Simulator(config); + + auto cli_addr = Address::TestAddress(1); + auto srv_addr = Address::TestAddress(2); + + Io cli_io = simulator.Register(cli_addr); + Io srv_io = simulator.Register(srv_addr); + + auto srv_thread = std::jthread(run_server, std::move(srv_io)); + simulator.IncrementServerCountAndWaitForQuiescentState(srv_addr); + + auto req = ScanVerticesRequest{2, std::nullopt}; + + auto res_f = cli_io.Request(srv_addr, req); + auto res_rez = std::move(res_f).Wait(); + simulator.ShutDown(); + return 0; +} diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 55fb7b01b..9de4860ef 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -396,3 +396,7 @@ find_package(Boost REQUIRED) add_unit_test(websocket.cpp) target_link_libraries(${test_prefix}websocket mg-communication Boost::headers) + +# Test future +add_unit_test(future.cpp) +target_link_libraries(${test_prefix}future mg-io) \ No newline at end of file diff --git a/tests/unit/future.cpp b/tests/unit/future.cpp new file mode 100644 index 000000000..490e19bbc --- /dev/null +++ b/tests/unit/future.cpp @@ -0,0 +1,55 @@ +// Copyright 2022 Memgraph Ltd. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +// License, and you may not use this file except in compliance with the Business Source License. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +#include +#include + +#include "gtest/gtest.h" + +#include "io/future.hpp" + +using namespace memgraph::io; + +void Fill(Promise promise_1) { promise_1.Fill("success"); } + +void Wait(Future future_1, Promise promise_2) { + std::string result_1 = std::move(future_1).Wait(); + EXPECT_TRUE(result_1 == "success"); + promise_2.Fill("it worked"); +} + +TEST(Future, BasicLifecycle) { + std::atomic_bool waiting = false; + + std::function notifier = [&] { + waiting.store(true, std::memory_order_seq_cst); + return false; + }; + + auto [future_1, promise_1] = FuturePromisePairWithNotifier(notifier); + auto [future_2, promise_2] = FuturePromisePair(); + + std::jthread t1(Wait, std::move(future_1), std::move(promise_2)); + + // spin in a loop until the promise signals + // that it is waiting + while (!waiting.load(std::memory_order_acquire)) { + std::this_thread::yield(); + } + + std::jthread t2(Fill, std::move(promise_1)); + + t1.join(); + t2.join(); + + std::string result_2 = std::move(future_2).Wait(); + EXPECT_TRUE(result_2 == "it worked"); +}