|
| 1 | +/* Copyright (c) 2026. The SWAT Team. All rights reserved. */ |
| 2 | + |
| 3 | +/* This program is free software; you can redistribute it and/or modify it |
| 4 | + * under the terms of the license (GNU LGPL) which comes with this package. */ |
| 5 | + |
| 6 | +#include <gtest/gtest.h> |
| 7 | + |
| 8 | +#include <array> |
| 9 | + |
| 10 | +#include <fsmod/FileSystem.hpp> |
| 11 | +#include <fsmod/JBODStorage.hpp> |
| 12 | +#include <fsmod/OneDiskStorage.hpp> |
| 13 | + |
| 14 | +#include <simgrid/s4u/Actor.hpp> |
| 15 | +#include <simgrid/s4u/Engine.hpp> |
| 16 | +#include <simgrid/s4u/Host.hpp> |
| 17 | + |
| 18 | +#include "./test_util.hpp" |
| 19 | +#include "dtlmod/DTL.hpp" |
| 20 | +#include "dtlmod/DTLException.hpp" |
| 21 | + |
| 22 | +XBT_LOG_NEW_DEFAULT_CATEGORY(dtlmod_test_eos, "Logging category for this dtlmod test"); |
| 23 | + |
| 24 | +namespace sg4 = simgrid::s4u; |
| 25 | +namespace sgfs = simgrid::fsmod; |
| 26 | + |
| 27 | +// End-of-stream: once every publisher has closed its engine, a subscriber that asks for a transaction which was never |
| 28 | +// produced must be released with an EndOfStreamException (instead of blocking forever). This is what lets a consumer |
| 29 | +// terminate when its upstream stops, rather than hang. Each test has a publisher produce a fixed number of |
| 30 | +// transactions and then close; the subscriber loops until it catches the exception. |
| 31 | +// |
| 32 | +// The per-subscriber outcome (how many transactions were read, whether end-of-stream was reached) is recorded in |
| 33 | +// variables captured by reference and asserted AFTER Engine::run() returns -- never inside the subscriber actor. If |
| 34 | +// the mechanism were missing the subscriber would deadlock, run() would return with the actor still blocked, and an |
| 35 | +// in-actor assertion would simply never execute (a false pass). Asserting after run() turns that hang into a failure. |
| 36 | +class DTLEndOfStreamTest : public ::testing::Test { |
| 37 | +public: |
| 38 | + DTLEndOfStreamTest() = default; |
| 39 | + |
| 40 | + sg4::NetZone* add_cluster(sg4::NetZone* root, const std::string& suffix, const int num_hosts) |
| 41 | + { |
| 42 | + auto* cluster = root->add_netzone_star("cluster" + suffix); |
| 43 | + cluster->set_gateway(cluster->add_router("cluster" + suffix + "-router")); |
| 44 | + auto* backbone = cluster->add_link("backbone" + suffix, "100Gbps")->set_latency("100us"); |
| 45 | + for (int i = 0; i < num_hosts; i++) { |
| 46 | + std::string name = "host-" + std::to_string(i) + suffix; |
| 47 | + const auto* host = cluster->add_host(name, "1Gf"); |
| 48 | + const auto* link = cluster->add_link(name + "_link", "10Gbps")->set_latency("10us"); |
| 49 | + cluster->add_route(host, nullptr, {link, backbone}); |
| 50 | + } |
| 51 | + cluster->seal(); |
| 52 | + return cluster; |
| 53 | + } |
| 54 | + |
| 55 | + void setup_staging_platform() |
| 56 | + { |
| 57 | + auto* root = sg4::Engine::get_instance()->get_netzone_root(); |
| 58 | + auto* internet = root->add_link("internet", "500MBps")->set_latency("1ms"); |
| 59 | + auto* prod_cluster = add_cluster(root, ".prod", 4); |
| 60 | + auto* cons_cluster = add_cluster(root, ".cons", 4); |
| 61 | + root->add_route(prod_cluster, cons_cluster, {internet}); |
| 62 | + root->seal(); |
| 63 | + dtlmod::DTL::create(); |
| 64 | + } |
| 65 | + |
| 66 | + void setup_file_platform() |
| 67 | + { |
| 68 | + sg4::NetZone* cluster = sg4::Engine::get_instance()->get_netzone_root()->add_netzone_star("cluster"); |
| 69 | + auto pfs_server = cluster->add_host("pfs_server", "1Gf"); |
| 70 | + std::vector<sg4::Disk*> pfs_disks; |
| 71 | + for (int i = 0; i < 4; i++) |
| 72 | + pfs_disks.push_back(pfs_server->add_disk("pfs_disk" + std::to_string(i), "2.5GBps", "1.2GBps")); |
| 73 | + auto remote_storage = sgfs::JBODStorage::create("pfs_storage", pfs_disks); |
| 74 | + remote_storage->set_raid_level(sgfs::JBODStorage::RAID::RAID5); |
| 75 | + |
| 76 | + std::vector<std::shared_ptr<sgfs::OneDiskStorage>> local_storages; |
| 77 | + for (int i = 0; i < 4; i++) { |
| 78 | + std::string hostname = "node-" + std::to_string(i); |
| 79 | + auto* host = cluster->add_host(hostname, "1Gf"); |
| 80 | + auto* disk = host->add_disk(hostname + "_disk", "5.5GBps", "2.1GBps"); |
| 81 | + local_storages.push_back(sgfs::OneDiskStorage::create(hostname + "_local_storage", disk)); |
| 82 | + std::string linkname = "link_" + std::to_string(i); |
| 83 | + auto* link_up = cluster->add_link(linkname + "_UP", "1Gbps"); |
| 84 | + auto* link_down = cluster->add_link(linkname + "_DOWN", "1Gbps"); |
| 85 | + auto* loopback = |
| 86 | + cluster->add_link(hostname + "_loopback", "10Gbps")->set_sharing_policy(sg4::Link::SharingPolicy::FATPIPE); |
| 87 | + cluster->add_route(host, nullptr, {sg4::LinkInRoute(link_up)}, false); |
| 88 | + cluster->add_route(nullptr, host, {sg4::LinkInRoute(link_down)}, false); |
| 89 | + cluster->add_route(host, host, {loopback}); |
| 90 | + } |
| 91 | + cluster->seal(); |
| 92 | + |
| 93 | + auto my_fs = sgfs::FileSystem::create("my_fs"); |
| 94 | + sgfs::FileSystem::register_file_system(cluster, my_fs); |
| 95 | + my_fs->mount_partition("/pfs/", remote_storage, "500TB"); |
| 96 | + for (int i = 0; i < 4; i++) |
| 97 | + my_fs->mount_partition("/node-" + std::to_string(i) + "/scratch/", local_storages.at(i), "1TB"); |
| 98 | + |
| 99 | + dtlmod::DTL::create(); |
| 100 | + } |
| 101 | + |
| 102 | + // Publisher actor body shared by the tests: produce n_tx transactions then close. |
| 103 | + static void publish_n(dtlmod::Engine::Type type, dtlmod::Transport::Method method, const std::string& engine_name, |
| 104 | + int n_tx) |
| 105 | + { |
| 106 | + auto dtl = dtlmod::DTL::connect(); |
| 107 | + auto stream = dtl->add_stream("my-output"); |
| 108 | + stream->set_engine_type(type); |
| 109 | + stream->set_transport_method(method); |
| 110 | + auto var = stream->define_variable("var", {100, 100}, {0, 0}, {100, 100}, sizeof(double)); |
| 111 | + auto engine = stream->open(engine_name, dtlmod::Stream::Mode::Publish); |
| 112 | + for (int i = 0; i < n_tx; i++) { |
| 113 | + engine->begin_transaction(); |
| 114 | + engine->put(var); |
| 115 | + engine->end_transaction(); |
| 116 | + } |
| 117 | + engine->close(); |
| 118 | + dtlmod::DTL::disconnect(); |
| 119 | + } |
| 120 | + |
| 121 | + // Subscriber actor body: read until end-of-stream, recording the outcome through the referenced variables. |
| 122 | + static void consume_until_eos(const std::string& engine_name, int& reads, bool& eos) |
| 123 | + { |
| 124 | + auto dtl = dtlmod::DTL::connect(); |
| 125 | + auto stream = dtl->add_stream("my-output"); |
| 126 | + auto engine = stream->open(engine_name, dtlmod::Stream::Mode::Subscribe); |
| 127 | + auto var_sub = stream->inquire_variable("var"); |
| 128 | + var_sub->set_selection({0, 0}, {100, 100}); |
| 129 | + try { |
| 130 | + while (true) { |
| 131 | + engine->begin_transaction(); |
| 132 | + engine->get(var_sub); |
| 133 | + engine->end_transaction(); |
| 134 | + reads++; |
| 135 | + } |
| 136 | + } catch (const dtlmod::EndOfStreamException&) { |
| 137 | + eos = true; |
| 138 | + } |
| 139 | + engine->close(); |
| 140 | + dtlmod::DTL::disconnect(); |
| 141 | + } |
| 142 | +}; |
| 143 | + |
| 144 | +TEST_F(DTLEndOfStreamTest, StagingSingleSubscriber_MQ) |
| 145 | +{ |
| 146 | + DO_TEST_WITH_FORK([this]() { |
| 147 | + this->setup_staging_platform(); |
| 148 | + int reads = 0; |
| 149 | + bool eos = false; |
| 150 | + sg4::Host::by_name("host-0.prod")->add_actor("Pub", []() { |
| 151 | + publish_n(dtlmod::Engine::Type::Staging, dtlmod::Transport::Method::MQ, "my-output", 2); |
| 152 | + }); |
| 153 | + sg4::Host::by_name("host-0.cons")->add_actor("Sub", [&reads, &eos]() { |
| 154 | + consume_until_eos("my-output", reads, eos); |
| 155 | + }); |
| 156 | + ASSERT_NO_THROW(sg4::Engine::get_instance()->run()); |
| 157 | + ASSERT_TRUE(eos); |
| 158 | + ASSERT_EQ(reads, 2); |
| 159 | + }); |
| 160 | +} |
| 161 | + |
| 162 | +TEST_F(DTLEndOfStreamTest, StagingSingleSubscriber_Mailbox) |
| 163 | +{ |
| 164 | + DO_TEST_WITH_FORK([this]() { |
| 165 | + this->setup_staging_platform(); |
| 166 | + int reads = 0; |
| 167 | + bool eos = false; |
| 168 | + sg4::Host::by_name("host-0.prod")->add_actor("Pub", []() { |
| 169 | + publish_n(dtlmod::Engine::Type::Staging, dtlmod::Transport::Method::Mailbox, "my-output", 3); |
| 170 | + }); |
| 171 | + sg4::Host::by_name("host-0.cons")->add_actor("Sub", [&reads, &eos]() { |
| 172 | + consume_until_eos("my-output", reads, eos); |
| 173 | + }); |
| 174 | + ASSERT_NO_THROW(sg4::Engine::get_instance()->run()); |
| 175 | + ASSERT_TRUE(eos); |
| 176 | + ASSERT_EQ(reads, 3); |
| 177 | + }); |
| 178 | +} |
| 179 | + |
| 180 | +// Two subscribers sharing the same Staging engine must both reach end-of-stream. This is the case that exercises the |
| 181 | +// per-subscriber rollback of num_subscribers_starting_ on the EOS throw: an imbalance there would desynchronize the |
| 182 | +// publisher/subscriber rendez-vous and either deadlock or crash. |
| 183 | +TEST_F(DTLEndOfStreamTest, StagingMultipleSubscribers_MQ) |
| 184 | +{ |
| 185 | + DO_TEST_WITH_FORK([this]() { |
| 186 | + this->setup_staging_platform(); |
| 187 | + std::array<int, 2> reads = {0, 0}; |
| 188 | + std::array<bool, 2> eos = {false, false}; |
| 189 | + sg4::Host::by_name("host-0.prod")->add_actor("Pub", []() { |
| 190 | + publish_n(dtlmod::Engine::Type::Staging, dtlmod::Transport::Method::MQ, "my-output", 2); |
| 191 | + }); |
| 192 | + for (int s = 0; s < 2; s++) |
| 193 | + sg4::Host::by_name("host-" + std::to_string(s) + ".cons") |
| 194 | + ->add_actor("Sub" + std::to_string(s), |
| 195 | + [&reads, &eos, s]() { consume_until_eos("my-output", reads[s], eos[s]); }); |
| 196 | + ASSERT_NO_THROW(sg4::Engine::get_instance()->run()); |
| 197 | + for (int s = 0; s < 2; s++) { |
| 198 | + ASSERT_TRUE(eos[s]) << "subscriber " << s << " did not reach end of stream"; |
| 199 | + ASSERT_EQ(reads[s], 2) << "subscriber " << s << " read a wrong number of transactions"; |
| 200 | + } |
| 201 | + }); |
| 202 | +} |
| 203 | + |
| 204 | +TEST_F(DTLEndOfStreamTest, FileEngineSingleSubscriber) |
| 205 | +{ |
| 206 | + DO_TEST_WITH_FORK([this]() { |
| 207 | + this->setup_file_platform(); |
| 208 | + int reads = 0; |
| 209 | + bool eos = false; |
| 210 | + const std::string engine_name = "cluster:my_fs:/node-0/scratch/my-output"; |
| 211 | + sg4::Host::by_name("node-0")->add_actor("Pub", [engine_name]() { |
| 212 | + publish_n(dtlmod::Engine::Type::File, dtlmod::Transport::Method::File, engine_name, 2); |
| 213 | + }); |
| 214 | + sg4::Host::by_name("node-1")->add_actor( |
| 215 | + "Sub", [&reads, &eos, engine_name]() { consume_until_eos(engine_name, reads, eos); }); |
| 216 | + ASSERT_NO_THROW(sg4::Engine::get_instance()->run()); |
| 217 | + ASSERT_TRUE(eos); |
| 218 | + ASSERT_EQ(reads, 2); |
| 219 | + }); |
| 220 | +} |
0 commit comments