diff --git a/README.md b/README.md index 0996e24b..b315f5f7 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,13 @@ ![CMake](https://github.com/fair-acc/opencmw-cpp/workflows/CMake/badge.svg) [![Language grade: C++](https://img.shields.io/lgtm/grade/cpp/github/fair-acc/opencmw-cpp)](https://lgtm.com/projects/g/fair-acc/opencmw-cpp/context:cpp) -[![Codacy Badge](https://app.codacy.com/project/badge/Grade/b3f9b8c8730a411a90475dce6fb259d6)](https://www.codacy.com/gh/fair-acc/opencmw-cpp/dashboard?utm_source=github.com&utm_medium=referral&utm_content=fair-acc/opencmw-cpp&utm_campaign=Badge_Grade) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/b3f9b8c8730a411a90475dce6fb259d6)](https://www.codacy.com/gh/fair-acc/opencmw-cpp/dashboard?utm_source=github.com&utm_medium=referral&utm_content=fair-acc/opencmw-cpp&utm_campaign=Badge_Grade) [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/b3f9b8c8730a411a90475dce6fb259d6)](https://www.codacy.com/gh/fair-acc/opencmw-cpp/dashboard?utm_source=github.com&utm_medium=referral&utm_content=fair-acc/opencmw-cpp&utm_campaign=Badge_Coverage) [![codecov](https://codecov.io/gh/fair-acc/opencmw-cpp/branch/main/graph/badge.svg?token=CKXPO2UQIL)](https://codecov.io/gh/fair-acc/opencmw-cpp) [![Coverity Build Status](https://scan.coverity.com/projects/fair-acc-opencmw-cpp/badge.svg)](https://scan.coverity.com/projects/fair-acc-opencmw-cpp) + # Open Common Middle-Ware + ... is a modular event-driven [micro-](https://en.wikipedia.org/wiki/Microservices) and [middle-ware](https://en.wikipedia.org/wiki/Middleware#:~:text=Middleware%20is%20a%20computer%20software,described%20as%20%22software%20glue%22.) library for equipment- and beam-based monitoring as well as feedback control systems for the [FAIR](https://fair-center.eu/) Accelerator Facility ([video](https://www.youtube.com/watch?v=gCHzDR7hdoM)) or any other project that may find this useful. @@ -28,45 +30,101 @@ and buffering, settings management, Role-Based-Access-Control (RBAC), and other while still being open to expert-level modifications, extensions or improvements. ### General Schematic -OpenCMW combines [ZeroMQ](https://zeromq.org/)'s [Majordomo](https://rfc.zeromq.org/spec/7/) with LMAX's [disruptor](https://lmax-exchange.github.io/disruptor/) -([C++ port](https://github.com/Abc-Arbitrage/Disruptor-cpp)) design pattern that both provide a very efficient lock-free mechanisms -for distributing, streaming and processing of data objects. A schematic outline of the internal [architecture](https://edms.cern.ch/document/2444348/1) + +OpenCMW combines [ZeroMQ](https://zeromq.org/)'s [Majordomo](https://rfc.zeromq.org/spec/7/) with LMAX's [disruptor](https://lmax-exchange.github.io/disruptor/) +([C++ port](https://github.com/Abc-Arbitrage/Disruptor-cpp)) design pattern that both provide a very efficient lock-free mechanisms +for distributing, streaming and processing of data objects. A schematic outline of the internal [architecture](https://edms.cern.ch/document/2444348/1) ([local copy](assets/F-CS-SIS-en-B_0006_FAIR_Service_Middle_Ware_V1_0.pdf)) is shown below: ![OpenCMW architectural schematic](./assets/FAIR_microservice_schematic.svg) ### Glossary -*Majordomo Broker* or *'Broker':* is the central authority where multiple workers can register their services, allowing clients to perform get, set or subscriptions requests. +_Majordomo Broker_ or _'Broker':_ is the central authority where multiple workers can register their services, allowing clients to perform get, set or subscriptions requests. There can be multiple brokers for subset of services. -*Worker:* functional unit which provides one or more services that are registered with a broker. OpenCMW provides base implementations at different abstraction levels ([BasicMdpWorker](BasicMdpWorker.cpp) (low-level) and +_Worker:_ functional unit which provides one or more services that are registered with a broker. OpenCMW provides base implementations at different abstraction levels ([BasicMdpWorker](BasicMdpWorker.cpp) (low-level) and [MajordomoWorker](MajordomoWorker.cpp)) as well as different internal and external service workers, e.g. MajordomoRestPlugin or the broker's mmi services. Workers communicate with the broker using the OpenCMW worker [protocol](docs/MajordomoProtocol.md) internally or externally via ZeroMQ sockets via `inproc`, `tcp`, `udp` or another suitable low-level network protocol scheme that is supported by ZeroMQ. -*Endpoint:* address for a service following the standardised [URI](https://tools.ietf.org/html/rfc3986) convention of `scheme:[//authority]path[?query][#fragment]`. Services usually omit the authority part and provide only relative paths as this information is managed and added by their broker. +_Endpoint:_ address for a service following the standardised [URI](https://tools.ietf.org/html/rfc3986) convention of `scheme:[//authority]path[?query][#fragment]`. Services usually omit the authority part and provide only relative paths as this information is managed and added by their broker. Each broker acts individually as a DNS for its own services as well as can forward this information to another (for the time being) central DNS Broker. -*internal/mmi workers:* each broker by default starts some lightweight management services as specified by the Majodomo [mmi](https://rfc.zeromq.org/spec/8/) extension: +_internal/mmi workers:_ each broker by default starts some lightweight management services as specified by the Majodomo [mmi](https://rfc.zeromq.org/spec/8/) extension: + - `/mmi.service`: endpoints of all services registered at this broker - `/mmi.openapi`: openapi descriptions for the services - `/mmi.dns`: service lookup -*Context:* information that (if applicable) is matched to the URI's query parameter and required for every request and reply, specified by a domain object. +_Context:_ information that (if applicable) is matched to the URI's query parameter and required for every request and reply, specified by a domain object. They (partially) map to the filters used in the EventStore and the query parameters of the communication library. The context is used for (partial/wildcard) matching and can be used in the EventStore's filter config. -*EventStore:* based on LMAX's [disruptor](https://lmax-exchange.github.io/disruptor/) ([C++ port](https://github.com/Abc-Arbitrage/Disruptor-cpp)) pattern, +_EventStore:_ based on LMAX's [disruptor](https://lmax-exchange.github.io/disruptor/) ([C++ port](https://github.com/Abc-Arbitrage/Disruptor-cpp)) pattern, the EventStore provides datastructures and setup methods to define processing pipelines based on incoming data. -*EventHandler:* used to define specific internal processing steps based on EventStore events. The last EventHandler is usually also a Majordomo worker to export the processed information via the network. +_EventHandler:_ used to define specific internal processing steps based on EventStore events. The last EventHandler is usually also a Majordomo worker to export the processed information via the network. -*Publisher:* the [DataSourcePublisher](DataSourceExample.cpp) provides an interface to populate the EventStore +_Publisher:_ the [DataSourcePublisher](DataSourceExample.cpp) provides an interface to populate the EventStore ring-buffer with events from OpenCMW, REST services or other sources. While using disruptor ring-buffers is the preferred and most performing options, the client also supports classic patterns of registering call-back functions or returning `Future` objects. +### OpenCMW Majordomo Protocol + +The OpenCMW Majordomo [protocol](docs/MajordomoProtocol.md) is based on the [ZeroMQ Majordomo protocol](https://rfc.zeromq.org/spec/7/), both extending and slightly modifying it (see [the comparison](docs/Majordomo_protocol_comparison.pdf)). + +#### Service Names + +Service names must always start with `/`. For consistency, this also applies to the built-in MDP broker services like `/mmi.service` (instead of `mmi.service` without leading slash as in ZeroMQ Majordomo). A service name is a non-empty alphanumerical string (also allowing `.`, `_`), that must start with `/` but not end with `/`. It contain additional `/` to denote a hierarchical structure. + +Examples: + +- `/dashboards` - valid +- `/DeviceName/Acquisition` - valid +- `/mmi.service` - valid +- `DeviceName/Acquisition` - invalid (no leading slash) +- `/DeviceName/Acquisition/` - invalid (trailing slash) +- `/a-service/` - invalid (`-` not allowed) + +#### Topics + +The "topic" field (frame 5 in the [OpenCMW MDP protocol](docs/Majordomo_protocol_comparison.pdf)) specifies the topic for subscriptions and GET/SET requests. It contains a URI with the service name as path and optional query parameters to specify further requests parameters and filter criteria. + +Examples: + +- `/DeviceName/Acquisition` - service name only, no params +- `/DeviceName/Acquisition?signal=test` - service name (/DeviceName/Acquisition) and query +- `/dashboards` - service name (/dashboards) +- `/dashboards/dashboard1?what=header` - service name (/dashboards/dashboard1) and query + +Note that the whole path is considered the service name, and that there's no additional path component denoting different entities, objects etc. neither for subscriptions nor GET/SET requests. Requesting specific objects like in the `/dashboards/dashboard1` example are handled via the service-matching for requests in the broker, where a service name `/dashboard/dashboard1` would match a worker `/dashboard`, which then can extract the `dashboard1` component from the topic frame (see below). + +See also the documentation for [mdp::Topic](src/core/include/Topic.hpp). + +#### URL to Service/Topic Mapping (mds/mdp and REST) + +With both the MDS/MDP-based ZeroMQ clients as well as the REST interface, a common scheme is used to map from mdp/hds/http(s) URLs used for subscriptions and requests to the OpenCMW service name and topic fields. + +See, for example: + +``` + https://example.com:8080/serviceName?signal=test + \___/ \______________/\__________/ \_________/ + | | | | + scheme authority path query +``` + +Here the URL is mapped by the REST interface reachable via example.com:8080 (the authority part), to the service name `/serviceName` (path) and the topic `/serviceName?signal=test` (path and query). + +Other examples are: + +- `https://example.com:8080/DeviceName/Acquisition?signal=test` => service name `/DeviceName/Acquisition`, topic `/DeviceName/Acquisition?signal=test` (REST). +- `mds://example.com:8080/DeviceName/Acquisition?signal=test` => service name `/DeviceName/Acquisition`, topic `/DeviceName/Acquisition?signal=test` (subscription via mds). +- `mdp://example.com:8080/dashboards/dashboard1?what=header` => service name `/dashboards/dashboard1`, topic `/dashboards/dashboard1?what=header` (Request via mdp). + ### Compile-Time-Reflection + The serialisers are based on a [compile-time-reflection](docs/CompileTimeSerialiser.md) that efficiently transform domain-objects to and from the given wire-format (binary, JSON, ...). -Compile-time reflection will become part of [C++23](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0592r4.html) as described by [David Sankel et al. , “C++ Extensions for Reflection”, ISO/IEC CD TS 23619, N4856](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4856.pdf). -Until then, the compile-time reflection is emulated by the [refl-cpp](https://github.com/veselink1/refl-cpp) (header-only) library through a `constexpr` visitor-pattern and -- for the use in opencmw -- +Compile-time reflection will become part of [C++23](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0592r4.html) as described by [David Sankel et al. , “C++ Extensions for Reflection”, ISO/IEC CD TS 23619, N4856](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4856.pdf). +Until then, the compile-time reflection is emulated by the [refl-cpp](https://github.com/veselink1/refl-cpp) (header-only) library through a `constexpr` visitor-pattern and — for the use in opencmw — simplified/feature-reduced `ENABLE_REFLECTION_FOR(...)` macro: ```cpp @@ -76,11 +134,12 @@ struct className { float field2; std::string field3; }; -ENABLE_REFLECTION_FOR(className, field1, field2, field3) +ENABLE_REFLECTION_FOR(className, field1, field2, field3) ``` + Beside common primitive and STL container types, it is possible to extend and to provide custom serialisation schemes for any other arbitrary type or struct/class constructs. Further, the interface provides also an optional light-weight `constexpr` annotation template wrapper `Annotated ...` that can be used to provide some extra meta-information (e.g. unit, descriptions, etc.) -that in turn can be used to (re-)generate and document the class definition (e.g. for other programming languages or projects that do not have the primary domain-object definition at hand) +that in turn can be used to (re-)generate and document the class definition (e.g. for other programming languages or projects that do not have the primary domain-object definition at hand) or to generate a generic [OpenAPI](https://swagger.io/specification/) definition. More details can be found [here](docs/CompileTimeSerialiser.md). ### Building from source @@ -159,10 +218,12 @@ or RESTful (HTTP)-based high-level protocols, or through a simple RESTful web-in [comment]: <> (UI designs it is planned to allow embedding of WebAssembly-based ([WASM](https://en.wikipedia.org/wiki/WebAssembly)) applications.) ### Performance + The end-to-end transmission achieving roughly 10k messages per second for synchronous communications and about 140k messages per second for asynchronous and or publish-subscribe style data acquisition (TCP link via locahost). The domain-object abstraction and serialiser taking typically only 5% of the overall performance w.r.t. bare-metal transmissions (i.e. raw byte buffer transmission performance via ZeroMQ): + ``` CPU:AMD Ryzen 9 5900X 12-Core Processor description; n_exec; n_workers #0; #1; #2; #3; #4; avg @@ -175,22 +236,25 @@ sub, async, eventStore, domain-object ; 10000; 1; 70222.57; 115074.45; 161601.2 sub, async, eventStore, raw-byte[] ; 10000; 1; 121308.73; 123829.95; 124283.37; 166348.23; 128094.40; 135638.99 sub, async, callback, domain-object ; 10000; 1; 111274.04; 118184.64; 123098.70; 116418.52; 107858.25; 116390.03 ``` + Your mileage may vary depending on the specific domain-object, processing logic, and choice of hardware (CPU/RAM), but you can check and compare the results for your platform using the [RoundTripAndNotifyEvaluation](RoundTripAndNotifyEvaluation.cpp) and/or [MdpImplementationBenchmark](MdpImplementationBenchmark.cpp) benchmarks. ### Documentation + .... more to follow. ### Don't like Cpp? + For prototyping applications or services that do not interact with hardware-based systems, a Java-based [OpenCMW](https://github.com/fair-acc/opencmw-java) twin-project is being developed which follows the same functional style but takes advantage of more concise implementation and C++-based type safety. ### Acknowledgements + The implementation heavily relies upon and re-uses time-tried and well-established concepts from [ZeroMQ](https://zeromq.org/) (notably the [Majordomo](https://rfc.zeromq.org/spec/7/) communication pattern, see [Z-Guide](https://zguide.zeromq.org/docs/chapter4/#Service-Oriented-Reliable-Queuing-Majordomo-Pattern) for details), LMAX's lock-free ring-buffer [disruptor](https://lmax-exchange.github.io/disruptor/) ([C++ port](https://github.com/Abc-Arbitrage/Disruptor-cpp)), [GNU-Radio](https://www.gnuradio.org/) real-time signal processing framework, as well as previous implementations and experiences gained at [GSI](https://www.gsi.de/), [FAIR](https://fair-center.eu/) and [CERN](https://home.cern/). - diff --git a/cmake/DependenciesNative.cmake b/cmake/DependenciesNative.cmake index 0bf1a02f..90392b3e 100644 --- a/cmake/DependenciesNative.cmake +++ b/cmake/DependenciesNative.cmake @@ -18,7 +18,7 @@ option(WITH_PERF_TOOL "Build with perf-tools" OFF) FetchContent_Declare( cpp-httplib GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git - GIT_TAG v0.11.2 # latest v0.11.2 + GIT_TAG v0.14.2 # latest v0.14.2 ) # zlib: optional httplib dependency diff --git a/concepts/client/RestSubscription_example.cpp b/concepts/client/RestSubscription_example.cpp index 7f5141d4..99cb82dc 100644 --- a/concepts/client/RestSubscription_example.cpp +++ b/concepts/client/RestSubscription_example.cpp @@ -85,7 +85,7 @@ int main() { data.put(0); opencmw::client::Command command; command.command = opencmw::mdp::Command::Subscribe; - command.endpoint = opencmw::URI("http://localhost:8080/event"); + command.topic = opencmw::URI("http://localhost:8080/event"); command.data = std::move(data); command.callback = [&received](const opencmw::mdp::Message &rep) { fmt::print("SSE client received reply = '{}' - body size: '{}'\n", rep.data.asString(), rep.data.size()); diff --git a/concepts/client/helpers.hpp b/concepts/client/helpers.hpp index 013abd9a..ff97c6cc 100644 --- a/concepts/client/helpers.hpp +++ b/concepts/client/helpers.hpp @@ -47,7 +47,7 @@ struct rest_test_step { requires(command != opencmw::mdp::Command::Set) : _client(client), _resultChecker(std::move(resultChecker)), _expectedRepliesCount(expectedRepliesCount) { _command.command = command; - _command.endpoint = endpoint; + _command.topic = endpoint; _command.callback = [this](const opencmw::mdp::Message &reply) { fmt::print("Reply R\"({})\"\n", reply.data.asString()); if (_resultChecker && !_resultChecker(reply)) { @@ -66,7 +66,7 @@ struct rest_test_step { : _client(client) { _command.command = command; _command.data = opencmw::IoBuffer(new_data.data(), new_data.size()); - _command.endpoint = endpoint; + _command.topic = endpoint; _command.callback = [this](const opencmw::mdp::Message & /*reply*/) { next_step(); }; diff --git a/concepts/majordomo/FilterSubscription_example.cpp b/concepts/majordomo/FilterSubscription_example.cpp index c5bb0a26..c59a9885 100644 --- a/concepts/majordomo/FilterSubscription_example.cpp +++ b/concepts/majordomo/FilterSubscription_example.cpp @@ -119,7 +119,7 @@ class AcquisitionWorker : public Worker("mds://127.0.0.1:12345"))) { std::cerr << "Could not bind to broker address" << std::endl; return 1; @@ -154,11 +154,11 @@ int main() { std::atomic receivedA{ 0 }; std::atomic receivedAB{ 0 }; client.subscribe(URI("mds://127.0.0.1:12345/DeviceName/Acquisition?signalFilter=A"), [&receivedA](const opencmw::mdp::Message &update) { - fmt::print("Client('A') received message from service '{}' for endpoint '{}'\n", update.serviceName, update.endpoint.str()); + fmt::print("Client('A') received message from service '{}' for endpoint '{}'\n", update.serviceName, update.topic.str()); receivedA++; }); - client.subscribe(URI("mds://127.0.0.1:12345/DeviceName/Acquisition?signalFilter=A,B"), [&receivedAB](const opencmw::mdp::Message &update) { - fmt::print("Client('A,B') received message from service '{}' for endpoint '{}'\n", update.serviceName, update.endpoint.str()); + client.subscribe(URI("mds://127.0.0.1:12345/DeviceName/Acquisition?signalFilter=A%2CB"), [&receivedAB](const opencmw::mdp::Message &update) { + fmt::print("Client('A,B') received message from service '{}' for endpoint '{}'\n", update.serviceName, update.topic.str()); receivedAB++; }); diff --git a/concepts/majordomo/MajordomoRest_example.cpp b/concepts/majordomo/MajordomoRest_example.cpp index b0b87a35..818ae601 100644 --- a/concepts/majordomo/MajordomoRest_example.cpp +++ b/concepts/majordomo/MajordomoRest_example.cpp @@ -35,7 +35,7 @@ int main(int argc, char **argv) { // note: inconsistency: brokerName as ctor argument, worker's serviceName as NTTP // note: default roles different from java (has: ADMIN, READ_WRITE, READ_ONLY, ANYONE, NULL) - majordomo::Broker primaryBroker("PrimaryBroker", testSettings()); + majordomo::Broker primaryBroker("/PrimaryBroker", testSettings()); opencmw::query::registerTypes(SimpleContext(), primaryBroker); auto fs = cmrc::assets::get_filesystem(); @@ -72,19 +72,19 @@ int main(int argc, char **argv) { }); // second broker to test DNS functionalities - majordomo::Broker secondaryBroker("SecondaryTestBroker", { .dnsAddress = brokerRouterAddress->str() }); + majordomo::Broker secondaryBroker("/SecondaryTestBroker", { .dnsAddress = brokerRouterAddress->str() }); std::jthread secondaryBrokerThread([&secondaryBroker] { secondaryBroker.run(); - }); + }); // - majordomo::Worker<"helloWorld", SimpleContext, SimpleRequest, SimpleReply, majordomo::description<"A friendly service saying hello">> helloWorldWorker(primaryBroker, HelloWorldHandler()); - majordomo::Worker<"addressbook", SimpleContext, AddressRequest, AddressEntry> addressbookWorker(primaryBroker, TestAddressHandler()); - majordomo::Worker<"addressbookBackup", SimpleContext, AddressRequest, AddressEntry> addressbookBackupWorker(primaryBroker, TestAddressHandler()); - majordomo::BasicWorker<"beverages"> beveragesWorker(primaryBroker, TestIntHandler(10)); + majordomo::Worker<"/helloWorld", SimpleContext, SimpleRequest, SimpleReply, majordomo::description<"A friendly service saying hello">> helloWorldWorker(primaryBroker, HelloWorldHandler()); + majordomo::Worker<"/addressbook", SimpleContext, AddressRequest, AddressEntry> addressbookWorker(primaryBroker, TestAddressHandler()); + majordomo::Worker<"/addressbookBackup", SimpleContext, AddressRequest, AddressEntry> addressbookBackupWorker(primaryBroker, TestAddressHandler()); + majordomo::BasicWorker<"/beverages"> beveragesWorker(primaryBroker, TestIntHandler(10)); // - ImageServiceWorker<"testImage", majordomo::description<"Returns an image">> imageWorker(primaryBroker, std::chrono::seconds(10)); + ImageServiceWorker<"/testImage", majordomo::description<"Returns an image">> imageWorker(primaryBroker, std::chrono::seconds(10)); // RunInThread runHelloWorld(helloWorldWorker); @@ -92,7 +92,7 @@ int main(int argc, char **argv) { RunInThread runAddressbookBackup(addressbookBackupWorker); RunInThread runBeverages(beveragesWorker); RunInThread runImage(imageWorker); - waitUntilServiceAvailable(primaryBroker.context, "addressbook"); + waitUntilWorkerServiceAvailable(primaryBroker.context, addressbookWorker); // Fake message publisher - sends messages on notifier.service TestNode publisher(primaryBroker.context); @@ -102,9 +102,9 @@ int main(int argc, char **argv) { { std::cerr << "Sending new number (step " << i << ")\n"; mdp::Message notifyMessage; - notifyMessage.endpoint = mdp::Message::URI("/wine"); - const auto data = std::to_string(i); - notifyMessage.data = opencmw::IoBuffer(data.data(), data.size()); + notifyMessage.topic = mdp::Message::URI("/wine"); + const auto data = std::to_string(i); + notifyMessage.data = opencmw::IoBuffer(data.data(), data.size()); beveragesWorker.notify(std::move(notifyMessage)); } @@ -125,15 +125,15 @@ int main(int argc, char **argv) { .contentType = opencmw::MIME::JSON }; - addressbookWorker.notify("/addressbook", context, entry); + addressbookWorker.notify(context, entry); context.testFilter = "main"; entry.city = "London"; - addressbookWorker.notify("/addressbook", context, entry); + addressbookWorker.notify(context, entry); context.testFilter = "alternate"; entry.city = "Brighton"; - addressbookWorker.notify("/addressbook", context, entry); + addressbookWorker.notify(context, entry); } std::this_thread::sleep_for(3s); diff --git a/concepts/majordomo/helpers.hpp b/concepts/majordomo/helpers.hpp index 2cff6764..a6c88bf8 100644 --- a/concepts/majordomo/helpers.hpp +++ b/concepts/majordomo/helpers.hpp @@ -182,14 +182,16 @@ class ImageServiceWorker : public majordomo::Workersecond.value_or("") : ""; + path = ::detail::stripPrefix(path, "/"); out.resourceName = ::detail::stripPrefix(::detail::stripPrefix(path, PROPERTY_NAME), "/"); out.image.base64 = base64pp::encode(imageData[selectedImage]); out.image.contentType = "image/png"; // MIME::PNG; @@ -281,26 +283,22 @@ class TestNode { return zmq::invoke(zmq_bind, _socket, mdp::toZeroMQEndpoint(address).data()).isValid(); } - bool connect(const opencmw::URI &address, const mdp::SubscriptionTopic &subscription = {}) { + bool connect(const opencmw::URI &address, const std::string_view subscriptionTopic = {}) { auto result = zmq::invoke(zmq_connect, _socket, mdp::toZeroMQEndpoint(address).data()); if (!result) return false; - if (!subscription.empty()) { - return subscribe(subscription); + if (!subscriptionTopic.empty()) { + return subscribe(subscriptionTopic); } return true; } - bool subscribe(const mdp::SubscriptionTopic &subscription) { - const auto topic = subscription.toZmqTopic(); - assert(!topic.empty()); + bool subscribe(std::string_view topic) { return zmq::invoke(zmq_setsockopt, _socket, ZMQ_SUBSCRIBE, topic.data(), topic.size()).isValid(); } - bool unsubscribe(const mdp::SubscriptionTopic &subscription) { - const auto topic = subscription.toZmqTopic(); - assert(!topic.empty()); + bool unsubscribe(const std::string_view topic) { return zmq::invoke(zmq_setsockopt, _socket, ZMQ_UNSUBSCRIBE, topic.data(), topic.size()).isValid(); } @@ -355,7 +353,7 @@ inline bool waitUntilServiceAvailable(const zmq::Context &context, std::string_v mdp::Message request; request.protocolName = mdp::clientProtocol; request.command = mdp::Command::Get; - request.serviceName = "mmi.service"; + request.serviceName = "/mmi.service"; request.data = opencmw::IoBuffer(serviceName.data(), serviceName.size()); client.send(std::move(request)); @@ -372,6 +370,11 @@ inline bool waitUntilServiceAvailable(const zmq::Context &context, std::string_v return false; } +template +inline bool waitUntilWorkerServiceAvailable(const zmq::Context &context, const Worker &, const opencmw::URI &brokerAddress = opencmw::majordomo::INTERNAL_ADDRESS_BROKER) { + return waitUntilServiceAvailable(context, Worker::name, brokerAddress); +} + class TestIntHandler { int _x = 10; diff --git a/src/client/include/Client.hpp b/src/client/include/Client.hpp index 0b3981ea..fe9c6127 100644 --- a/src/client/include/Client.hpp +++ b/src/client/include/Client.hpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include #include @@ -96,26 +96,26 @@ class Client : public MDClientBase { void get(const URI &uri, std::string_view req_id) override { const auto &con = findConnection(uri); - auto message = createRequestTemplate(mdp::Command::Get, uri.relativeRefNoFragment().value(), req_id); + auto message = createRequestTemplate(mdp::Command::Get, uri, req_id); zmq::send(std::move(message), con._socket).assertSuccess(); } void set(const URI &uri, std::string_view req_id, const std::span &request) override { const auto &con = findConnection(uri); - auto message = createRequestTemplate(mdp::Command::Set, uri.relativeRefNoFragment().value(), req_id); + auto message = createRequestTemplate(mdp::Command::Set, uri, req_id); message.data = IoBuffer(reinterpret_cast(request.data()), request.size()); zmq::send(std::move(message), con._socket).assertSuccess(); } void subscribe(const URI &uri, std::string_view req_id) override { const auto &con = findConnection(uri); - auto message = createRequestTemplate(mdp::Command::Subscribe, uri.relativeRefNoFragment().value(), req_id); + auto message = createRequestTemplate(mdp::Command::Subscribe, uri, req_id); zmq::send(std::move(message), con._socket).assertSuccess(); } void unsubscribe(const URI &uri, std::string_view req_id) override { const auto &con = findConnection(uri); - auto message = createRequestTemplate(mdp::Command::Unsubscribe, uri.relativeRefNoFragment().value(), req_id); + auto message = createRequestTemplate(mdp::Command::Unsubscribe, uri, req_id); zmq::send(std::move(message), con._socket).assertSuccess(); } @@ -134,7 +134,7 @@ class Client : public MDClientBase { static bool handleMessage(mdp::Message &message) { if (message.command == mdp::Command::Notify || message.command == mdp::Command::Final) { - message.arrivalTime = std::chrono::system_clock::now(); + message.arrivalTime = std::chrono::system_clock::now(); const auto requestId_sv = message.clientRequestID.asString(); if (auto result = std::from_chars(requestId_sv.data(), requestId_sv.data() + requestId_sv.size(), message.id); result.ec == std::errc::invalid_argument || result.ec == std::errc::result_out_of_range) { message.id = 0; @@ -184,11 +184,13 @@ class Client : public MDClientBase { } private: - static mdp::Message createRequestTemplate(mdp::Command command, std::string_view serviceName, std::string_view req_id) { + static mdp::Message createRequestTemplate(mdp::Command command, const URI &uri, std::string_view req_id) { + const auto subscription = mdp::Topic::fromString(uri.relativeRefNoFragment().value_or("")); mdp::Message req; req.protocolName = mdp::clientProtocol; req.command = command; - req.serviceName = std::string(serviceName); + req.serviceName = subscription.service(); + req.topic = subscription.toMdpTopic(); req.clientRequestID = IoBuffer(req_id.data(), req_id.size()); return req; @@ -246,17 +248,17 @@ class SubscriptionClient : public MDClientBase { } void subscribe(const URI &uri, std::string_view /*reqId*/) override { - auto &con = findConnection(uri); - const auto serviceName = mdp::SubscriptionTopic::fromURI(uri).toZmqTopic(); - assert(!serviceName.empty()); - opencmw::zmq::invoke(zmq_setsockopt, con._socket, ZMQ_SUBSCRIBE, serviceName.data(), serviceName.size()).assertSuccess(); + auto &con = findConnection(uri); + const auto topic = mdp::Topic::fromString(uri.relativeRefNoFragment().value_or("")).toZmqTopic(); + assert(!topic.empty()); + opencmw::zmq::invoke(zmq_setsockopt, con._socket, ZMQ_SUBSCRIBE, topic.data(), topic.size()).assertSuccess(); } void unsubscribe(const URI &uri, std::string_view /*reqId*/) override { - auto &con = findConnection(uri); - const auto serviceName = mdp::SubscriptionTopic::fromURI(uri).toZmqTopic(); - assert(!serviceName.empty()); - opencmw::zmq::invoke(zmq_setsockopt, con._socket, ZMQ_UNSUBSCRIBE, serviceName.data(), serviceName.size()).assertSuccess(); + auto &con = findConnection(uri); + const auto topic = mdp::Topic::fromString(uri.relativeRefNoFragment().value_or("")).toZmqTopic(); + assert(!topic.empty()); + opencmw::zmq::invoke(zmq_setsockopt, con._socket, ZMQ_UNSUBSCRIBE, topic.data(), topic.size()).assertSuccess(); } bool disconnect(detail::Connection &con) { @@ -280,7 +282,7 @@ class SubscriptionClient : public MDClientBase { output.error = std::move(message.error); // output.serviceName = URI(std::string{ message.serviceName() }); output.serviceName = std::move(message.sourceId); // temporary hack until serviceName -> 'requestedTopic' and 'topic' -> 'replyTopic' - output.endpoint = std::move(message.endpoint); + output.topic = std::move(message.topic); output.clientRequestID = std::move(message.clientRequestID); output.rbac = std::move(message.rbac); output.id = 0; // review if this is still needed @@ -389,15 +391,15 @@ class MDClientCtx : public ClientBase { if (cmd.callback) { if (cmd.command == mdp::Command::Get || cmd.command == mdp::Command::Set) { req_id = _request_id++; - _requests.insert({ req_id, Request{ .uri = cmd.endpoint, .callback = std::move(cmd.callback), .timestamp_received = cmd.arrivalTime } }); + _requests.insert({ req_id, Request{ .uri = cmd.topic, .callback = std::move(cmd.callback), .timestamp_received = cmd.arrivalTime } }); } else if (cmd.command == mdp::Command::Subscribe) { req_id = _request_id++; - _subscriptions.insert({ mdp::SubscriptionTopic::fromURI(cmd.endpoint).toZmqTopic(), Subscription{ .uri = cmd.endpoint, .callback = std::move(cmd.callback), .timestamp_received = cmd.arrivalTime } }); + _subscriptions.insert({ mdp::Topic::fromMdpTopic(cmd.topic).toZmqTopic(), Subscription{ .uri = cmd.topic, .callback = std::move(cmd.callback), .timestamp_received = cmd.arrivalTime } }); } else if (cmd.command == mdp::Command::Unsubscribe) { _requests.erase(0); // todo: lookup correct subscription } } - sendCmd(cmd.endpoint, cmd.command, req_id, cmd.data); + sendCmd(cmd.topic, cmd.command, req_id, cmd.data); } private: diff --git a/src/client/include/ClientContext.hpp b/src/client/include/ClientContext.hpp index 60cd1346..2ad9f5a1 100644 --- a/src/client/include/ClientContext.hpp +++ b/src/client/include/ClientContext.hpp @@ -86,7 +86,7 @@ class ClientContext { if (cmd.command == mdp::Command::Invalid) { return false; } - auto &c = getClientCtx(cmd.endpoint); + auto &c = getClientCtx(cmd.topic); #ifdef EMSCRIPTEN // this is necessary for fetches to actually be called, as the new thread will start/init/end and then go into js runtime to fetch std::thread ql{ [&c, cmd]() { @@ -113,7 +113,7 @@ class ClientContext { bool published = _commandRingBuffer->tryPublishEvent([&endpoint, &cmd, cb = std::move(callback), d = std::move(data)](Command &&ev, long /*seq*/) mutable { ev.command = cmd; ev.callback = std::move(cb); - ev.endpoint = FWD(endpoint); + ev.topic = FWD(endpoint); ev.data = std::move(d); }); if (!published) { diff --git a/src/client/include/MockServer.hpp b/src/client/include/MockServer.hpp index 81ab8e08..ba4d4767 100644 --- a/src/client/include/MockServer.hpp +++ b/src/client/include/MockServer.hpp @@ -91,15 +91,16 @@ class MockServer { return true; } - void notify(const mdp::SubscriptionTopic &topic, std::string_view value) { - static const auto brokerName = ""; - static const auto serviceName = "a.service"; + void notify(std::string_view endpoint, std::string_view value) { + static const auto brokerName = ""; + static const auto serviceName = "a.service"; + const auto subscription = mdp::Topic::fromMdpTopic(URI<>(std::string(endpoint))); mdp::BasicMessage notify; notify.protocolName = mdp::clientProtocol; notify.command = mdp::Command::Final; notify.serviceName = serviceName; - notify.endpoint = topic.toEndpoint(); - notify.sourceId = topic.toZmqTopic(); + notify.topic = subscription.toMdpTopic(); + notify.sourceId = subscription.toZmqTopic(); notify.clientRequestID = IoBuffer(brokerName); notify.data = IoBuffer(value.data(), value.size()); zmq::send(std::move(notify), _pubSocket.value()).assertSuccess(); @@ -111,7 +112,7 @@ class MockServer { reply.command = mdp::Command::Final; reply.serviceName = request.serviceName; reply.clientRequestID = request.clientRequestID; - reply.endpoint = request.endpoint; + reply.topic = request.topic; reply.rbac = request.rbac; return reply; } diff --git a/src/client/include/RestClientEmscripten.hpp b/src/client/include/RestClientEmscripten.hpp index d2655a84..a50a26e0 100644 --- a/src/client/include/RestClientEmscripten.hpp +++ b/src/client/include/RestClientEmscripten.hpp @@ -74,17 +74,17 @@ struct FetchPayload { command.callback(mdp::Message{ .id = 0, .arrivalTime = std::chrono::system_clock::now(), - .protocolName = command.endpoint.scheme().value(), + .protocolName = command.topic.scheme().value(), .command = mdp::Command::Final, .clientRequestID = command.clientRequestID, - .endpoint = command.endpoint, + .topic = command.topic, .data = msgOK ? IoBuffer(body.data(), body.size()) : IoBuffer(), .error = errorMsg, .rbac = IoBuffer() }); } catch (const std::exception &e) { - std::cerr << fmt::format("caught exception '{}' in FetchPayload::returnMdpMessage(cmd={}, {}: {})", e.what(), command.endpoint, status, body) << std::endl; + std::cerr << fmt::format("caught exception '{}' in FetchPayload::returnMdpMessage(cmd={}, {}: {})", e.what(), command.topic, status, body) << std::endl; } catch (...) { - std::cerr << fmt::format("caught unknown exception in FetchPayload::returnMdpMessage(cmd={}, {}: {})", command.endpoint, status, body) << std::endl; + std::cerr << fmt::format("caught unknown exception in FetchPayload::returnMdpMessage(cmd={}, {}: {})", command.topic, status, body) << std::endl; } } @@ -114,9 +114,9 @@ struct SubscriptionPayload : FetchPayload { SubscriptionPayload &operator=(SubscriptionPayload &&other) noexcept = default; void requestNext() { - auto uri = command.endpoint; + auto uri = command.topic; fmt::print("URL 1 >>> {}\n", uri.relativeRef()); - auto preferredHeader = detail::getPreferredContentTypeHeader(command.endpoint, _mimeType); + auto preferredHeader = detail::getPreferredContentTypeHeader(command.topic, _mimeType); std::array preferredHeaderEmscripten; std::transform(preferredHeader.cbegin(), preferredHeader.cend(), preferredHeaderEmscripten.begin(), [](const auto &str) { return str.c_str(); }); @@ -223,8 +223,8 @@ class RestClient : public ClientBase { private: void executeCommand(Command &&cmd) const { - auto uri = opencmw::URI<>::factory(cmd.endpoint).build(); - auto preferredHeader = detail::getPreferredContentTypeHeader(cmd.endpoint, _mimeType); + auto uri = opencmw::URI<>::factory(cmd.topic).build(); + auto preferredHeader = detail::getPreferredContentTypeHeader(cmd.topic, _mimeType); std::array preferredHeaderEmscripten; std::transform(preferredHeader.cbegin(), preferredHeader.cend(), preferredHeaderEmscripten.begin(), [](const auto &str) { return str.c_str(); }); @@ -274,8 +274,8 @@ class RestClient : public ClientBase { } void startSubscription(Command &&cmd) { - auto uri = opencmw::URI<>::factory(cmd.endpoint).queryParam("LongPollingIdx=Next").build(); - cmd.endpoint = uri; + auto uri = opencmw::URI<>::factory(cmd.topic).queryParam("LongPollingIdx=Next").build(); + cmd.topic = uri; auto payload = std::make_unique(std::move(cmd), _mimeType); auto rawPayload = payload.get(); @@ -289,10 +289,10 @@ class RestClient : public ClientBase { // void get(...) // void set(...) // instead of going through a fake generic request(...)? - auto uri = opencmw::URI<>::factory(cmd.endpoint).queryParam("LongPollingIdx=Next").build(); + auto uri = opencmw::URI<>::factory(cmd.topic).queryParam("LongPollingIdx=Next").build(); auto payloadIt = std::find_if(detail::subscriptionPayloads.begin(), detail::subscriptionPayloads.end(), [&](const auto &ptr) { - return ptr->command.endpoint == uri; + return ptr->command.topic == uri; }); if (payloadIt == detail::subscriptionPayloads.end()) { return; diff --git a/src/client/include/RestClientNative.hpp b/src/client/include/RestClientNative.hpp index 0e4e1fba..12524223 100644 --- a/src/client/include/RestClientNative.hpp +++ b/src/client/include/RestClientNative.hpp @@ -229,7 +229,7 @@ class RestClient : public ClientBase { return {}; } - const auto httpError = httplib::detail::status_message(result->status); + const auto httpError = httplib::status_message(result->status); return fmt::format("{} - {}:{}", result->status, httpError, errorMsgExt.empty() ? result->body : errorMsgExt); }(); @@ -237,26 +237,26 @@ class RestClient : public ClientBase { cmd.callback(mdp::Message{ .id = 0, .arrivalTime = std::chrono::system_clock::now(), - .protocolName = cmd.endpoint.scheme().value(), + .protocolName = cmd.topic.scheme().value(), .command = mdp::Command::Final, .clientRequestID = cmd.clientRequestID, - .endpoint = cmd.endpoint, + .topic = cmd.topic, .data = errorMsg ? IoBuffer() : IoBuffer(result->body.data(), result->body.size()), .error = errorMsg.value_or(""), .rbac = IoBuffer() }); } catch (const std::exception &e) { - std::cerr << fmt::format("caught exception '{}' in RestClient::returnMdpMessage(cmd={}, {}: {})", e.what(), cmd.endpoint, result->status, result.value().body) << std::endl; + std::cerr << fmt::format("caught exception '{}' in RestClient::returnMdpMessage(cmd={}, {}: {})", e.what(), cmd.topic, result->status, result.value().body) << std::endl; } catch (...) { - std::cerr << fmt::format("caught unknown exception in RestClient::returnMdpMessage(cmd={}, {}: {})", cmd.endpoint, result->status, result.value().body) << std::endl; + std::cerr << fmt::format("caught unknown exception in RestClient::returnMdpMessage(cmd={}, {}: {})", cmd.topic, result->status, result.value().body) << std::endl; } } void executeCommand(Command &&cmd) const { using namespace std::string_literals; - std::cout << "RestClient::request(" << (cmd.endpoint.str()) << ")" << std::endl; - auto preferredHeader = getPreferredContentTypeHeader(cmd.endpoint); + std::cout << "RestClient::request(" << (cmd.topic.str()) << ")" << std::endl; + auto preferredHeader = getPreferredContentTypeHeader(cmd.topic); - auto endpointBuilder = URI<>::factory(cmd.endpoint); + auto endpointBuilder = URI<>::factory(cmd.topic); if (cmd.command == mdp::Command::Set) { preferredHeader.insert(std::make_pair("X-OPENCMW-METHOD"s, "PUT"s)); @@ -266,6 +266,7 @@ class RestClient : public ClientBase { auto endpoint = endpointBuilder.build(); auto callback = [&cmd, &preferredHeader, &endpoint](ClientType &client) { + client.set_follow_location(true); client.set_read_timeout(cmd.timeout); // default keep-alive value if (const httplib::Result &result = client.Get(endpoint.relativeRef()->data(), preferredHeader)) { returnMdpMessage(cmd, result); @@ -276,29 +277,29 @@ class RestClient : public ClientBase { errorStr << fmt::format(" - SSL error: '{}'", X509_verify_cert_error_string(sslResult)); } #endif - const std::string errorMsg = fmt::format("GET request failed for: '{}' - {} - CHECK_CERTIFICATES: {}", cmd.endpoint.str(), errorStr.str(), CHECK_CERTIFICATES); + const std::string errorMsg = fmt::format("GET request failed for: '{}' - {} - CHECK_CERTIFICATES: {}", cmd.topic.str(), errorStr.str(), CHECK_CERTIFICATES); returnMdpMessage(cmd, result, errorMsg); } }; - if (cmd.endpoint.scheme() && equal_with_case_ignore(cmd.endpoint.scheme().value(), "https")) { + if (cmd.topic.scheme() && equal_with_case_ignore(cmd.topic.scheme().value(), "https")) { #ifdef CPPHTTPLIB_OPENSSL_SUPPORT - httplib::SSLClient client(cmd.endpoint.hostName().value(), cmd.endpoint.port() ? cmd.endpoint.port().value() : 443); + httplib::SSLClient client(cmd.topic.hostName().value(), cmd.topic.port() ? cmd.topic.port().value() : 443); client.set_ca_cert_store(_client_cert_store); client.enable_server_certificate_verification(CHECK_CERTIFICATES); callback(client); #else throw std::invalid_argument("https is not supported"); #endif - } else if (cmd.endpoint.scheme() && equal_with_case_ignore(cmd.endpoint.scheme().value(), "http")) { - httplib::Client client(cmd.endpoint.hostName().value(), cmd.endpoint.port() ? cmd.endpoint.port().value() : 80); + } else if (cmd.topic.scheme() && equal_with_case_ignore(cmd.topic.scheme().value(), "http")) { + httplib::Client client(cmd.topic.hostName().value(), cmd.topic.port() ? cmd.topic.port().value() : 80); callback(client); return; } else { - if (cmd.endpoint.scheme()) { - throw std::invalid_argument(fmt::format("unsupported protocol '{}' for endpoint '{}'", cmd.endpoint.scheme(), cmd.endpoint.str())); + if (cmd.topic.scheme()) { + throw std::invalid_argument(fmt::format("unsupported protocol '{}' for endpoint '{}'", cmd.topic.scheme(), cmd.topic.str())); } else { - throw std::invalid_argument(fmt::format("no protocol provided for endpoint '{}'", cmd.endpoint.str())); + throw std::invalid_argument(fmt::format("no protocol provided for endpoint '{}'", cmd.topic.str())); } } } @@ -309,20 +310,21 @@ class RestClient : public ClientBase { void startSubscription(Command &&cmd) { std::scoped_lock lock(_subscriptionLock); - if (equal_with_case_ignore(*cmd.endpoint.scheme(), "http") + if (equal_with_case_ignore(*cmd.topic.scheme(), "http") #ifdef CPPHTTPLIB_OPENSSL_SUPPORT - || equal_with_case_ignore(*cmd.endpoint.scheme(), "https") + || equal_with_case_ignore(*cmd.topic.scheme(), "https") #endif ) { - auto it = _subscription1.find(cmd.endpoint); + auto it = _subscription1.find(cmd.topic); if (it == _subscription1.end()) { - auto &client = _subscription1.try_emplace(cmd.endpoint, httplib::Client(cmd.endpoint.hostName().value(), cmd.endpoint.port().value())).first->second; + auto &client = _subscription1.try_emplace(cmd.topic, httplib::Client(cmd.topic.hostName().value(), cmd.topic.port().value())).first->second; + client.set_follow_location(true); - auto longPollingEndpoint = [&] { - if (!cmd.endpoint.queryParamMap().contains(LONG_POLLING_IDX_TAG)) { - return URI<>::factory(cmd.endpoint).addQueryParameter(LONG_POLLING_IDX_TAG, "Next").build(); + auto longPollingEndpoint = [&] { + if (!cmd.topic.queryParamMap().contains(LONG_POLLING_IDX_TAG)) { + return URI<>::factory(cmd.topic).addQueryParameter(LONG_POLLING_IDX_TAG, "Next").build(); } else { - return URI<>::factory(cmd.endpoint).build(); + return URI<>::factory(cmd.topic).build(); } }(); @@ -347,29 +349,29 @@ class RestClient : public ClientBase { } else { // failed or server is down -> wait until retry std::this_thread::sleep_for(cmd.timeout); // time-out until potential retry if (_run) { - returnMdpMessage(cmd, result, fmt::format("Long-Polling-GET request failed for {}: {}", cmd.endpoint.str(), static_cast(result.error()))); + returnMdpMessage(cmd, result, fmt::format("Long-Polling-GET request failed for {}: {}", cmd.topic.str(), static_cast(result.error()))); } } } } } else { - throw std::invalid_argument(fmt::format("unsupported scheme '{}' for requested subscription '{}'", cmd.endpoint.scheme(), cmd.endpoint.str())); + throw std::invalid_argument(fmt::format("unsupported scheme '{}' for requested subscription '{}'", cmd.topic.scheme(), cmd.topic.str())); } } void stopSubscription(const Command &cmd) { // stop subscription that matches URI std::scoped_lock lock(_subscriptionLock); - if (equal_with_case_ignore(*cmd.endpoint.scheme(), "http")) { - auto it = _subscription1.find(cmd.endpoint); + if (equal_with_case_ignore(*cmd.topic.scheme(), "http")) { + auto it = _subscription1.find(cmd.topic); if (it != _subscription1.end()) { it->second.stop(); _subscription1.erase(it); return; } - } else if (equal_with_case_ignore(*cmd.endpoint.scheme(), "https")) { + } else if (equal_with_case_ignore(*cmd.topic.scheme(), "https")) { #ifdef CPPHTTPLIB_OPENSSL_SUPPORT - auto it = _subscription2.find(cmd.endpoint); + auto it = _subscription2.find(cmd.topic); if (it != _subscription2.end()) { it->second.stop(); _subscription2.erase(it); @@ -379,7 +381,7 @@ class RestClient : public ClientBase { throw std::runtime_error("https is not supported - enable CPPHTTPLIB_OPENSSL_SUPPORT"); #endif } else { - throw std::invalid_argument(fmt::format("unsupported scheme '{}' for requested subscription '{}'", cmd.endpoint.scheme(), cmd.endpoint.str())); + throw std::invalid_argument(fmt::format("unsupported scheme '{}' for requested subscription '{}'", cmd.topic.scheme(), cmd.topic.str())); } } diff --git a/src/client/test/ClientPublisher_tests.cpp b/src/client/test/ClientPublisher_tests.cpp index beaf1265..144219f4 100644 --- a/src/client/test/ClientPublisher_tests.cpp +++ b/src/client/test/ClientPublisher_tests.cpp @@ -36,10 +36,10 @@ TEST_CASE("Basic get/set test", "[ClientContext]") { std::this_thread::sleep_for(20ms); // allow the request to reach the server server.processRequest([&endpoint](auto &&req, auto &reply) { REQUIRE(req.command == Command::Get); - reply.data = IoBuffer("100"); - reply.error = "404"; - reply.rbac = IoBuffer("rbac"); - reply.endpoint = Message::URI::factory(endpoint).addQueryParameter("ctx", "test_ctx").build(); + reply.data = IoBuffer("100"); + reply.error = "404"; + reply.rbac = IoBuffer("rbac"); + reply.topic = Message::URI::factory(endpoint).addQueryParameter("ctx", "test_ctx").build(); }); std::this_thread::sleep_for(20ms); // hacky: this is needed because the requests are only identified using their uri, so we cannot have multiple requests with identical uris @@ -58,8 +58,8 @@ TEST_CASE("Basic get/set test", "[ClientContext]") { server.processRequest([&endpoint](auto &&req, auto &reply) { REQUIRE(req.command == Command::Set); REQUIRE(req.data.asString() == "abc"); - reply.data = IoBuffer(); - reply.endpoint = Message::URI::factory(endpoint).addQueryParameter("ctx", "test_ctx").build(); + reply.data = IoBuffer(); + reply.topic = Message::URI::factory(endpoint).addQueryParameter("ctx", "test_ctx").build(); }); std::this_thread::sleep_for(10ms); // allow the reply to reach the client REQUIRE(received == 2); @@ -74,7 +74,7 @@ TEST_CASE("Basic subscription test", "[ClientContext]") { ClientContext clientContext{ std::move(clients) }; std::this_thread::sleep_for(100ms); // subscription - auto endpoint = URI::factory(URI(server.addressSub())).scheme("mds").path("/a.topic").addQueryParameter("C", "2").build(); + auto endpoint = URI::factory(URI(server.addressSub())).scheme("mds").path("/a.service").addQueryParameter("C", "2").build(); std::atomic received{ 0 }; clientContext.subscribe(endpoint, [&received, &payload](const Message &update) { if (update.data.size() == payload.size()) { @@ -84,7 +84,8 @@ TEST_CASE("Basic subscription test", "[ClientContext]") { std::this_thread::sleep_for(100ms); // allow for the subscription request to be processed // send notifications for (int i = 0; i < 100; i++) { - server.notify(mdp::SubscriptionTopic::fromURI(endpoint), payload); + server.notify("/a.service?C=2", payload); // received by client + server.notify("/a.service?C=3", payload); // not received by client } std::this_thread::sleep_for(10ms); // allow for all the notifications to reach the client fmt::print("received notifications {}\n", received.load()); diff --git a/src/client/test/CmwClient_tests.cpp b/src/client/test/CmwClient_tests.cpp index f8a71d65..8ba77692 100644 --- a/src/client/test/CmwClient_tests.cpp +++ b/src/client/test/CmwClient_tests.cpp @@ -10,7 +10,7 @@ using opencmw::client::SubscriptionClient; using opencmw::majordomo::MockServer; using opencmw::mdp::Command; using opencmw::mdp::Message; -using opencmw::mdp::SubscriptionTopic; +using opencmw::mdp::Topic; using namespace opencmw; using namespace std::chrono_literals; @@ -34,8 +34,8 @@ TEST_CASE("Basic Client Get/Set Test", "[Client]") { REQUIRE(req.command == Command::Get); REQUIRE(req.data.empty()); REQUIRE(req.clientRequestID.asString() == "1"); - reply.data = opencmw::IoBuffer("42"); - reply.endpoint = Message::URI::factory(uri).addQueryParameter("ctx", "test_ctx1").build(); + reply.data = opencmw::IoBuffer("42"); + reply.topic = Message::URI::factory(uri).addQueryParameter("ctx", "test_ctx1").build(); }); Message result; REQUIRE(client.receive(result)); @@ -50,8 +50,8 @@ TEST_CASE("Basic Client Get/Set Test", "[Client]") { REQUIRE(req.command == Command::Set); REQUIRE(req.data.asString() == "100"); REQUIRE(req.clientRequestID.asString() == "2"); - reply.data = opencmw::IoBuffer(); - reply.endpoint = Message::URI::factory(uri).addQueryParameter("ctx", "test_ctx2").build(); + reply.data = opencmw::IoBuffer(); + reply.topic = Message::URI::factory(uri).addQueryParameter("ctx", "test_ctx2").build(); }); Message result; @@ -71,14 +71,14 @@ TEST_CASE("Basic Client Subscription Test", "[Client]") { subscriptionClient.connect(uri); subscriptionClient.housekeeping(std::chrono::system_clock::now()); - const auto endpoint = URI::UriFactory(uri).path("a.topic").build(); + const auto endpoint = URI::UriFactory(uri).path("/a.service").build(); std::string reqId = "2"; subscriptionClient.subscribe(endpoint, reqId); std::this_thread::sleep_for(50ms); // allow for subscription to be established - server.notify(SubscriptionTopic("a.topic?ctx=test_ctx1"), "101"); - server.notify(SubscriptionTopic("a.topic?ctx=test_ctx2"), "102"); + server.notify("/a.service?ctx=test_ctx1", "101"); + server.notify("/a.service?ctx=test_ctx2", "102"); Message resultOfNotify1; REQUIRE(subscriptionClient.receive(resultOfNotify1)); diff --git a/src/client/test/MockServerTest.cpp b/src/client/test/MockServerTest.cpp index e252770d..b08164fe 100644 --- a/src/client/test/MockServerTest.cpp +++ b/src/client/test/MockServerTest.cpp @@ -62,11 +62,11 @@ TEST_CASE("MockServer Subscription Test", "[mock-server][lambda_handler]") { BrokerMessageNode client(context, ZMQ_SUB); REQUIRE(client.connect(opencmw::URI<>(server.addressSub()))); - client.subscribe(mdp::SubscriptionTopic("a.topic")); + client.subscribe("/a.service"); std::this_thread::sleep_for(10ms); // wait for the subscription to be set-up. todo: investigate more clever way - server.notify(mdp::SubscriptionTopic("a.topic"), "100"); - server.notify(mdp::SubscriptionTopic("a.topic"), "23"); + server.notify("/a.service", "100"); + server.notify("/a.service", "23"); { auto reply = client.tryReadOne(); @@ -74,7 +74,7 @@ TEST_CASE("MockServer Subscription Test", "[mock-server][lambda_handler]") { REQUIRE(reply); REQUIRE(reply->data.asString() == "100"); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->endpoint.str() == "/a.topic"); + REQUIRE(reply->topic.str() == "/a.service"); } { auto reply = client.tryReadOne(); @@ -82,16 +82,16 @@ TEST_CASE("MockServer Subscription Test", "[mock-server][lambda_handler]") { REQUIRE(reply); REQUIRE(reply->data.asString() == "23"); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->endpoint.str() == "/a.topic"); + REQUIRE(reply->topic.str() == "/a.service"); } - server.notify(mdp::SubscriptionTopic("a.topic"), "10"); + server.notify("/a.service", "10"); { auto reply = client.tryReadOne(); fmt::print("{}\n", reply.has_value()); REQUIRE(reply); REQUIRE(reply->data.asString() == "10"); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->endpoint.str() == "/a.topic"); + REQUIRE(reply->topic.str() == "/a.service"); } } diff --git a/src/client/test/RestClient_tests.cpp b/src/client/test/RestClient_tests.cpp index fb2aead0..0f30d46c 100644 --- a/src/client/test/RestClient_tests.cpp +++ b/src/client/test/RestClient_tests.cpp @@ -108,7 +108,7 @@ TEST_CASE("Basic Rest Client Get/Set Test - HTTP", "[Client]") { data.put('C'); Command command; command.command = mdp::Command::Get; - command.endpoint = URI("http://localhost:8080/endPoint"); + command.topic = URI("http://localhost:8080/endPoint"); command.data = std::move(data); command.callback = [&done](const mdp::Message & /*rep*/) { done.store(true, std::memory_order_release); @@ -162,7 +162,7 @@ TEST_CASE("Basic Rest Client Get/Set Test - HTTPS", "[Client]") { data.put(0); Command command; command.command = mdp::Command::Get; - command.endpoint = URI("https://localhost:8080/endPoint"); + command.topic = URI("https://localhost:8080/endPoint"); command.data = std::move(data); command.callback = [&done](const mdp::Message & /*rep*/) { done.store(true, std::memory_order_release); @@ -253,7 +253,7 @@ TEST_CASE("Basic Rest Client Subscribe/Unsubscribe Test", "[Client]") { Command command; command.command = mdp::Command::Subscribe; - command.endpoint = URI("http://localhost:8080/event"); + command.topic = URI("http://localhost:8080/event"); command.data = std::move(data); command.callback = [&receivedRegular, &receivedError](const mdp::Message &rep) { fmt::print("SSE client received reply = '{}' - body size: '{}'\n", rep.data.asString(), rep.data.size()); diff --git a/src/core/include/MdpMessage.hpp b/src/core/include/MdpMessage.hpp index d4a29687..b63fef48 100644 --- a/src/core/include/MdpMessage.hpp +++ b/src/core/include/MdpMessage.hpp @@ -70,7 +70,7 @@ struct BasicMessage { Command command = Command::Invalid; ///< command type (GET, SET, SUBSCRIBE, UNSUBSCRIBE, PARTIAL, FINAL, NOTIFY, READY, DISCONNECT, HEARTBEAT) std::string serviceName{ "/" }; ///< service endpoint name (normally the URI path only), or client source ID (for broker <-> worker messages) IoBuffer clientRequestID; ///< stateful: worker mirrors clientRequestID; stateless: worker generates unique increasing IDs (to detect packet loss) - URI endpoint{ "/" }; ///< URI containing at least and optionally parameters + URI topic{ "/" }; ///< URI containing at least and optionally parameters IoBuffer data; ///< request/reply body -- opaque binary, e.g. YaS-, CmwLight-, JSON-, or HTML-based std::string error; ///< UTF-8 strings containing error code and/or stack-trace (e.g. "404 Not Found") IoBuffer rbac; ///< optional RBAC meta-info -- may contain token, role, signed message hash (implementation dependent) diff --git a/src/core/include/SubscriptionTopic.hpp b/src/core/include/SubscriptionTopic.hpp deleted file mode 100644 index 7646b456..00000000 --- a/src/core/include/SubscriptionTopic.hpp +++ /dev/null @@ -1,170 +0,0 @@ -#ifndef OPENCMW_CORE_SUBSCRIPTIONTOPIC_H -#define OPENCMW_CORE_SUBSCRIPTIONTOPIC_H - -#include // hash_combine -#include - -#include -#include -#include - -#include - -namespace opencmw::mdp { - -using namespace std::string_literals; - -// -// Using strings (even URI-formatted) opens up users to -// create invalid service-topic-parameters combinations. -// SubscriptionTopic serves to make sure the information -// about subscription is passable in a unified manner -// (serializable and deserializable to a string/URI). -// -struct SubscriptionTopic { - using map = std::unordered_map>; - -private: - std::string _service; - std::string _path; - map _params; - - static std::string stripSlashes(std::string_view str, bool addLeadingSlash) { - auto firstNonSlash = str.find_first_not_of("/ "); - if (firstNonSlash == std::string::npos) { - return ""; - } - - auto lastNonSlash = str.find_last_not_of("/ "); - - if (addLeadingSlash) { - return "/"s + std::string(str.data() + firstNonSlash, lastNonSlash - firstNonSlash + 1); - } else { - return std::string(str.data() + firstNonSlash, lastNonSlash - firstNonSlash + 1); - } - } - -public: - SubscriptionTopic() = default; - - template - requires(!std::same_as>) - explicit SubscriptionTopic(PathString &&path) - : SubscriptionTopic(""s, std::forward(path), {}) {} - - SubscriptionTopic(const SubscriptionTopic &other) = default; - SubscriptionTopic &operator=(const SubscriptionTopic &) = default; - SubscriptionTopic(SubscriptionTopic &&) noexcept = default; - SubscriptionTopic &operator=(SubscriptionTopic &&) noexcept = default; - - auto operator<=>(const SubscriptionTopic &) const = default; - - template - static SubscriptionTopic fromURI(const TURI &uri) { - return SubscriptionTopic(""s, uri.path().value_or(""s), uri.queryParamMap()); - } - - template - static SubscriptionTopic fromURIAndServiceName(const TURI &uri, ServiceString &&serviceName) { - return SubscriptionTopic(std::forward(serviceName), uri.path().value_or(""s), uri.queryParamMap()); - } - - opencmw::URI toEndpoint() const { - return opencmw::URI::factory().path(_path).setQuery(_params).build(); - } - - bool empty() const { - return _service.empty() && _path.empty() && _params.empty(); - } - - std::string toZmqTopic() const { - std::string topic = _path; - if (_params.empty()) { - return topic; - } - topic += "?"s; - bool isFirst = true; - // sort params - for (const auto &[key, value] : std::map{ _params.begin(), _params.end() }) { - if (!isFirst) { - topic += "&"s; - } - topic += key; - if (value) { - topic += "="s + opencmw::URI<>::encode(*value); - } - isFirst = false; - } - return topic; - } - - [[nodiscard]] std::size_t hash() const noexcept { - std::size_t seed = 0; - opencmw::detail::hash_combine(seed, _service); - opencmw::detail::hash_combine(seed, _path); - for (const auto &[key, value] : _params) { - opencmw::detail::hash_combine(seed, key); - opencmw::detail::hash_combine(seed, value); - } - - return seed; - } - - std::string_view path() const { return _path; } - std::string_view service() const { return _service; } - const auto ¶ms() const { return _params; } - -private: - template - SubscriptionTopic(ServiceString &&service, PathString &&path, std::unordered_map> params) - : _service(stripSlashes(std::forward(service), false)) - , _path(stripSlashes(std::forward(path), true)) - , _params(std::move(params)) { - if (_path.find("?") != std::string::npos) { - if (_params.size() != 0) { - throw fmt::format("Parameters are not empty, and there are more in the path {} {}\n", _params, _path); - } - auto parsed = opencmw::URI(_path); - _path = parsed.path().value_or("/"); - _params = parsed.queryParamMap(); - } - - auto is_char_valid = [](char c) { - return std::isalnum(c) || c == '.'; - }; - - if (!std::all_of(_service.cbegin(), _service.cbegin(), is_char_valid)) { - throw fmt::format("Invalid service name {}\n", _service); - } - if (!std::all_of(_path.cbegin(), _path.cbegin(), is_char_valid)) { - throw fmt::format("Invalid path {}\n", _path); - } - } -}; - -} // namespace opencmw::mdp - -template<> -struct fmt::formatter { - template - constexpr auto parse(ParseContext &ctx) { - return ctx.begin(); - } - - template - auto format(const opencmw::mdp::SubscriptionTopic &v, FormatContext &ctx) const { - return fmt::format_to(ctx.out(), "[service:{}, path:{}, params:{}]", v.service(), v.path(), v.params()); - } -}; - -namespace std { -template<> -struct hash { - std::size_t operator()(const opencmw::mdp::SubscriptionTopic &k) const { - return k.hash(); - } -}; - -} // namespace std - -#endif diff --git a/src/core/include/Topic.hpp b/src/core/include/Topic.hpp new file mode 100644 index 00000000..33509982 --- /dev/null +++ b/src/core/include/Topic.hpp @@ -0,0 +1,188 @@ +#ifndef OPENCMW_CORE_TOPIC_H +#define OPENCMW_CORE_TOPIC_H + +#include // hash_combine +#include + +#include +#include +#include +#include + +#include + +namespace opencmw::mdp { + +constexpr bool isValidServiceName(std::string_view str) { + auto is_allowed = [](char ch) { + // std::isalnum is not constexpr + return ch == '/' || ch == '.' || ch == '_' || (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); + }; + return str.starts_with("/") && str.size() > 1 && !str.ends_with("/") && std::ranges::all_of(str, is_allowed); +} + +/** + * A topic is the combination of a service name (mandatory) and query parameters (optional). + * + * Examples: + * - /DeviceName/Acquisition - service name only, no params + * - /DeviceName/Acquisition?signal=test - service name (/DeviceName/Acquisition) and query + * - /dashboards - service name (/dashboards) + * - /dashboards/dashboard1?what=header - service name (/dashboards/dashboard1) and query + * + * When encoded as mdp/mds or http/https URLs, the service name is the URL's path (with leading slash), with the + * topic's query parameters being the URI's query parameters (minus e.g. REST-specific parameters like "LongPollingIdx" + * that are not forwarded). + * + * Examples: + * - http://localhost:8080/DeviceName/Acquisition => service: /DeviceName/Acquisition + * - http://localhost:8080/DeviceName/Acquisition?LongPollingIdx=Next&signal=test => service: /DeviceName/Acquisition, query: signal=test + * - mds://localhost:12345/DeviceName/Acquisition?signal=test => service: /DeviceName/Acquisition, query: signal=test + * - mdp://localhost:12345/dashboards/dashboard1?what=header => serviceName: /dashboards/dashboard1, query: what=header + * + * Note that the whole path is considered the service name, and that there's no additional path component denoting + * different entities, objects etc. neither for subscriptions nor GET/SET requests. Requesting specific objects like + * in the "/dashboards/dashboard1" example are handled via the service-matching for requests in the broker, where a + * service name "/dashboard/dashboard1" would match a worker "/dashboard", which then can extract the "dashboard1" component + * from the topic frame (see below). + * + * For subscriptions, only the worker's service name is to be used, any filtering for specific messages is + * done via the query parameters. + * + * On the protocol level, the topic is used in two contexts: + * + * - for ZMQ PUB/SUB subscriptions: ZMQ uses a string-based subscription mechanism, where topics are simple strings + * (allowing trailing wildcards) that must match exactly (or via prefix, if using wildcards). For that purpose + * Topic::toZmqTopic() ensures that the params in e.g. /service?b&a are always ordered alphabetically by key + * (/service?a&b), as /service?a&b and service?b&a are supposed to be equivalent, but wouldn't match with the ZMQ + * mechanism. + * - The OpenCMW MDP topic frame (frame 5), used by the commands GET, SET, SUBSCRIBE, UNSUBSCRIBE, FINAL, PARTIAL and + * NOTIFY: Here the frame contains a URI (with unspecified ordering of query parameters). + **/ +struct Topic { + using Params = std::unordered_map>; + +private: + std::string _service; + Params _params; + +public: + Topic(const Topic &other) = default; + Topic &operator=(const Topic &) = default; + Topic(Topic &&) noexcept = default; + Topic &operator=(Topic &&) noexcept = default; + + bool operator==(const Topic &) const = default; + + /** + * Parses subscription from a "service" or "service?param" string + * + * @param str A string where the first path segment is the service name, e.g. "/service/" or "/service?param" + * @param params Optional query parameters, if non-empty, @p str must not contain query parameters + */ + static Topic fromString(std::string_view str, Params params = {}) { + return Topic(str, std::move(params)); + } + + template + static Topic fromMdpTopic(const URI &topic) { + return Topic(topic.path().value_or("/"), topic.queryParamMap()); + } + + static Topic fromZmqTopic(std::string_view topic) { + return fromString(topic, {}); + } + + opencmw::URI toMdpTopic() const { + return opencmw::URI::factory().path(_service).setQuery(_params).build(); + } + + std::string toZmqTopic() const { + using namespace std::string_literals; + std::string zmqTopic = _service; + if (_params.empty()) { + return zmqTopic; + } + zmqTopic += "?"s; + bool isFirst = true; + // sort params + for (const auto &[key, value] : std::map{ _params.begin(), _params.end() }) { + if (!isFirst) { + zmqTopic += "&"s; + } + zmqTopic += key; + if (value) { + zmqTopic += "="s + opencmw::URI<>::encode(*value); + } + isFirst = false; + } + return zmqTopic; + } + + [[nodiscard]] std::size_t hash() const noexcept { + std::size_t seed = 0; + opencmw::detail::hash_combine(seed, _service); + for (const auto &[key, value] : _params) { + opencmw::detail::hash_combine(seed, key); + opencmw::detail::hash_combine(seed, value); + } + + return seed; + } + + std::string_view service() const { return _service; } + const auto ¶ms() const { return _params; } + + void addParam(std::string_view key, std::string_view value) { + _params[std::string(key)] = std::string(value); + } + +private: + static std::string parseService(std::string_view str) { + if (const auto queryPos = str.find_first_of("?"); queryPos != std::string::npos) { + str = str.substr(0, queryPos); + } + + while (str.ends_with("/")) { + str.remove_suffix(1); + } + + auto r = std::string(str); + + if (!r.starts_with("/")) { + return "/" + r; + } + + return r; + } + + Topic(std::string_view serviceOrServiceAndQuery, Params params) + : _service(parseService(serviceOrServiceAndQuery)) + , _params(std::move(params)) { + if (serviceOrServiceAndQuery.find("?") != std::string::npos) { + if (!_params.empty()) { + throw std::invalid_argument(fmt::format("Parameters are not empty ({}), and there are more in the service string ({})\n", _params, serviceOrServiceAndQuery)); + } + const auto parsed = opencmw::URI(std::string(serviceOrServiceAndQuery)); + _params = parsed.queryParamMap(); + } + + if (!isValidServiceName(_service)) { + throw std::invalid_argument(fmt::format("Invalid service name '{}'\n", _service)); + } + } +}; + +} // namespace opencmw::mdp + +namespace std { +template<> +struct hash { + std::size_t operator()(const opencmw::mdp::Topic &k) const { + return k.hash(); + } +}; + +} // namespace std + +#endif diff --git a/src/core/test/URI_tests.cpp b/src/core/test/URI_tests.cpp index 17641be8..46cd46d6 100644 --- a/src/core/test/URI_tests.cpp +++ b/src/core/test/URI_tests.cpp @@ -83,15 +83,18 @@ TEST_CASE("query parsing", "[URI][query_parsing]") { using TestCase = std::pair>>; static const std::array testCases = { + TestCase{ "", {} }, TestCase{ "scheme:/host/property", {} }, TestCase{ "scheme:/host/property?testKey1=42", { { "testKey1", "42" } } }, + TestCase{ "?testKey1=42", { { "testKey1", "42" } } }, TestCase{ "scheme:/host/property?testKey1=42&testKey2=24", { { "testKey1", "42" }, { "testKey2", "24" } } }, - TestCase{ "scheme:/host/property?k0;k1=v1;k2=v2&k3&k4=", { { "k0", std::nullopt }, { "k1", "v1" }, { "k2", "v2" }, { "k3", std::nullopt }, { "k4", std::nullopt } } } + TestCase{ "scheme:/host/property?k0;k1=v1;k2=v2&k3&k4=", { { "k0", std::nullopt }, { "k1", "v1" }, { "k2", "v2" }, { "k3", std::nullopt }, { "k4", std::nullopt } } }, + TestCase{ "?k0;k1=v1;k2=v2&k3&k4=", { { "k0", std::nullopt }, { "k1", "v1" }, { "k2", "v2" }, { "k3", std::nullopt }, { "k4", std::nullopt } } } }; - for (const auto &testCase : testCases) { - REQUIRE_NOTHROW(opencmw::URI<>(testCase.first)); - REQUIRE(opencmw::URI<>(testCase.first).queryParamMap() == testCase.second); + for (const auto &[uriString, expectedQuery] : testCases) { + REQUIRE_NOTHROW(opencmw::URI<>(uriString)); + REQUIRE(opencmw::URI<>(uriString).queryParamMap() == expectedQuery); } } diff --git a/src/majordomo/include/majordomo/Broker.hpp b/src/majordomo/include/majordomo/Broker.hpp index d23cb66b..b871e024 100644 --- a/src/majordomo/include/majordomo/Broker.hpp +++ b/src/majordomo/include/majordomo/Broker.hpp @@ -16,6 +16,7 @@ #include #include "Rbac.hpp" +#include "Topic.hpp" #include "URI.hpp" #include @@ -138,7 +139,7 @@ inline std::string findDnsEntry(std::string_view brokerName, std::unordered_map< const auto queryScheme = query.scheme(); const auto queryPath = query.path().value_or(""); const auto strippedQueryPath = stripStart(queryPath, "/"); - const auto stripStartFromSearchPath = strippedQueryPath.starts_with("mmi.") ? fmt::format("/{}", brokerName) : "/"; // crop initial broker name for broker-specific MMI services + const auto stripStartFromSearchPath = strippedQueryPath.starts_with("/mmi.") ? fmt::format("/{}", brokerName) : "/"; // crop initial broker name for broker-specific MMI services const auto entryMatches = [&queryScheme, &strippedQueryPath, &stripStartFromSearchPath](const auto &dnsEntry) { if (queryScheme && !iequal(dnsEntry.scheme().value_or(""), *queryScheme)) { @@ -192,11 +193,10 @@ class Broker { const zmq::Socket &socket; const std::string id; const std::string serviceName; - const std::string serviceNameTopic; Timestamp expiry; explicit Worker(const zmq::Socket &s, const std::string &id_, const std::string &serviceName_, Timestamp expiry_) - : socket(s), id{ std::move(id_) }, serviceName{ std::move(serviceName_) }, serviceNameTopic(std::string("/") + serviceName), expiry{ std::move(expiry_) } {} + : socket(s), id{ std::move(id_) }, serviceName{ std::move(serviceName_) }, expiry{ std::move(expiry_) } {} }; struct Service { @@ -260,21 +260,21 @@ class Broker { const std::string brokerName; private: - Timestamp _heartbeatAt = Clock::now() + settings.heartbeatInterval; - SubscriptionMatcher _subscriptionMatcher; - std::unordered_map> _subscribedClientsByTopic; // topic -> client IDs - std::unordered_map _subscribedTopics; // topic -> subscription count - std::unordered_map _clients; - std::unordered_map _workers; - std::unordered_map _services; - std::unordered_map _dnsCache; - std::set _dnsAddresses; - Timestamp _dnsHeartbeatAt; - bool _connectedToDns = false; - - const IoBuffer _rbac = IoBuffer("RBAC=ADMIN,abcdef12345"); - - std::atomic _shutdownRequested = false; + Timestamp _heartbeatAt = Clock::now() + settings.heartbeatInterval; + SubscriptionMatcher _subscriptionMatcher; + std::unordered_map> _subscribedClientsByTopic; // topic -> client IDs + std::unordered_map _subscribedTopics; // topic -> subscription count + std::unordered_map _clients; + std::unordered_map _workers; + std::unordered_map _services; + std::unordered_map _dnsCache; + std::set _dnsAddresses; + Timestamp _dnsHeartbeatAt; + bool _connectedToDns = false; + + const IoBuffer _rbac = IoBuffer("RBAC=ADMIN,abcdef12345"); + + std::atomic _shutdownRequested = false; // Sockets collection. The Broker class will be used as the handler const zmq::Socket _routerSocket; @@ -291,7 +291,9 @@ class Broker { , _pubSocket(context, ZMQ_XPUB) , _subSocket(context, ZMQ_SUB) , _dnsSocket(context, ZMQ_DEALER) { - addInternalService("mmi.dns", [this](BrokerMessage &&message) { + assert(mdp::isValidServiceName(brokerName)); + + addInternalService("/mmi.dns", [this](BrokerMessage &&message) { using namespace std::literals; message.command = mdp::Command::Final; @@ -327,9 +329,9 @@ class Broker { return message; }); - addInternalService("mmi.echo", [](BrokerMessage &&message) { return message; }); + addInternalService("/mmi.echo", [](BrokerMessage &&message) { return message; }); - addInternalService("mmi.service", [this](BrokerMessage &&message) { + addInternalService("/mmi.service", [this](BrokerMessage &&message) { message.command = mdp::Command::Final; if (message.data.empty()) { #if not defined(__EMSCRIPTEN__) and (not defined(__clang__) or (__clang_major__ >= 16)) @@ -355,7 +357,7 @@ class Broker { return message; }); - addInternalService("mmi.openapi", [this](BrokerMessage &&message) { + addInternalService("/mmi.openapi", [this](BrokerMessage &&message) { message.command = mdp::Command::Final; const auto serviceName = std::string(message.data.asString()); const auto serviceIt = _services.find(serviceName); @@ -509,7 +511,7 @@ class Broker { } private: - void subscribe(const mdp::SubscriptionTopic &topic) { + void subscribe(const mdp::Topic &topic) { auto [it, inserted] = _subscribedTopics.try_emplace(topic, 0); it->second++; if (it->second == 1) { @@ -518,17 +520,16 @@ class Broker { } } - void unsubscribe(const mdp::SubscriptionTopic &topic) { + void unsubscribe(const mdp::Topic &topic) { auto it = _subscribedTopics.find(topic); - if (it != _subscribedTopics.end()) { - it->second--; - if (it->second == 0) { - const auto topicStr = topic.toZmqTopic(); - zmq::invoke(zmq_setsockopt, _subSocket, ZMQ_UNSUBSCRIBE, topicStr.data(), topicStr.size()).assertSuccess(); - _subscribedTopics.erase(it); - } - } else { - throw fmt::format("Nothing subscribed to {}\n", topic); + if (it == _subscribedTopics.end()) { + return; + } + it->second--; + if (it->second == 0) { + const auto topicStr = topic.toZmqTopic(); + zmq::invoke(zmq_setsockopt, _subSocket, ZMQ_UNSUBSCRIBE, topicStr.data(), topicStr.size()).assertSuccess(); + _subscribedTopics.erase(it); } } @@ -546,16 +547,19 @@ class Broker { return false; } - const auto topicString = data.substr(1); - const auto topicURI = URI(std::string(topicString)); - const auto topic = mdp::SubscriptionTopic::fromURI(topicURI); - + const auto topicString = data.substr(1); + std::optional topic; + try { + topic = mdp::Topic::fromZmqTopic(topicString); + } catch (...) { + // malformed topic, ignore + return false; + } if (data[0] == '\x1') { - subscribe(topic); + subscribe(*topic); } else { - unsubscribe(topic); + unsubscribe(*topic); } - return true; } @@ -571,7 +575,7 @@ class Broker { if (message.protocolName == mdp::clientProtocol) { switch (message.command) { case mdp::Command::Ready: { - if (const auto topicURI = URI(message.endpoint.str()); topicURI.scheme()) { + if (const auto topicURI = URI(message.topic.str()); topicURI.scheme()) { auto [iter, inserted] = _dnsCache.try_emplace(message.serviceName, std::string(message.sourceId), message.serviceName); iter->second.uris.insert(topicURI); iter->second.expiry = updatedDnsExpiry(); @@ -579,20 +583,30 @@ class Broker { return true; } case mdp::Command::Subscribe: { - const auto subscription = mdp::SubscriptionTopic::fromURIAndServiceName(message.endpoint, message.serviceName); - - subscribe(subscription); + std::optional subscription; + try { + subscription = mdp::Topic::fromMdpTopic(message.topic); + } catch (...) { + // malformed topic, ignore + return false; + } - auto [it, inserted] = _subscribedClientsByTopic.try_emplace(subscription, std::set{}); + subscribe(*subscription); + auto [it, inserted] = _subscribedClientsByTopic.try_emplace(*subscription); it->second.emplace(message.sourceId); return true; } case mdp::Command::Unsubscribe: { - const auto subscription = mdp::SubscriptionTopic::fromURIAndServiceName(message.endpoint, message.serviceName); - - unsubscribe(subscription); + std::optional subscription; + try { + subscription = mdp::Topic::fromMdpTopic(message.topic); + } catch (...) { + // malformed topic, ignore + return false; + } - auto it = _subscribedClientsByTopic.find(subscription); + unsubscribe(*subscription); + auto it = _subscribedClientsByTopic.find(*subscription); if (it != _subscribedClientsByTopic.end()) { it->second.erase(message.sourceId); if (it->second.empty()) { @@ -676,22 +690,25 @@ class Broker { } void dispatchMessageToMatchingSubscribers(BrokerMessage &&message) { - const auto subscription = mdp::SubscriptionTopic::fromURIAndServiceName(message.endpoint, message.serviceName); + std::optional notification; + try { + notification = mdp::Topic::fromMdpTopic(message.topic); + } catch (...) { + // malformed topic, ignore + return; + } // TODO avoid clone() for last message sent out for (const auto &[topic, _] : _subscribedTopics) { - if (_subscriptionMatcher(subscription, topic)) { - auto copy = message; - const auto subscriptionURI = mdp::Message::URI(std::string(subscription.path())); - copy.endpoint = subscriptionURI; - copy.sourceId = topic.toZmqTopic(); + if (_subscriptionMatcher(*notification, topic)) { + auto copy = message; + copy.sourceId = topic.toZmqTopic(); zmq::send(std::move(copy), _pubSocket).assertSuccess(); const auto it = _subscribedClientsByTopic.find(topic); if (it != _subscribedClientsByTopic.end()) { for (const auto &clientId : it->second) { auto clientCopy = message; - clientCopy.endpoint = subscriptionURI; clientCopy.sourceId = clientId; zmq::send(std::move(clientCopy), _routerSocket).assertSuccess(); } @@ -747,9 +764,9 @@ class Broker { // not implemented -- reply according to Majordomo Management Interface (MMI) as defined in http://rfc.zeromq.org/spec:8 - auto reply = std::move(clientMessage); - reply.command = mdp::Command::Final; - reply.endpoint = INTERNAL_SERVICE_NAMES_URI; + auto reply = std::move(clientMessage); + reply.command = mdp::Command::Final; + reply.topic = INTERNAL_SERVICE_NAMES_URI; reply.data.clear(); reply.error = fmt::format("unknown service (error 501): '{}'", reply.serviceName); reply.rbac = _rbac; @@ -837,7 +854,7 @@ class Broker { // notify potential listeners BrokerMessage notify; notify.serviceName = INTERNAL_SERVICE_NAMES; - notify.endpoint = INTERNAL_SERVICE_NAMES_URI; + notify.topic = INTERNAL_SERVICE_NAMES_URI; notify.clientRequestID = IoBuffer(brokerName.data(), brokerName.size()); notify.sourceId = INTERNAL_SERVICE_NAMES; zmq::send(std::move(notify), _pubSocket).assertSuccess(); @@ -899,7 +916,6 @@ class Broker { disconnect.command = mdp::Command::Disconnect; disconnect.sourceId = worker.id; disconnect.serviceName = worker.serviceName; - disconnect.endpoint = mdp::Message::URI(worker.serviceNameTopic); disconnect.data = IoBuffer("broker shutdown"); disconnect.rbac = _rbac; zmq::send(std::move(disconnect), worker.socket).assertSuccess(); @@ -920,8 +936,8 @@ class Broker { if (Clock::now() > _dnsHeartbeatAt || force) { const auto ready = createDnsReadyMessage(); for (const auto &dnsAddress : _dnsAddresses) { - auto toSend = ready; - toSend.endpoint = mdp::Message::URI(dnsAddress); + auto toSend = ready; + toSend.topic = mdp::Message::URI(dnsAddress); registerWithDnsServices(std::move(toSend)); } for (const auto &[name, service] : _services) { @@ -935,14 +951,14 @@ class Broker { auto ready = createDnsReadyMessage(); const auto address = fmt::format("{}/{}", dnsAddress, detail::stripStart(serviceName, "/")); // TODO use URI factory? - ready.endpoint = mdp::Message::URI(address); + ready.topic = mdp::Message::URI(address); registerWithDnsServices(std::move(ready)); } } void registerWithDnsServices(mdp::Message &&readyMessage) { auto [it, inserted] = _dnsCache.try_emplace(brokerName, std::string(), brokerName); - it->second.uris.insert(URI(readyMessage.endpoint.str())); + it->second.uris.insert(URI(readyMessage.topic.str())); it->second.expiry = updatedDnsExpiry(); zmq::send(std::move(readyMessage), _dnsSocket).ignoreResult(); } diff --git a/src/majordomo/include/majordomo/Constants.hpp b/src/majordomo/include/majordomo/Constants.hpp index 294d0316..ff6e4b4a 100644 --- a/src/majordomo/include/majordomo/Constants.hpp +++ b/src/majordomo/include/majordomo/Constants.hpp @@ -18,7 +18,7 @@ const opencmw::URI<> INPROC_BROKER = opencmw::URI<>("inp const opencmw::URI<> INTERNAL_ADDRESS_BROKER = opencmw::URI<>::factory(INPROC_BROKER).path(SUFFIX_ROUTER).build(); const opencmw::URI<> INTERNAL_ADDRESS_PUBLISHER = opencmw::URI<>::factory(INPROC_BROKER).path(SUFFIX_PUBLISHER).build(); const opencmw::URI<> INTERNAL_ADDRESS_SUBSCRIBE = opencmw::URI<>::factory(INPROC_BROKER).path(SUFFIX_SUBSCRIBE).build(); -/*constexpr*/ const std::string INTERNAL_SERVICE_NAMES = "mmi.service"; +/*constexpr*/ const std::string INTERNAL_SERVICE_NAMES = "/mmi.service"; const opencmw::URI<> INTERNAL_SERVICE_NAMES_URI = opencmw::URI<>("/mmi.service"); } // namespace opencmw::majordomo diff --git a/src/majordomo/include/majordomo/RestBackend.hpp b/src/majordomo/include/majordomo/RestBackend.hpp index b353984d..3d627e30 100644 --- a/src/majordomo/include/majordomo/RestBackend.hpp +++ b/src/majordomo/include/majordomo/RestBackend.hpp @@ -27,6 +27,7 @@ #include #include #include +#include // Majordomo #include @@ -69,7 +70,7 @@ constexpr std::size_t MAX_CACHED_REPLIES = 32; namespace detail { // Provides a safe alternative to getenv -const char *getEnvFilenameOr(const char *field, const char *defaultValue) { +inline const char *getEnvFilenameOr(const char *field, const char *defaultValue) { const char *result = ::getenv(field); if (result == nullptr) { result = defaultValue; @@ -109,7 +110,7 @@ enum class RestMethod { Invalid }; -std::string_view acceptedMimeForRequest(const auto &request) { +inline std::string_view acceptedMimeForRequest(const auto &request) { static constexpr std::array acceptableMimeTypes = { MIME::JSON.typeName(), MIME::HTML.typeName(), MIME::BINARY.typeName() }; @@ -160,7 +161,7 @@ bool respondWithError(auto &response, std::string_view message) { return true; }; -bool respondWithServicesList(auto &broker, const httplib::Request &request, httplib::Response &response) { +inline bool respondWithServicesList(auto &broker, const httplib::Request &request, httplib::Response &response) { // Mmi is not a MajordomoWorker, so it doesn't know JSON (TODO) const auto acceptedFormat = acceptedMimeForRequest(request); @@ -189,8 +190,8 @@ bool respondWithServicesList(auto &broker, const httplib::Request &request, http // sort services, move mmi. services to the end auto serviceLessThan = [](const auto &lhs, const auto &rhs) { - const auto lhsIsMmi = lhs.name.starts_with("mmi."); - const auto rhsIsMmi = rhs.name.starts_with("mmi."); + const auto lhsIsMmi = lhs.name.starts_with("/mmi."); + const auto rhsIsMmi = rhs.name.starts_with("/mmi."); if (lhsIsMmi != rhsIsMmi) { return rhsIsMmi; } @@ -211,17 +212,10 @@ bool respondWithServicesList(auto &broker, const httplib::Request &request, http } } -struct SubscriptionInfo { - std::string serviceName; - std::string topicName; - auto operator<=>(const SubscriptionInfo &other) const = default; - bool operator==(const SubscriptionInfo &other) const = default; -}; - struct Connection { - zmq::Socket notificationSubscriptionSocket; - zmq::Socket requestResponseSocket; - SubscriptionInfo subscriptionInfo; + zmq::Socket notificationSubscriptionSocket; + zmq::Socket requestResponseSocket; + std::string subscriptionKey; using Timestamp = std::chrono::time_point; Timestamp lastUsed = std::chrono::system_clock::now(); @@ -239,17 +233,17 @@ struct Connection { Connection(Connection &&other) noexcept : notificationSubscriptionSocket(std::move(other.notificationSubscriptionSocket)) , requestResponseSocket(std::move(other.requestResponseSocket)) - , subscriptionInfo(std::move(other.subscriptionInfo)) + , subscriptionKey(std::move(other.subscriptionKey)) , lastUsed(std::move(other.lastUsed)) , _cachedReplies(std::move(other._cachedReplies)) , _nextPollingIndex(other._nextPollingIndex) { } public: - Connection(const zmq::Context &context, SubscriptionInfo _subscriptionInfo) + Connection(const zmq::Context &context, std::string _subscriptionKey) : notificationSubscriptionSocket(context, ZMQ_DEALER) , requestResponseSocket(context, ZMQ_SUB) - , subscriptionInfo(std::move(_subscriptionInfo)) {} + , subscriptionKey(std::move(_subscriptionKey)) {} Connection(const Connection &other) = delete; Connection &operator=(const Connection &) = delete; @@ -326,27 +320,26 @@ class RestBackend : public Mode { URI<> _restAddress; private: - std::jthread _connectionUpdaterThread; - std::shared_mutex _connectionsMutex; - - std::map> _connectionForService; + std::jthread _connectionUpdaterThread; + std::shared_mutex _connectionsMutex; + std::map> _connectionForService; public: using BrokerType = Broker; // returns a connection with refcount 1. Make sure you lower it to // zero at some point - detail::Connection *notificationSubscriptionConnectionFor(const detail::SubscriptionInfo &subscriptionInfo) { + detail::Connection *notificationSubscriptionConnectionFor(const std::string &subscriptionKey) { detail::WriteLock lock(_connectionsMutex); // TODO: No need to find + emplace as separate steps - if (auto it = _connectionForService.find(subscriptionInfo); it != _connectionForService.end()) { + if (auto it = _connectionForService.find(subscriptionKey); it != _connectionForService.end()) { auto *connection = it->second.get(); connection->increaseReferenceCount(); return connection; } auto [it, inserted] = _connectionForService.emplace(std::piecewise_construct, - std::forward_as_tuple(subscriptionInfo), - std::forward_as_tuple(std::make_unique(_broker.context, subscriptionInfo))); + std::forward_as_tuple(subscriptionKey), + std::forward_as_tuple(std::make_unique(_broker.context, subscriptionKey))); if (!inserted) { assert(inserted); @@ -360,8 +353,8 @@ class RestBackend : public Mode { mdp::Message subscribeMessage; subscribeMessage.protocolName = mdp::clientProtocol; subscribeMessage.command = mdp::Command::Subscribe; - subscribeMessage.serviceName = subscriptionInfo.serviceName; - subscribeMessage.endpoint = mdp::Message::URI(subscriptionInfo.topicName); + subscribeMessage.topic = mdp::Message::URI(subscriptionKey); + subscribeMessage.serviceName = subscribeMessage.topic.path().value_or("/"); if (!zmq::send(std::move(subscribeMessage), connection->notificationSubscriptionSocket)) { std::terminate(); @@ -383,8 +376,8 @@ class RestBackend : public Mode { detail::WriteLock lock(_connectionsMutex); // Expired subscriptions cleanup - std::vector expiredSubscriptions; - for (auto &[info, connection] : _connectionForService) { + std::vector expiredSubscriptions; + for (auto &[subscriptionKey, connection] : _connectionForService) { // fmt::print("Reference count is {}\n", connection->referenceCount()); if (connection->referenceCount() == 0) { auto connectionLock = connection->writeLock(); @@ -392,12 +385,12 @@ class RestBackend : public Mode { continue; } if (std::chrono::system_clock::now() - connection->lastUsed > UNUSED_SUBSCRIPTION_EXPIRATION_TIME) { - expiredSubscriptions.push_back(&info); + expiredSubscriptions.push_back(subscriptionKey); } } } - for (auto *subscriptionInfo : expiredSubscriptions) { - _connectionForService.erase(*subscriptionInfo); + for (const auto &subscriptionKey : expiredSubscriptions) { + _connectionForService.erase(subscriptionKey); } // Reading the missed messages @@ -465,21 +458,37 @@ class RestBackend : public Mode { virtual ~RestBackend() { _svr.stop(); + // shutdown thread before _connectionForService is destroyed + _connectionUpdaterThread.request_stop(); + _connectionUpdaterThread.join(); } auto handleServiceRequest(const httplib::Request &request, httplib::Response &response, const httplib::ContentReader *content_reader_ = nullptr) { using detail::RestMethod; - auto service = [&] { - if (!request.path.empty() && request.path[0] == '/') { - return std::string_view(request.path.begin() + 1, request.path.end()); - } else { - return std::string_view(request.path); + + auto convertParams = [](const httplib::Params ¶ms) { + mdp::Topic::Params r; + for (const auto &[key, value] : params) { + if (key == "LongPollingIdx" || key == "SubscriptionContext") { + continue; + } + if (value.empty()) { + r[key] = std::nullopt; + } else { + r[key] = value; + } } - }(); + return r; + }; - if (service.empty()) { - return detail::respondWithError(response, "Error: Service not specified\n"); + std::optional maybeTopic; + + try { + maybeTopic = mdp::Topic::fromString(request.path, convertParams(request.params)); + } catch (const std::exception &e) { + return detail::respondWithError(response, fmt::format("Error: {}\n", e.what())); } + auto topic = std::move(*maybeTopic); auto restMethod = [&] { // clang-format off @@ -497,8 +506,6 @@ class RestBackend : public Mode { // clang-format on }(); - std::string subscriptionContext; - for (const auto &[key, value] : request.params) { if (key == "LongPollingIdx") { // This parameter is not passed on, it just means we @@ -506,7 +513,7 @@ class RestBackend : public Mode { restMethod = value == "Subscription" ? RestMethod::Subscribe : RestMethod::LongPoll; } else if (key == "SubscriptionContext") { - subscriptionContext = value; + topic = mdp::Topic::fromString(value, {}); // params are parsed from value } } @@ -519,13 +526,13 @@ class RestBackend : public Mode { switch (restMethod) { case RestMethod::Get: case RestMethod::Post: - return worker.respondWithPubSub(request, response, service, restMethod, content_reader_); + return worker.respondWithPubSub(request, response, topic, restMethod, content_reader_); case RestMethod::LongPoll: - return worker.respondWithLongPoll(request, response, service, subscriptionContext); + return worker.respondWithLongPoll(request, response, topic); case RestMethod::Subscribe: - return worker.respondWithSubscription(response, service, subscriptionContext); + return worker.respondWithSubscription(response, topic); default: // std::unreachable() is C++23 @@ -592,11 +599,11 @@ struct RestBackend::RestWorker { zmq_pollitem_t pollItem{}; - RestWorker(RestBackend &rest) + explicit RestWorker(RestBackend &rest) : restBackend(rest) { } - RestWorker(RestWorker &&other) = default; + RestWorker(RestWorker &&other) noexcept = default; detail::Connection connect() { detail::Connection connection(restBackend._broker.context, {}); @@ -608,7 +615,7 @@ struct RestBackend::RestWorker { return std::move(connection).unsafeMove(); } - bool respondWithPubSub(const httplib::Request &request, httplib::Response &response, const std::string_view &service, detail::RestMethod restMethod, const httplib::ContentReader *content_reader_ = nullptr) { + bool respondWithPubSub(const httplib::Request &request, httplib::Response &response, mdp::Topic topic, detail::RestMethod restMethod, const httplib::ContentReader *content_reader_ = nullptr) { // clang-format off const mdp::Command mdpMessageCommand = restMethod == detail::RestMethod::Post ? mdp::Command::Set : @@ -643,14 +650,11 @@ struct RestBackend::RestWorker { mdp::Message message; message.protocolName = mdp::clientProtocol; message.command = mdpMessageCommand; - message.serviceName = std::string(service); const auto acceptedFormat = detail::acceptedMimeForRequest(request); - uri = std::move(uri).addQueryParameter("contentType", std::string(acceptedFormat)); - - auto topic = std::string(service) + uri.toString(); - - message.endpoint = mdp::Message::URI(topic); + topic.addParam("contentType", acceptedFormat); + message.serviceName = std::string(topic.service()); + message.topic = topic.toMdpTopic(); if (request.is_multipart_form_data()) { if (content_reader_ != nullptr) { @@ -711,7 +715,7 @@ struct RestBackend::RestWorker { } else { response.status = HTTP_OK; - response.set_header("X-OPENCMW-TOPIC", responseMessage->endpoint.str().data()); + response.set_header("X-OPENCMW-TOPIC", responseMessage->topic.str().data()); response.set_header("X-OPENCMW-SERVICE-NAME", responseMessage->serviceName.data()); response.set_header("Access-Control-Allow-Origin", "*"); const auto data = responseMessage->data.asString(); @@ -725,10 +729,9 @@ struct RestBackend::RestWorker { return true; } - bool respondWithSubscription(httplib::Response &response, const std::string_view &service, const std::string_view &topic) { - detail::SubscriptionInfo subscriptionInfo{ std::string(service), std::string(topic) }; - - auto *connection = restBackend.notificationSubscriptionConnectionFor(subscriptionInfo); + bool respondWithSubscription(httplib::Response &response, const mdp::Topic &subscription) { + const auto subscriptionKey = subscription.toZmqTopic(); + auto *connection = restBackend.notificationSubscriptionConnectionFor(subscriptionKey); assert(connection); response.set_header("Access-Control-Allow-Origin", "*"); @@ -755,11 +758,11 @@ struct RestBackend::RestWorker { return true; } - bool respondWithLongPollRedirect(const httplib::Request &request, httplib::Response &response, const std::string_view &subscriptionContext, detail::PollingIndex redirectLongPollingIdx) { + bool respondWithLongPollRedirect(const httplib::Request &request, httplib::Response &response, const mdp::Topic &subscription, detail::PollingIndex redirectLongPollingIdx) { auto uri = URI<>::factory() .path(request.path) .addQueryParameter("LongPollingIdx", std::to_string(redirectLongPollingIdx)) - .addQueryParameter("SubscriptionContext", std::string(subscriptionContext)); + .addQueryParameter("SubscriptionContext", subscription.toMdpTopic().str()); // copy over the original query parameters addParameters(request, uri); @@ -769,14 +772,14 @@ struct RestBackend::RestWorker { return true; } - bool respondWithLongPoll(const httplib::Request &request, httplib::Response &response, const std::string_view &service, const std::string_view &topic) { + bool respondWithLongPoll(const httplib::Request &request, httplib::Response &response, const mdp::Topic &subscription) { // TODO: After the URIs are formalized, rethink service and topic auto uri = URI<>::factory(); addParameters(request, uri); - detail::SubscriptionInfo subscriptionInfo{ std::string(service), std::string(topic) }; + const auto subscriptionKey = subscription.toZmqTopic(); - const auto longPollingIdxIt = request.params.find("LongPollingIdx"); + const auto longPollingIdxIt = request.params.find("LongPollingIdx"); if (longPollingIdxIt == request.params.end()) { return detail::respondWithError(response, "Error: LongPollingIdx parameter not specified"); } @@ -788,10 +791,10 @@ struct RestBackend::RestWorker { detail::PollingIndex nextPollingIndex = 0; detail::Connection *connection = nullptr; }; - auto fetchCache = [this, &subscriptionInfo] { + auto fetchCache = [this, &subscriptionKey] { std::shared_lock lock(restBackend._connectionsMutex); auto &recycledConnectionForService = restBackend._connectionForService; - if (auto it = recycledConnectionForService.find(subscriptionInfo); it != recycledConnectionForService.cend()) { + if (auto it = recycledConnectionForService.find(subscriptionKey); it != recycledConnectionForService.cend()) { auto *connectionCache = it->second.get(); detail::Connection::KeepAlive keep(connectionCache); auto connectionCacheLock = connectionCache->readLock(); @@ -818,19 +821,19 @@ struct RestBackend::RestWorker { response.set_header("Access-Control-Allow-Origin", "*"); if (longPollingIdxParam == "Next") { - return respondWithLongPollRedirect(request, response, topic, cache.nextPollingIndex); + return respondWithLongPollRedirect(request, response, subscription, cache.nextPollingIndex); } if (longPollingIdxParam == "Last") { if (cache.connection != nullptr) { - return respondWithLongPollRedirect(request, response, topic, cache.nextPollingIndex - 1); + return respondWithLongPollRedirect(request, response, subscription, cache.nextPollingIndex - 1); } else { - return respondWithLongPollRedirect(request, response, topic, cache.nextPollingIndex); + return respondWithLongPollRedirect(request, response, subscription, cache.nextPollingIndex); } } if (longPollingIdxParam == "FirstAvailable") { - return respondWithLongPollRedirect(request, response, topic, cache.firstCachedIndex); + return respondWithLongPollRedirect(request, response, subscription, cache.firstCachedIndex); } if (std::from_chars(longPollingIdxParam.data(), longPollingIdxParam.data() + longPollingIdxParam.size(), requestedLongPollingIdx).ec != std::errc{}) { @@ -854,7 +857,7 @@ struct RestBackend::RestWorker { } // Fallback to creating a connection and waiting - auto *connection = restBackend.notificationSubscriptionConnectionFor(subscriptionInfo); + auto *connection = restBackend.notificationSubscriptionConnectionFor(subscriptionKey); assert(connection); detail::Connection::KeepAlive keep(connection); diff --git a/src/majordomo/include/majordomo/SubscriptionMatcher.hpp b/src/majordomo/include/majordomo/SubscriptionMatcher.hpp index 55081eff..61944158 100644 --- a/src/majordomo/include/majordomo/SubscriptionMatcher.hpp +++ b/src/majordomo/include/majordomo/SubscriptionMatcher.hpp @@ -2,10 +2,9 @@ #define OPENCMW_MAJORDOMO_SUBSCRIPTIONMATCHER_H #include -#include #include +#include -#include #include namespace opencmw::majordomo { @@ -15,29 +14,26 @@ class SubscriptionMatcher { std::unordered_map> _filters; public: - using URI = const opencmw::URI; // relaxed because we need "*" - template void addFilter(const std::string &key) { _filters.emplace(key, std::make_unique()); } - bool operator()(const mdp::SubscriptionTopic ¬ified, const mdp::SubscriptionTopic &subscriber) const noexcept { - return testPathOnly(notified.service(), subscriber.service()) - && testPathOnly(notified.path(), subscriber.path()) + bool operator()(const mdp::Topic ¬ified, const mdp::Topic &subscriber) const noexcept { + return notified.service() == subscriber.service() && testQueries(notified, subscriber); } private: - bool testQueries(const mdp::SubscriptionTopic ¬ified, const mdp::SubscriptionTopic &subscriber) const noexcept { - const auto subscriberQuery = subscriber.params(); + bool testQueries(const mdp::Topic ¬ified, const mdp::Topic &subscriber) const noexcept { + const auto &subscriberQuery = subscriber.params(); if (subscriberQuery.empty()) { return true; } - const auto notificationQuery = notified.params(); + const auto ¬ificationQuery = notified.params(); - auto doesSatisfy = [this, ¬ificationQuery](const auto &subscriptionParam) { + auto doesSatisfy = [this, ¬ificationQuery](const auto &subscriptionParam) { const auto &key = subscriptionParam.first; const auto &value = subscriptionParam.second; @@ -51,7 +47,7 @@ class SubscriptionMatcher { assert(filterIt->second); const auto notifyIt = notificationQuery.find(key); - if (notifyIt == notificationQuery.end() && value) { + if (notifyIt == notificationQuery.end()) { // specific/required subscription topic but not corresponding filter in notification set return false; } @@ -61,25 +57,6 @@ class SubscriptionMatcher { return std::all_of(subscriberQuery.begin(), subscriberQuery.end(), doesSatisfy); } - - bool testPathOnly(const std::string_view ¬ified, const std::string_view &subscriber) const { - if (subscriber.empty() || std::all_of(subscriber.begin(), subscriber.end(), [](char c) { return std::isblank(c); })) { - return true; - } - - if (subscriber.find('*') == std::string_view::npos) { - return notified == subscriber; - } - - auto pathSubscriberView = std::string_view(subscriber); - - if (pathSubscriberView.ends_with("*")) { - pathSubscriberView.remove_suffix(1); - } - - // match path (leading characters) only - assumes trailing asterisk - return notified.starts_with(pathSubscriberView); - } }; } // namespace opencmw::majordomo diff --git a/src/majordomo/include/majordomo/Worker.hpp b/src/majordomo/include/majordomo/Worker.hpp index 759fc430..c6a2ab74 100644 --- a/src/majordomo/include/majordomo/Worker.hpp +++ b/src/majordomo/include/majordomo/Worker.hpp @@ -2,9 +2,7 @@ #define OPENCMW_MAJORDOMO_WORKER_H #include #include - #include -#include #include #include #include @@ -22,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -60,8 +59,6 @@ using description = worker_detail::description_impl class BasicWorker { - static_assert(!serviceName.empty()); - using Clock = std::chrono::steady_clock; using Timestamp = std::chrono::time_point; using Description = opencmw::find_type; @@ -95,7 +92,7 @@ class BasicWorker { std::array _pollerItems; SubscriptionMatcher _subscriptionMatcher; mutable std::mutex _activeSubscriptionsLock; - std::unordered_set _activeSubscriptions; + std::unordered_set _activeSubscriptions; zmq::Socket _notifyListenerSocket; std::unordered_map _notificationHandlers; std::shared_mutex _notificationHandlersLock; @@ -104,6 +101,7 @@ class BasicWorker { public: static constexpr std::string_view name = serviceName.data(); + static_assert(mdp::isValidServiceName(name)); explicit BasicWorker(opencmw::URI brokerAddress, std::function handler, const zmq::Context &context, Settings settings = {}) : _handler{ std::move(handler) }, _settings{ std::move(settings) }, _brokerAddress{ std::move(brokerAddress) }, _context(context), _notifyListenerSocket(_context, ZMQ_PULL), _notifyAddress(makeNotifyAddress()) { @@ -128,9 +126,9 @@ class BasicWorker { _subscriptionMatcher.addFilter(key); } - std::unordered_set activeSubscriptions() const noexcept { - std::lock_guard lockGuard(_activeSubscriptionsLock); - std::unordered_set copy = _activeSubscriptions; + std::unordered_set activeSubscriptions() const noexcept { + std::lock_guard lockGuard(_activeSubscriptionsLock); + std::unordered_set copy = _activeSubscriptions; return copy; } @@ -193,7 +191,7 @@ class BasicWorker { private: std::string makeNotifyAddress() const noexcept { - return fmt::format("inproc://workers/{}-{}/notify", serviceName.data(), worker_detail::nextWorkerId()); + return fmt::format("inproc://workers{}-{}/notify", name, worker_detail::nextWorkerId()); } NotificationHandler ¬ificationHandlerForThisThread() { @@ -235,7 +233,7 @@ class BasicWorker { reply.command = mdp::Command::Final; reply.serviceName = request.serviceName; // serviceName == clientSourceId reply.clientRequestID = request.clientRequestID; - reply.endpoint = request.endpoint; + reply.topic = request.topic; reply.rbac = request.rbac; return reply; } @@ -255,18 +253,25 @@ class BasicWorker { return true; } - const auto topicString = data.substr(1); - const auto topicUrl = URI(std::string(topicString)); - const auto subscription = mdp::SubscriptionTopic::fromURIAndServiceName(topicUrl, serviceName.data()); + const auto topicString = data.substr(1); + std::optional subscription; + try { + subscription = mdp::Topic::fromZmqTopic(topicString); + } catch (...) { + return false; + } + if (subscription->service() != name) { + return true; + } // this assumes that the broker does the subscribe/unsubscribe counting // for multiple clients and sends us a single sub/unsub for each topic if (data[0] == '\x1') { std::lock_guard lockGuard(_activeSubscriptionsLock); - _activeSubscriptions.insert(subscription); + _activeSubscriptions.insert(*subscription); } else { std::lock_guard lockGuard(_activeSubscriptionsLock); - _activeSubscriptions.erase(subscription); + _activeSubscriptions.erase(*subscription); } return true; @@ -274,7 +279,7 @@ class BasicWorker { bool receiveNotificationMessage() { if (auto message = zmq::receive(_notifyListenerSocket)) { - const auto currentSubscription = mdp::SubscriptionTopic::fromURIAndServiceName(message->endpoint, message->serviceName); + const auto currentSubscription = mdp::Topic::fromMdpTopic(message->topic); const auto matchesNotificationTopic = [this, ¤tSubscription](const auto &activeSubscription) { return _subscriptionMatcher(currentSubscription, activeSubscription); @@ -452,13 +457,11 @@ inline void serialiseAndWriteToBody(RequestContext &rawCtx, const ReflectableCla } inline void writeResult(std::string_view workerName, RequestContext &rawCtx, const auto &replyContext, const auto &output) { - auto replyQuery = query::serialise(replyContext); - const auto baseUri = rawCtx.reply.endpoint.empty() ? rawCtx.request.endpoint : rawCtx.reply.endpoint; - const auto topicUriOld = mdp::Message::URI::factory(baseUri).setQuery(std::move(replyQuery)).build(); - const auto topicUriNew = mdp::Message::URI::factory(baseUri).build(); - const auto &topicUri = topicUriOld; + auto replyQuery = query::serialise(replyContext); + const auto baseUri = rawCtx.reply.topic.empty() ? rawCtx.request.topic : rawCtx.reply.topic; + const auto topicUri = mdp::Message::URI::factory(baseUri).setQuery(std::move(replyQuery)).build(); - rawCtx.reply.endpoint = topicUri; + rawCtx.reply.topic = topicUri; const auto replyMimetype = query::getMimeType(replyContext); const auto mimeType = replyMimetype != MIME::UNKNOWN ? replyMimetype : rawCtx.mimeType; if (mimeType == MIME::JSON) { @@ -487,10 +490,10 @@ inline void writeResult(std::string_view workerName, RequestContext &rawCtx, con inline void writeResultFull(std::string_view workerName, RequestContext &rawCtx, const auto &requestContext, const auto &replyContext, const auto &input, const auto &output) { auto replyQuery = query::serialise(replyContext); - const auto baseUri = rawCtx.reply.endpoint.empty() ? rawCtx.request.endpoint : rawCtx.reply.endpoint; + const auto baseUri = rawCtx.reply.topic.empty() ? rawCtx.request.topic : rawCtx.reply.topic; const auto topicUri = mdp::Message::URI::factory(baseUri).setQuery(std::move(replyQuery)).build(); - rawCtx.reply.endpoint = topicUri; + rawCtx.reply.topic = topicUri; const auto replyMimetype = query::getMimeType(replyContext); const auto mimeType = replyMimetype != MIME::UNKNOWN ? replyMimetype : rawCtx.mimeType; if (mimeType == MIME::JSON) { @@ -540,7 +543,7 @@ struct HandlerImpl { } void operator()(RequestContext &rawCtx) { - const auto reqTopic = rawCtx.request.endpoint; + const auto reqTopic = rawCtx.request.topic; const auto queryMap = reqTopic.queryParamMap(); ContextType requestCtx = query::deserialise(queryMap); @@ -582,12 +585,8 @@ class Worker : public BasicWorker { } bool notify(const ContextType &context, const OutputType &reply) { - return notify("/", context, reply); - } - - bool notify(std::string_view path, const ContextType &context, const OutputType &reply) { RequestContext rawCtx; - rawCtx.reply.endpoint = mdp::Message::URI(std::string(path)); + rawCtx.reply.topic = URI<>(std::string(Worker::name)); worker_detail::writeResult(Worker::name, rawCtx, context, reply); return BasicWorker::notify(std::move(rawCtx.reply)); } diff --git a/src/majordomo/test/majordomo_benchmark.cpp b/src/majordomo/test/majordomo_benchmark.cpp index 6dcc7dc4..383a16da 100644 --- a/src/majordomo/test/majordomo_benchmark.cpp +++ b/src/majordomo/test/majordomo_benchmark.cpp @@ -85,16 +85,16 @@ struct Result { }; Result simpleOneWorkerBenchmark(const URI<> &routerAddress, Get mode, int iterations, std::size_t payloadSize) { - auto broker = Broker("benchmarkbroker", benchmarkSettings()); + auto broker = Broker("/benchmarkbroker", benchmarkSettings()); REQUIRE(broker.bind(routerAddress, BindOption::Router)); - BasicWorker<"blob"> worker(broker, PayloadHandler(std::string(payloadSize, '\xab'))); + BasicWorker<"/blob"> worker(broker, PayloadHandler(std::string(payloadSize, '\xab'))); - Context clientContext; - TestClient client(routerAddress.scheme() == opencmw::majordomo::SCHEME_INPROC ? broker.context : clientContext); + Context clientContext; + TestClient client(routerAddress.scheme() == opencmw::majordomo::SCHEME_INPROC ? broker.context : clientContext); - RunInThread brokerRun(broker); - RunInThread workerRun(worker); + RunInThread brokerRun(broker); + RunInThread workerRun(worker); REQUIRE(client.connect(routerAddress)); @@ -130,18 +130,18 @@ Result simpleOneWorkerBenchmark(const URI<> &routerAddress, Get mode, int iterat } void simpleTwoWorkerBenchmark(const URI<> &routerAddress, Get mode, int iterations, std::size_t payload1_size, std::size_t payload2_size) { - Broker broker("benchmarkbroker", benchmarkSettings()); + Broker broker("/benchmarkbroker", benchmarkSettings()); REQUIRE(broker.bind(routerAddress, BindOption::Router)); - RunInThread brokerRun(broker); + RunInThread brokerRun(broker); - BasicWorker<"blob"> worker1(broker, PayloadHandler(std::string(payload1_size, '\xab'))); - RunInThread worker1_run(worker1); + BasicWorker<"/blob"> worker1(broker, PayloadHandler(std::string(payload1_size, '\xab'))); + RunInThread worker1_run(worker1); - BasicWorker<"blob"> worker2(broker, PayloadHandler(std::string(payload2_size, '\xab'))); - RunInThread worker2_run(worker2); + BasicWorker<"/blob"> worker2(broker, PayloadHandler(std::string(payload2_size, '\xab'))); + RunInThread worker2_run(worker2); - Context clientContext; - TestClient client(routerAddress.scheme() == opencmw::majordomo::SCHEME_INPROC ? broker.context : clientContext); + Context clientContext; + TestClient client(routerAddress.scheme() == opencmw::majordomo::SCHEME_INPROC ? broker.context : clientContext); REQUIRE(client.connect(routerAddress)); const auto before = std::chrono::system_clock::now(); diff --git a/src/majordomo/test/majordomo_tests.cpp b/src/majordomo/test/majordomo_tests.cpp index 59c5e744..f3f26563 100644 --- a/src/majordomo/test/majordomo_tests.cpp +++ b/src/majordomo/test/majordomo_tests.cpp @@ -42,10 +42,10 @@ TEST_CASE("Test mmi.dns", "[broker][mmi][mmi_dns]") { const auto dnsAddress = opencmw::URI("mdp://127.0.0.1:22345"); const auto brokerAddress = opencmw::URI("mdp://127.0.0.1:22346"); - Broker dnsBroker("dnsBroker", settings); + Broker dnsBroker("/dnsBroker", settings); REQUIRE(dnsBroker.bind(dnsAddress)); settings.dnsAddress = dnsAddress.str(); - Broker broker("testbroker", settings); + Broker broker("/testbroker", settings); REQUIRE(broker.bind(brokerAddress)); // Add another address for DNS (REST interface) @@ -76,7 +76,7 @@ TEST_CASE("Test mmi.dns", "[broker][mmi][mmi_dns]") { REQUIRE(client.connect(brokerAddress)); auto request = createClientMessage(mdp::Command::Set); - request.serviceName = "mmi.dns"; + request.serviceName = "/mmi.dns"; request.data = IoBuffer("Hello World!"); client.send(std::move(request)); @@ -85,8 +85,8 @@ TEST_CASE("Test mmi.dns", "[broker][mmi][mmi_dns]") { REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "mmi.dns"); - REQUIRE(reply->data.asString() == "[testbroker: https://127.0.0.1:8080,https://127.0.0.1:8080/aDevice/aProperty,https://127.0.0.1:8080/mmi.dns," + REQUIRE(reply->serviceName == "/mmi.dns"); + REQUIRE(reply->data.asString() == "[/testbroker: https://127.0.0.1:8080,https://127.0.0.1:8080/aDevice/aProperty,https://127.0.0.1:8080/mmi.dns," "https://127.0.0.1:8080/mmi.echo,https://127.0.0.1:8080/mmi.openapi,https://127.0.0.1:8080/mmi.service," "mdp://127.0.0.1:22346,mdp://127.0.0.1:22346/aDevice/aProperty,mdp://127.0.0.1:22346/mmi.dns," "mdp://127.0.0.1:22346/mmi.echo,mdp://127.0.0.1:22346/mmi.openapi,mdp://127.0.0.1:22346/mmi.service]"); @@ -98,7 +98,7 @@ TEST_CASE("Test mmi.dns", "[broker][mmi][mmi_dns]") { REQUIRE(client.connect(dnsAddress)); auto request = createClientMessage(mdp::Command::Set); - request.serviceName = "mmi.dns"; + request.serviceName = "/mmi.dns"; request.data = IoBuffer("Hello World!"); client.send(std::move(request)); @@ -107,10 +107,10 @@ TEST_CASE("Test mmi.dns", "[broker][mmi][mmi_dns]") { REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "mmi.dns"); - REQUIRE(reply->data.asString() == "[dnsBroker: mdp://127.0.0.1:22345,mdp://127.0.0.1:22345/mmi.dns,mdp://127.0.0.1:22345/mmi.echo," + REQUIRE(reply->serviceName == "/mmi.dns"); + REQUIRE(reply->data.asString() == "[/dnsBroker: mdp://127.0.0.1:22345,mdp://127.0.0.1:22345/mmi.dns,mdp://127.0.0.1:22345/mmi.echo," "mdp://127.0.0.1:22345/mmi.openapi,mdp://127.0.0.1:22345/mmi.service]," - "[testbroker: https://127.0.0.1:8080,https://127.0.0.1:8080/aDevice/aProperty,https://127.0.0.1:8080/mmi.dns," + "[/testbroker: https://127.0.0.1:8080,https://127.0.0.1:8080/aDevice/aProperty,https://127.0.0.1:8080/mmi.dns," "https://127.0.0.1:8080/mmi.echo,https://127.0.0.1:8080/mmi.openapi,https://127.0.0.1:8080/mmi.service," "mdp://127.0.0.1:22346,mdp://127.0.0.1:22346/aDevice/aProperty,mdp://127.0.0.1:22346/mmi.dns," "mdp://127.0.0.1:22346/mmi.echo,mdp://127.0.0.1:22346/mmi.openapi,mdp://127.0.0.1:22346/mmi.service]"); @@ -122,9 +122,8 @@ TEST_CASE("Test mmi.dns", "[broker][mmi][mmi_dns]") { REQUIRE(client.connect(dnsAddress)); auto request = createClientMessage(mdp::Command::Set); - request.serviceName = "mmi.dns"; + request.serviceName = "/mmi.dns"; - // atm services must be prepended by "/" to form URIs that opencmw::URI can parse // send query with some crazy whitespace request.data = IoBuffer(" /mmi.dns , /aDevice/aProperty"); client.send(std::move(request)); @@ -134,7 +133,7 @@ TEST_CASE("Test mmi.dns", "[broker][mmi][mmi_dns]") { REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "mmi.dns"); + REQUIRE(reply->serviceName == "/mmi.dns"); REQUIRE(reply->data.asString() == "[/mmi.dns: https://127.0.0.1:8080/mmi.dns,mdp://127.0.0.1:22345/mmi.dns,mdp://127.0.0.1:22346/mmi.dns],[/aDevice/aProperty: https://127.0.0.1:8080/aDevice/aProperty,mdp://127.0.0.1:22346/aDevice/aProperty]"); } } @@ -142,7 +141,7 @@ TEST_CASE("Test mmi.dns", "[broker][mmi][mmi_dns]") { TEST_CASE("Test mmi.service", "[broker][mmi][mmi_service]") { using opencmw::majordomo::Broker; - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); RunInThread brokerRun(broker); MessageNode client(broker.context); @@ -150,8 +149,8 @@ TEST_CASE("Test mmi.service", "[broker][mmi][mmi_service]") { { // ask for not yet existing service auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "mmi.service"; - request.data = IoBuffer("a.service"); + request.serviceName = "/mmi.service"; + request.data = IoBuffer("/a.service"); client.send(std::move(request)); const auto reply = client.tryReadOne(); @@ -159,7 +158,7 @@ TEST_CASE("Test mmi.service", "[broker][mmi][mmi_service]") { REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "mmi.service"); + REQUIRE(reply->serviceName == "/mmi.service"); REQUIRE(reply->data.asString() == "404"); } @@ -168,17 +167,17 @@ TEST_CASE("Test mmi.service", "[broker][mmi][mmi_service]") { REQUIRE(worker.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); auto ready = createWorkerMessage(mdp::Command::Ready); - ready.serviceName = "a.service"; + ready.serviceName = "/a.service"; ready.data = IoBuffer("API description"); ready.rbac = IoBuffer("rbacToken"); worker.send(std::move(ready)); - REQUIRE(waitUntilServiceAvailable(broker.context, "a.service")); + REQUIRE(waitUntilServiceAvailable(broker.context, "/a.service")); { // service now exists auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "mmi.service"; - request.data = IoBuffer("a.service"); + request.serviceName = "/mmi.service"; + request.data = IoBuffer("/a.service"); client.send(std::move(request)); const auto reply = client.tryReadOne(); @@ -186,35 +185,35 @@ TEST_CASE("Test mmi.service", "[broker][mmi][mmi_service]") { REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "mmi.service"); + REQUIRE(reply->serviceName == "/mmi.service"); REQUIRE(reply->data.asString() == "200"); } { // list services auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "mmi.service"; + request.serviceName = "/mmi.service"; client.send(std::move(request)); const auto reply = client.tryReadOne(); REQUIRE(reply.has_value()); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "mmi.service"); - REQUIRE(reply->data.asString() == "a.service,mmi.dns,mmi.echo,mmi.openapi,mmi.service"); + REQUIRE(reply->serviceName == "/mmi.service"); + REQUIRE(reply->data.asString() == "/a.service,/mmi.dns,/mmi.echo,/mmi.openapi,/mmi.service"); } } TEST_CASE("Test mmi.echo", "[broker][mmi][mmi_echo]") { using opencmw::majordomo::Broker; - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); RunInThread brokerRun(broker); MessageNode client(broker.context); REQUIRE(client.connect(INTERNAL_ADDRESS_BROKER)); auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "mmi.echo"; + request.serviceName = "/mmi.echo"; request.data = IoBuffer("Wie heisst der Buergermeister von Wesel"); request.rbac = IoBuffer("rbac"); @@ -227,7 +226,7 @@ TEST_CASE("Test mmi.echo", "[broker][mmi][mmi_echo]") { REQUIRE(reply->command == mdp::Command::Get); REQUIRE(reply->serviceName == request.serviceName); REQUIRE(reply->clientRequestID.asString() == request.clientRequestID.asString()); - REQUIRE(reply->endpoint == request.endpoint); + REQUIRE(reply->topic == request.topic); REQUIRE(reply->data.asString() == request.data.asString()); REQUIRE(reply->error == request.error); REQUIRE(reply->rbac.asString() == request.rbac.asString()); @@ -236,7 +235,7 @@ TEST_CASE("Test mmi.echo", "[broker][mmi][mmi_echo]") { TEST_CASE("Test mmi.openapi", "[broker][mmi][mmi_openapi]") { using opencmw::majordomo::Broker; - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); RunInThread brokerRun(broker); MessageNode client(broker.context); @@ -244,8 +243,8 @@ TEST_CASE("Test mmi.openapi", "[broker][mmi][mmi_openapi]") { { // request API of not yet existing service auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "mmi.openapi"; - request.data = IoBuffer("a.service"); + request.serviceName = "/mmi.openapi"; + request.data = IoBuffer("/a.service"); client.send(std::move(request)); const auto reply = client.tryReadOne(); @@ -253,9 +252,9 @@ TEST_CASE("Test mmi.openapi", "[broker][mmi][mmi_openapi]") { REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "mmi.openapi"); + REQUIRE(reply->serviceName == "/mmi.openapi"); REQUIRE(reply->data.asString() == ""); - REQUIRE(reply->error == "Requested invalid service 'a.service'"); + REQUIRE(reply->error == "Requested invalid service '/a.service'"); } // register worker as a.service @@ -263,17 +262,17 @@ TEST_CASE("Test mmi.openapi", "[broker][mmi][mmi_openapi]") { REQUIRE(worker.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); auto ready = createWorkerMessage(mdp::Command::Ready); - ready.serviceName = "a.service"; + ready.serviceName = "/a.service"; ready.data = IoBuffer("API description"); ready.rbac = IoBuffer("rbacToken"); worker.send(std::move(ready)); - REQUIRE(waitUntilServiceAvailable(broker.context, "a.service")); + REQUIRE(waitUntilServiceAvailable(broker.context, "/a.service")); { // service now exists, API description is returned auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "mmi.openapi"; - request.data = IoBuffer("a.service"); + request.serviceName = "/mmi.openapi"; + request.data = IoBuffer("/a.service"); client.send(std::move(request)); const auto reply = client.tryReadOne(); @@ -281,7 +280,7 @@ TEST_CASE("Test mmi.openapi", "[broker][mmi][mmi_openapi]") { REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "mmi.openapi"); + REQUIRE(reply->serviceName == "/mmi.openapi"); REQUIRE(reply->data.asString() == "API description"); REQUIRE(reply->error == ""); } @@ -292,7 +291,7 @@ TEST_CASE("Request answered with unknown service", "[broker][unknown_service]") const auto address = URI<>("inproc://testrouter"); - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); REQUIRE(broker.bind(address, BindOption::Router)); @@ -302,9 +301,9 @@ TEST_CASE("Request answered with unknown service", "[broker][unknown_service]") RunInThread brokerRun(broker); auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "no.service"; + request.serviceName = "/no.service"; request.clientRequestID = IoBuffer("1"); - request.endpoint = mdp::Message::URI("/topic"); + request.topic = mdp::Message::URI("/topic"); request.rbac = IoBuffer("rbacToken"); client.send(std::move(request)); @@ -313,11 +312,11 @@ TEST_CASE("Request answered with unknown service", "[broker][unknown_service]") REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "no.service"); + REQUIRE(reply->serviceName == "/no.service"); REQUIRE(reply->clientRequestID.asString() == "1"); - REQUIRE(reply->endpoint.str() == "/mmi.service"); + REQUIRE(reply->topic.str() == "/mmi.service"); REQUIRE(reply->data.empty()); - REQUIRE(reply->error == "unknown service (error 501): 'no.service'"); + REQUIRE(reply->error == "unknown service (error 501): '/no.service'"); REQUIRE(reply->rbac.asString() == "RBAC=ADMIN,abcdef12345"); } @@ -327,6 +326,53 @@ TEST_CASE("Test toZeroMQEndpoint conversion", "[utils][toZeroMQEndpoint]") { REQUIRE(mdp::toZeroMQEndpoint(URI<>("inproc://test")) == "inproc://test"); } +TEST_CASE("Test Topic class", "[mdp][topic]") { + using mdp::Topic; + using Params = Topic::Params; + SECTION("Topic::fromString") { + REQUIRE_THROWS_AS(Topic::fromString("/a/service?p1=foo", { { "p2", "foo" } }), std::invalid_argument); + REQUIRE_THROWS_AS(Topic::fromString("/invalid%20service?p1=foo", {}), std::invalid_argument); + REQUIRE_THROWS_AS(Topic::fromString("g!a@r#b$a%g^e", {}), std::invalid_argument); + REQUIRE_THROWS_AS(Topic::fromString({}, {}), std::invalid_argument); + REQUIRE_THROWS_AS(Topic::fromString({}, { { "p1", "foo" } }), std::invalid_argument); + const auto t1 = Topic::fromString("/a/service?p1=foo", {}); + const auto t2 = Topic::fromString("/a/service", { { "p1", "foo" } }); + REQUIRE(t1 == t2); + REQUIRE(t1.service() == "/a/service"); + REQUIRE(t1.params() == Params{ { "p1", "foo" } }); + } + + SECTION("serialization from/to ZMQ and MDP topic with params") { + const auto t1 = Topic::fromString("/a/service?p1=foo&p2=bar", {}); + const auto t2 = Topic::fromString("/a/service", { { "p1", "foo" }, { "p2", "bar" } }); + const auto t3 = Topic::fromZmqTopic("/a/service?p1=foo&p2=bar"); + const auto t4 = Topic::fromMdpTopic(URI<>("/a/service?p2=bar&p1=foo")); + REQUIRE_THROWS_AS(Topic::fromMdpTopic(URI<>("")), std::invalid_argument); + REQUIRE(t1 == t2); + REQUIRE(t1 == t3); + REQUIRE(t1 == t4); + REQUIRE(t1.toZmqTopic() == "/a/service?p1=foo&p2=bar"); + REQUIRE(t1.toZmqTopic() == t2.toZmqTopic()); + REQUIRE(t1.toZmqTopic() == t3.toZmqTopic()); + REQUIRE(t1.toZmqTopic() == t4.toZmqTopic()); + REQUIRE(t1.toMdpTopic().path() == t2.toMdpTopic().path()); + REQUIRE(t1.toMdpTopic().queryParamMap() == t2.toMdpTopic().queryParamMap()); + } + + SECTION("serialization from/to ZMQ and MDP topic without params") { + const auto t1 = Topic::fromString("/a/service", {}); + const auto t2 = Topic::fromZmqTopic("/a/service"); + const auto t3 = Topic::fromMdpTopic(URI<>("/a/service")); + REQUIRE(t1 == t2); + REQUIRE(t1 == t3); + REQUIRE(t1.toZmqTopic() == "/a/service"); + REQUIRE(t1.toZmqTopic() == t2.toZmqTopic()); + REQUIRE(t1.toZmqTopic() == t3.toZmqTopic()); + REQUIRE(t1.toMdpTopic().path().value_or("") == "/a/service"); + REQUIRE(t1.toMdpTopic().queryParamMap().empty()); + } +} + TEST_CASE("Bind broker to endpoints", "[broker][bind]") { // the tcp/mdp/mds test cases rely on the ports being free, use wildcards/search for free ports if this turns out to be a problem static const std::array testcases = { @@ -341,7 +387,7 @@ TEST_CASE("Bind broker to endpoints", "[broker][bind]") { std::tuple{ URI<>("inproc://bindtest_pub"), BindOption::Pub, std::make_optional>("inproc://bindtest_pub") }, }; - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); for (const auto &testcase : testcases) { const auto endpoint = std::get<0>(testcase); @@ -354,7 +400,7 @@ TEST_CASE("Bind broker to endpoints", "[broker][bind]") { TEST_CASE("One client/one worker roundtrip", "[broker][roundtrip]") { using opencmw::majordomo::Broker; - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); MessageNode worker(broker.context); REQUIRE(worker.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); @@ -363,7 +409,7 @@ TEST_CASE("One client/one worker roundtrip", "[broker][roundtrip]") { REQUIRE(client.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); auto ready = createWorkerMessage(mdp::Command::Ready); - ready.serviceName = "a.service"; + ready.serviceName = "/a.service"; ready.data = IoBuffer("API description"); ready.rbac = IoBuffer("rbacToken"); worker.send(std::move(ready)); @@ -371,9 +417,9 @@ TEST_CASE("One client/one worker roundtrip", "[broker][roundtrip]") { broker.processMessages(); auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "a.service"; + request.serviceName = "/a.service"; request.clientRequestID = IoBuffer("1"); - request.endpoint = mdp::Message::URI("/topic"); + request.topic = mdp::Message::URI("/a.service?what=topic"); request.rbac = IoBuffer("rbacToken"); client.send(std::move(request)); @@ -385,7 +431,7 @@ TEST_CASE("One client/one worker roundtrip", "[broker][roundtrip]") { REQUIRE(requestAtWorker->command == mdp::Command::Get); REQUIRE(!requestAtWorker->serviceName.empty()); // clientSourceID REQUIRE(requestAtWorker->clientRequestID.asString() == "1"); - REQUIRE(requestAtWorker->endpoint.str() == "/topic"); + REQUIRE(requestAtWorker->topic.str() == "/a.service?what=topic"); REQUIRE(requestAtWorker->data.empty()); REQUIRE(requestAtWorker->error.empty()); REQUIRE(requestAtWorker->rbac.asString() == "rbacToken"); @@ -393,7 +439,7 @@ TEST_CASE("One client/one worker roundtrip", "[broker][roundtrip]") { auto replyFromWorker = createWorkerMessage(mdp::Command::Final); replyFromWorker.serviceName = requestAtWorker->serviceName; // clientSourceID replyFromWorker.clientRequestID = IoBuffer("1"); - replyFromWorker.endpoint = mdp::Message::URI("/topic"); + replyFromWorker.topic = mdp::Message::URI("/a.service?what=topic"); replyFromWorker.data = IoBuffer("reply body"); replyFromWorker.rbac = IoBuffer("rbac_worker"); worker.send(std::move(replyFromWorker)); @@ -404,9 +450,9 @@ TEST_CASE("One client/one worker roundtrip", "[broker][roundtrip]") { REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "a.service"); + REQUIRE(reply->serviceName == "/a.service"); REQUIRE(reply->clientRequestID.asString() == "1"); - REQUIRE(reply->endpoint.str() == "/topic"); + REQUIRE(reply->topic.str() == "/a.service?what=topic"); REQUIRE(reply->data.asString() == "reply body"); REQUIRE(reply->error.empty()); REQUIRE(reply->rbac.asString() == "rbac_worker"); @@ -418,7 +464,7 @@ TEST_CASE("One client/one worker roundtrip", "[broker][roundtrip]") { REQUIRE(heartbeat.has_value()); REQUIRE(heartbeat->protocolName == mdp::workerProtocol); REQUIRE(heartbeat->command == mdp::Command::Heartbeat); - REQUIRE(heartbeat->serviceName == "a.service"); + REQUIRE(heartbeat->serviceName == "/a.service"); REQUIRE(heartbeat->rbac.asString() == "RBAC=ADMIN,abcdef12345"); } @@ -426,9 +472,8 @@ TEST_CASE("One client/one worker roundtrip", "[broker][roundtrip]") { REQUIRE(disconnect.has_value()); REQUIRE(disconnect->protocolName == mdp::workerProtocol); REQUIRE(disconnect->command == mdp::Command::Disconnect); - REQUIRE(disconnect->serviceName == "a.service"); + REQUIRE(disconnect->serviceName == "/a.service"); REQUIRE(disconnect->clientRequestID.empty()); - REQUIRE(disconnect->endpoint.str() == "/a.service"); REQUIRE(disconnect->data.asString() == "broker shutdown"); REQUIRE(disconnect->error.empty()); REQUIRE(disconnect->rbac.asString() == "RBAC=ADMIN,abcdef12345"); @@ -437,7 +482,7 @@ TEST_CASE("One client/one worker roundtrip", "[broker][roundtrip]") { TEST_CASE("Test service matching", "[broker][name-matcher]") { using opencmw::majordomo::Broker; - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); MessageNode worker(broker.context); REQUIRE(worker.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); @@ -446,8 +491,8 @@ TEST_CASE("Test service matching", "[broker][name-matcher]") { REQUIRE(client.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); auto ready = createWorkerMessage(mdp::Command::Ready); - ready.serviceName = "/DeviceA/dashboard"; - ready.data = IoBuffer("An example worker serving different dashbards"); + ready.serviceName = "/dashboard"; + ready.data = IoBuffer("An example worker serving different dashboards"); ready.rbac = IoBuffer("rbacToken"); worker.send(std::move(ready)); @@ -455,9 +500,9 @@ TEST_CASE("Test service matching", "[broker][name-matcher]") { { auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "/DeviceA/dashboard"; + request.serviceName = "/dashboard"; request.clientRequestID = IoBuffer("1"); - request.endpoint = mdp::Message::URI("/DeviceA/dashboard"); + request.topic = mdp::Message::URI("/dashboard"); request.rbac = IoBuffer("rbacToken"); client.send(std::move(request)); @@ -469,7 +514,7 @@ TEST_CASE("Test service matching", "[broker][name-matcher]") { REQUIRE(requestAtWorker->command == mdp::Command::Get); REQUIRE(!requestAtWorker->serviceName.empty()); // clientSourceID REQUIRE(requestAtWorker->clientRequestID.asString() == "1"); - REQUIRE(requestAtWorker->endpoint.str() == "/DeviceA/dashboard"); + REQUIRE(requestAtWorker->topic.str() == "/dashboard"); REQUIRE(requestAtWorker->data.empty()); REQUIRE(requestAtWorker->error.empty()); REQUIRE(requestAtWorker->rbac.asString() == "rbacToken"); @@ -477,7 +522,7 @@ TEST_CASE("Test service matching", "[broker][name-matcher]") { auto replyFromWorker = createWorkerMessage(mdp::Command::Final); replyFromWorker.serviceName = requestAtWorker->serviceName; // clientSourceID replyFromWorker.clientRequestID = IoBuffer("1"); - replyFromWorker.endpoint = mdp::Message::URI("/DeviceA/dashboard/default"); + replyFromWorker.topic = mdp::Message::URI("/dashboard/default"); replyFromWorker.data = IoBuffer("Testreply"); replyFromWorker.rbac = IoBuffer("rbac_worker"); worker.send(std::move(replyFromWorker)); @@ -488,9 +533,9 @@ TEST_CASE("Test service matching", "[broker][name-matcher]") { REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "/DeviceA/dashboard"); + REQUIRE(reply->serviceName == "/dashboard"); REQUIRE(reply->clientRequestID.asString() == "1"); - REQUIRE(reply->endpoint.str() == "/DeviceA/dashboard/default"); + REQUIRE(reply->topic.str() == "/dashboard/default"); REQUIRE(reply->data.asString() == "Testreply"); REQUIRE(reply->error.empty()); REQUIRE(reply->rbac.asString() == "rbac_worker"); @@ -498,9 +543,9 @@ TEST_CASE("Test service matching", "[broker][name-matcher]") { { auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "/DeviceA/dashboard/main"; + request.serviceName = "dashboard/main"; request.clientRequestID = IoBuffer("2"); - request.endpoint = mdp::Message::URI("/DeviceA/dashboard/main?revision=12"); + request.topic = mdp::Message::URI("/dashboard/main?revision=12"); request.rbac = IoBuffer("rbacToken"); client.send(std::move(request)); @@ -512,7 +557,7 @@ TEST_CASE("Test service matching", "[broker][name-matcher]") { REQUIRE(requestAtWorker->command == mdp::Command::Get); REQUIRE(!requestAtWorker->serviceName.empty()); // clientSourceID REQUIRE(requestAtWorker->clientRequestID.asString() == "2"); - REQUIRE(requestAtWorker->endpoint.str() == "/DeviceA/dashboard/main?revision=12"); + REQUIRE(requestAtWorker->topic.str() == "/dashboard/main?revision=12"); REQUIRE(requestAtWorker->data.empty()); REQUIRE(requestAtWorker->error.empty()); REQUIRE(requestAtWorker->rbac.asString() == "rbacToken"); @@ -520,7 +565,7 @@ TEST_CASE("Test service matching", "[broker][name-matcher]") { auto replyFromWorker = createWorkerMessage(mdp::Command::Final); replyFromWorker.serviceName = requestAtWorker->serviceName; // clientSourceID replyFromWorker.clientRequestID = IoBuffer("2"); - replyFromWorker.endpoint = mdp::Message::URI("/DeviceA/dashboard/main?revision=12"); + replyFromWorker.topic = mdp::Message::URI("/dashboard/main?revision=12"); replyFromWorker.data = IoBuffer("Testreply"); replyFromWorker.rbac = IoBuffer("rbac_worker"); worker.send(std::move(replyFromWorker)); @@ -531,9 +576,9 @@ TEST_CASE("Test service matching", "[broker][name-matcher]") { REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "/DeviceA/dashboard"); + REQUIRE(reply->serviceName == "/dashboard"); REQUIRE(reply->clientRequestID.asString() == "2"); - REQUIRE(reply->endpoint.str() == "/DeviceA/dashboard/main?revision=12"); + REQUIRE(reply->topic.str() == "/dashboard/main?revision=12"); REQUIRE(reply->data.asString() == "Testreply"); REQUIRE(reply->error.empty()); REQUIRE(reply->rbac.asString() == "rbac_worker"); @@ -546,9 +591,8 @@ TEST_CASE("Test service matching", "[broker][name-matcher]") { REQUIRE(disconnect.has_value()); REQUIRE(disconnect->protocolName == mdp::workerProtocol); REQUIRE(disconnect->command == mdp::Command::Disconnect); - REQUIRE(disconnect->serviceName == "/DeviceA/dashboard"); + REQUIRE(disconnect->serviceName == "/dashboard"); REQUIRE(disconnect->clientRequestID.empty()); - REQUIRE(disconnect->endpoint.str() == "//DeviceA/dashboard"); REQUIRE(disconnect->data.asString() == "broker shutdown"); REQUIRE(disconnect->error.empty()); REQUIRE(disconnect->rbac.asString() == "RBAC=ADMIN,abcdef12345"); @@ -560,75 +604,137 @@ TEST_CASE("Pubsub example using SUB client/DEALER worker", "[broker][pubsub_sub_ const auto publisherAddress = URI<>("inproc://testpub"); - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); + broker.addFilter>("topic"); REQUIRE(broker.bind(publisherAddress, BindOption::Pub)); BrokerMessageNode subscriber(broker.context, ZMQ_SUB); - REQUIRE(subscriber.connect(publisherAddress, mdp::SubscriptionTopic("/a.topic"))); - REQUIRE(subscriber.subscribe(mdp::SubscriptionTopic("/other.*"))); + REQUIRE(subscriber.connect(publisherAddress, "/a.service?topic=something")); + REQUIRE(subscriber.subscribe("/a.service?topic=other")); + REQUIRE(subscriber.subscribe("/a-service?topic=something")); // invalid, broker must ignore broker.processMessages(); - MessageNode publisher(broker.context); + MessageNode publisher(broker.context); // "/a.service" REQUIRE(publisher.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); - // send three notifications, two matching (one exact, one via wildcard), one not matching + MessageNode publisherInvalid(broker.context); // "/a-service" (invalid) + REQUIRE(publisherInvalid.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); + + // send invalid notification, broker must ignore { auto notify = createWorkerMessage(mdp::Command::Notify); - notify.serviceName = "a.service"; - notify.endpoint = mdp::Message::URI("/a.topic"); - notify.data = IoBuffer("Notification about /a.topic"); + notify.serviceName = "/a-service"; // invalid service name here and in topic + notify.topic = mdp::Message::URI("/a-service?topic=something"); + notify.data = IoBuffer("Notification about something"); + notify.rbac = IoBuffer("rbac_worker"); + publisherInvalid.send(std::move(notify)); + } + + broker.processMessages(); + + // notification matching subscription + { + auto notify = createWorkerMessage(mdp::Command::Notify); + notify.serviceName = "/a.service"; + notify.topic = mdp::Message::URI("/a.service?topic=something"); + notify.data = IoBuffer("Notification about something"); notify.rbac = IoBuffer("rbac_worker"); publisher.send(std::move(notify)); } broker.processMessages(); + // notification not matching subscription { auto notify = createWorkerMessage(mdp::Command::Notify); - notify.serviceName = "a.service"; - notify.endpoint = mdp::Message::URI("/a.topic_2"); - notify.data = IoBuffer("Notification about /a.topic_2"); + notify.serviceName = "/a.service"; + notify.topic = mdp::Message::URI("/a.service?topic=somethingelse"); + notify.data = IoBuffer("Notification about somethingelse"); notify.rbac = IoBuffer("rbac_worker"); publisher.send(std::move(notify)); } broker.processMessages(); + // notification matching subscription { auto notify = createWorkerMessage(mdp::Command::Notify); - notify.serviceName = "a.service"; - notify.endpoint = mdp::Message::URI("/other.topic"); - notify.data = IoBuffer("Notification about /other.topic"); + notify.serviceName = "/a.service"; + notify.topic = mdp::Message::URI("/a.service?topic=other"); + notify.data = IoBuffer("Notification about other"); notify.rbac = IoBuffer("rbac_worker"); publisher.send(std::move(notify)); } broker.processMessages(); - // receive only messages matching subscriptions + // receive the two messages matching subscriptions + { + const auto reply = subscriber.tryReadOne(); + REQUIRE(reply.has_value()); + REQUIRE(reply->protocolName == mdp::clientProtocol); + REQUIRE(reply->sourceId == "/a.service?topic=something"); + REQUIRE(reply->serviceName == "/a.service"); + REQUIRE(reply->topic.str() == "/a.service?topic=something"); + REQUIRE(reply->clientRequestID.empty()); + REQUIRE(reply->data.asString() == "Notification about something"); + REQUIRE(reply->error.empty()); + REQUIRE(reply->rbac.asString() == "rbac_worker"); + } { const auto reply = subscriber.tryReadOne(); REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); - REQUIRE(reply->sourceId == "/a.topic"); - REQUIRE(reply->serviceName == "a.service"); + REQUIRE(reply->sourceId == "/a.service?topic=other"); + REQUIRE(reply->serviceName == "/a.service"); REQUIRE(reply->clientRequestID.empty()); - REQUIRE(reply->data.asString() == "Notification about /a.topic"); + REQUIRE(reply->data.asString() == "Notification about other"); REQUIRE(reply->error.empty()); REQUIRE(reply->rbac.asString() == "rbac_worker"); } + REQUIRE(subscriber.unsubscribe("/a.service?topic=something")); + REQUIRE(subscriber.unsubscribe("/a%20service?topic=something")); // invalid, broker must ignore + + broker.processMessages(); + + // Still subscribed to "other" + + // not subscribed anymore + { + auto notify = createWorkerMessage(mdp::Command::Notify); + notify.serviceName = "/a.service"; + notify.topic = mdp::Message::URI("/a.service?topic=something"); + notify.data = IoBuffer("Notification about something"); + notify.rbac = IoBuffer("rbac_worker"); + publisher.send(std::move(notify)); + } + + broker.processMessages(); + + // notification still matching subscription + { + auto notify = createWorkerMessage(mdp::Command::Notify); + notify.serviceName = "/a.service"; + notify.topic = mdp::Message::URI("/a.service?topic=other"); + notify.data = IoBuffer("Notification about other"); + notify.rbac = IoBuffer("rbac_worker"); + publisher.send(std::move(notify)); + } + + broker.processMessages(); + { const auto reply = subscriber.tryReadOne(); REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); - REQUIRE(reply->sourceId == "/other.*"); - REQUIRE(reply->serviceName == "a.service"); + REQUIRE(reply->sourceId == "/a.service?topic=other"); + REQUIRE(reply->serviceName == "/a.service"); REQUIRE(reply->clientRequestID.empty()); - REQUIRE(reply->data.asString() == "Notification about /other.topic"); + REQUIRE(reply->data.asString() == "Notification about other"); REQUIRE(reply->error.empty()); REQUIRE(reply->rbac.asString() == "rbac_worker"); } @@ -643,7 +749,7 @@ TEST_CASE("Broker sends heartbeats", "[broker][heartbeat]") { Settings settings; settings.heartbeatInterval = heartbeatInterval; settings.heartbeatLiveness = 3; - Broker broker("testbroker", settings); + Broker broker("/testbroker", settings); MessageNode worker(broker.context); @@ -652,7 +758,7 @@ TEST_CASE("Broker sends heartbeats", "[broker][heartbeat]") { { auto ready = createWorkerMessage(mdp::Command::Ready); - ready.serviceName = "heartbeat.service"; + ready.serviceName = "/heartbeat.service"; ready.data = IoBuffer("API description"); ready.rbac = IoBuffer("rbac_worker"); worker.send(std::move(ready)); @@ -664,7 +770,7 @@ TEST_CASE("Broker sends heartbeats", "[broker][heartbeat]") { { auto heartbeat = createWorkerMessage(mdp::Command::Heartbeat); - heartbeat.serviceName = "heartbeat.service"; + heartbeat.serviceName = "/heartbeat.service"; heartbeat.rbac = IoBuffer("rbac_worker"); worker.send(std::move(heartbeat)); } @@ -702,7 +808,7 @@ TEST_CASE("Broker disconnects on unexpected heartbeat", "[broker][unexpected_hea Settings settings; settings.heartbeatInterval = heartbeatInterval; - Broker broker("testbroker", settings); + Broker broker("/testbroker", settings); MessageNode worker(broker.context); @@ -711,7 +817,7 @@ TEST_CASE("Broker disconnects on unexpected heartbeat", "[broker][unexpected_hea // send heartbeat without initial ready - invalid auto heartbeat = createWorkerMessage(mdp::Command::Heartbeat); - heartbeat.serviceName = "heartbeat.service"; + heartbeat.serviceName = "/heartbeat.service"; heartbeat.rbac = IoBuffer("rbac_worker"); worker.send(std::move(heartbeat)); @@ -729,7 +835,7 @@ TEST_CASE("Test RBAC role priority handling", "[broker][rbac]") { opencmw::majordomo::Settings settings; settings.heartbeatInterval = std::chrono::seconds(1); - Broker broker("testbroker", settings); + Broker broker("/testbroker", settings); RunInThread brokerRun(broker); MessageNode worker(broker.context); @@ -737,13 +843,13 @@ TEST_CASE("Test RBAC role priority handling", "[broker][rbac]") { { auto ready = createWorkerMessage(mdp::Command::Ready); - ready.serviceName = "a.service"; + ready.serviceName = "/a.service"; ready.data = IoBuffer("API description"); ready.rbac = IoBuffer("rbac_worker"); worker.send(std::move(ready)); } - REQUIRE(waitUntilServiceAvailable(broker.context, "a.service")); + REQUIRE(waitUntilServiceAvailable(broker.context, "/a.service")); MessageNode client(broker.context); REQUIRE(client.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); @@ -755,7 +861,7 @@ TEST_CASE("Test RBAC role priority handling", "[broker][rbac]") { auto msg = createClientMessage(mdp::Command::Get); const auto reqId = std::to_string(clientRequestId++); msg.clientRequestID = IoBuffer(reqId.data(), reqId.size()); - msg.serviceName = "a.service"; + msg.serviceName = "/a.service"; const auto rbac = fmt::format("RBAC={},123456abcdef", role); msg.rbac = IoBuffer(rbac.data(), rbac.size()); client.send(std::move(msg)); @@ -798,7 +904,9 @@ TEST_CASE("Test RBAC role priority handling", "[broker][rbac]") { TEST_CASE("pubsub example using router socket (DEALER client)", "[broker][pubsub_router]") { using opencmw::majordomo::Broker; - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); + broker.addFilter>("origin"); + broker.addFilter>("dish"); MessageNode subscriber(broker.context); REQUIRE(subscriber.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); @@ -809,33 +917,42 @@ TEST_CASE("pubsub example using router socket (DEALER client)", "[broker][pubsub MessageNode publisherTwo(broker.context); REQUIRE(publisherTwo.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); - // subscribe client to /cooking.italian + // subscribe client to Italian + { + auto subscribe = createClientMessage(mdp::Command::Subscribe); + subscribe.serviceName = "/first.service"; + subscribe.topic = mdp::Message::URI("/first.service?origin=italian"); + subscribe.rbac = IoBuffer("rbacToken"); + subscriber.send(std::move(subscribe)); + } + + // Send invalid subscription (invalid service name) { auto subscribe = createClientMessage(mdp::Command::Subscribe); - subscribe.serviceName = "first.service"; - subscribe.endpoint = mdp::Message::URI("/cooking.italian"); + subscribe.serviceName = "/s-e-r-v-i-c-e"; + subscribe.topic = mdp::Message::URI("/se-r-v-i-c-e?origin=italian"); subscribe.rbac = IoBuffer("rbacToken"); subscriber.send(std::move(subscribe)); } broker.processMessages(); - // subscribe client to /cooking.indian + // subscribe client to Indian { auto subscribe = createClientMessage(mdp::Command::Subscribe); - subscribe.serviceName = "second.service"; - subscribe.endpoint = mdp::Message::URI("/cooking.indian"); + subscribe.serviceName = "/second.service"; + subscribe.topic = mdp::Message::URI("/second.service?origin=indian"); subscribe.rbac = IoBuffer("rbacToken"); subscriber.send(std::move(subscribe)); } broker.processMessages(); - // publisher 1 sends a notification for /cooking.italian + // publisher 1 sends a notification for Italian { auto pubMsg = createWorkerMessage(mdp::Command::Notify); - pubMsg.serviceName = "first.service"; - pubMsg.endpoint = mdp::Message::URI("/cooking.italian"); + pubMsg.serviceName = "/first.service"; + pubMsg.topic = mdp::Message::URI("/first.service?origin=italian"); pubMsg.data = IoBuffer("Original carbonara recipe here!"); pubMsg.rbac = IoBuffer("rbac_worker_1"); publisherOne.send(std::move(pubMsg)); @@ -843,25 +960,25 @@ TEST_CASE("pubsub example using router socket (DEALER client)", "[broker][pubsub broker.processMessages(); - // client receives notification for /cooking.italian + // client receives notification for Italian { const auto reply = subscriber.tryReadOne(); REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "first.service"); + REQUIRE(reply->serviceName == "/first.service"); REQUIRE(reply->clientRequestID.empty()); - REQUIRE(reply->endpoint.str() == "/cooking.italian"); + REQUIRE(reply->topic == mdp::Message::URI("/first.service?origin=italian")); REQUIRE(reply->data.asString() == "Original carbonara recipe here!"); REQUIRE(reply->error.empty()); REQUIRE(reply->rbac.asString() == "rbac_worker_1"); } - // publisher 2 sends a notification for /cooking.indian + // publisher 2 sends a notification for Italian { auto pubMsg = createWorkerMessage(mdp::Command::Notify); - pubMsg.serviceName = "second.service"; - pubMsg.endpoint = mdp::Message::URI("/cooking.indian"); + pubMsg.serviceName = "/second.service"; + pubMsg.topic = mdp::Message::URI("/second.service?origin=indian"); pubMsg.data = IoBuffer("Try our Chicken Korma!"); pubMsg.rbac = IoBuffer("rbac_worker_2"); publisherTwo.send(std::move(pubMsg)); @@ -869,36 +986,36 @@ TEST_CASE("pubsub example using router socket (DEALER client)", "[broker][pubsub broker.processMessages(); - // client receives notification for /cooking.indian + // client receives notification for Indian { const auto reply = subscriber.tryReadOne(); REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "second.service"); + REQUIRE(reply->serviceName == "/second.service"); REQUIRE(reply->clientRequestID.empty()); - REQUIRE(reply->endpoint.str() == "/cooking.indian"); + REQUIRE(reply->topic == mdp::Message::URI("/second.service?origin=indian")); REQUIRE(reply->data.asString() == "Try our Chicken Korma!"); REQUIRE(reply->error.empty()); REQUIRE(reply->rbac.asString() == "rbac_worker_2"); } - // unsubscribe client from /cooking.italian + // unsubscribe client from Italian { auto unsubscribe = createClientMessage(mdp::Command::Unsubscribe); - unsubscribe.serviceName = "first.service"; - unsubscribe.endpoint = mdp::Message::URI("/cooking.italian"); + unsubscribe.serviceName = "/first.service"; + unsubscribe.topic = mdp::Message::URI("/first.service?origin=italian"); unsubscribe.rbac = IoBuffer("rbacToken"); subscriber.send(std::move(unsubscribe)); } broker.processMessages(); - // publisher 1 sends a notification for /cooking.italian + // publisher 1 sends a notification for Italian { auto pubMsg = createWorkerMessage(mdp::Command::Notify); - pubMsg.serviceName = "first.service"; - pubMsg.endpoint = mdp::Message::URI("/cooking.italian"); + pubMsg.serviceName = "/first.service"; + pubMsg.topic = mdp::Message::URI("/first.service?origin=italian"); pubMsg.data = IoBuffer("The best Margherita in town!"); pubMsg.rbac = IoBuffer("rbac_worker_1"); publisherOne.send(std::move(pubMsg)); @@ -906,11 +1023,11 @@ TEST_CASE("pubsub example using router socket (DEALER client)", "[broker][pubsub broker.processMessages(); - // publisher 2 sends a notification for /cooking.indian + // publisher 2 sends a notification for Indian { auto pubMsg = createWorkerMessage(mdp::Command::Notify); - pubMsg.serviceName = "second.service"; - pubMsg.endpoint = mdp::Message::URI("/cooking.indian"); + pubMsg.serviceName = "/second.service"; + pubMsg.topic = mdp::Message::URI("/second.service?origin=indian"); pubMsg.data = IoBuffer("Sizzling tikkas in our Restaurant!"); pubMsg.rbac = IoBuffer("rbac_worker_2"); publisherTwo.send(std::move(pubMsg)); @@ -926,9 +1043,9 @@ TEST_CASE("pubsub example using router socket (DEALER client)", "[broker][pubsub REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "second.service"); + REQUIRE(reply->serviceName == "/second.service"); REQUIRE(reply->clientRequestID.empty()); - REQUIRE(reply->endpoint.str() == "/cooking.indian"); + REQUIRE(reply->topic == mdp::Message::URI("/second.service?origin=indian")); REQUIRE(reply->data.asString() == "Sizzling tikkas in our Restaurant!"); REQUIRE(reply->error.empty()); REQUIRE(reply->rbac.asString() == "rbac_worker_2"); @@ -938,7 +1055,9 @@ TEST_CASE("pubsub example using router socket (DEALER client)", "[broker][pubsub TEST_CASE("pubsub example using PUB socket (SUB client)", "[broker][pubsub_subclient]") { using opencmw::majordomo::Broker; - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); + broker.addFilter>("origin"); + broker.addFilter>("dish"); BrokerMessageNode subscriber(broker.context, ZMQ_SUB); REQUIRE(subscriber.connect(opencmw::majordomo::INTERNAL_ADDRESS_PUBLISHER)); @@ -949,19 +1068,31 @@ TEST_CASE("pubsub example using PUB socket (SUB client)", "[broker][pubsub_subcl MessageNode publisherTwo(broker.context); REQUIRE(publisherTwo.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); - subscriber.subscribe(mdp::SubscriptionTopic("/cooking.italian*")); + subscriber.subscribe("/first.service?origin=italian"); broker.processMessages(); - subscriber.subscribe(mdp::SubscriptionTopic("/cooking.indian*")); + subscriber.subscribe("/second.service?origin=indian"); + + subscriber.subscribe("/s-e-r-v-i-c-e"); // invalid service name broker.processMessages(); - // publisher 1 sends a notification for /cooking.italian.pasta + // publisher 1 sends a notification that nobody receives (origin=chicago not matching) + { + auto pubMsg = createWorkerMessage(mdp::Command::Notify); + pubMsg.serviceName = "/first.service"; + pubMsg.topic = mdp::Message::URI("/first.service?origin=chicago&dish=pizza"); + pubMsg.data = IoBuffer("Deep dish"); + pubMsg.rbac = IoBuffer("rbac_worker_1"); + publisherOne.send(std::move(pubMsg)); + } + + // publisher 1 sends a notification for Italian pasta { auto pubMsg = createWorkerMessage(mdp::Command::Notify); - pubMsg.serviceName = "first.service"; - pubMsg.endpoint = mdp::Message::URI("/cooking.italian.pasta"); + pubMsg.serviceName = "/first.service"; + pubMsg.topic = mdp::Message::URI("/first.service?origin=italian&dish=pasta"); pubMsg.data = IoBuffer("Original carbonara recipe here!"); pubMsg.rbac = IoBuffer("rbac_worker_1"); publisherOne.send(std::move(pubMsg)); @@ -969,26 +1100,26 @@ TEST_CASE("pubsub example using PUB socket (SUB client)", "[broker][pubsub_subcl broker.processMessages(); - // client receives notification for /cooking.italian* + // client receives notification for Italian (not subscribed to pasta, but still a match) { const auto reply = subscriber.tryReadOne(); REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); - REQUIRE(reply->sourceId == "/cooking.italian*"); + REQUIRE(reply->sourceId == "/first.service?origin=italian"); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "first.service"); + REQUIRE(reply->serviceName == "/first.service"); REQUIRE(reply->clientRequestID.empty()); - REQUIRE(reply->endpoint.str() == "/cooking.italian.pasta"); + REQUIRE(reply->topic == mdp::Message::URI("/first.service?origin=italian&dish=pasta")); REQUIRE(reply->data.asString() == "Original carbonara recipe here!"); REQUIRE(reply->error.empty()); REQUIRE(reply->rbac.asString() == "rbac_worker_1"); } - // publisher 2 sends a notification for /cooking.indian.chicken + // publisher 2 sends a notification for Indian chicken { auto pubMsg = createWorkerMessage(mdp::Command::Notify); - pubMsg.serviceName = "second.service"; - pubMsg.endpoint = mdp::Message::URI("/cooking.indian.chicken"); + pubMsg.serviceName = "/second.service"; + pubMsg.topic = mdp::Message::URI("/second.service?origin=indian&dish=chicken"); pubMsg.data = IoBuffer("Try our Chicken Korma!"); pubMsg.rbac = IoBuffer("rbac_worker_2"); publisherTwo.send(std::move(pubMsg)); @@ -996,30 +1127,30 @@ TEST_CASE("pubsub example using PUB socket (SUB client)", "[broker][pubsub_subcl broker.processMessages(); - // client receives notification for /cooking.indian* + // client receives notification for Indian { const auto reply = subscriber.tryReadOne(); REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); - REQUIRE(reply->sourceId == "/cooking.indian*"); + REQUIRE(reply->sourceId == "/second.service?origin=indian"); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "second.service"); + REQUIRE(reply->serviceName == "/second.service"); REQUIRE(reply->clientRequestID.empty()); - REQUIRE(reply->endpoint.str() == "/cooking.indian.chicken"); + REQUIRE(reply->topic == mdp::Message::URI("/second.service?origin=indian&dish=chicken")); REQUIRE(reply->data.asString() == "Try our Chicken Korma!"); REQUIRE(reply->error.empty()); REQUIRE(reply->rbac.asString() == "rbac_worker_2"); } - subscriber.unsubscribe(mdp::SubscriptionTopic("/cooking.italian*")); + subscriber.unsubscribe("/first.service?origin=italian"); broker.processMessages(); - // publisher 1 sends a notification for /cooking.italian.pizza + // publisher 1 sends a notification for Italian pizza { auto pubMsg = createWorkerMessage(mdp::Command::Notify); - pubMsg.serviceName = "first.service"; - pubMsg.endpoint = mdp::Message::URI("/cooking.italian.pizza"); + pubMsg.serviceName = "/first.service"; + pubMsg.topic = mdp::Message::URI("/first.service?origin=italian&dish=pizza"); pubMsg.data = IoBuffer("The best Margherita in town!"); pubMsg.rbac = IoBuffer("rbac_worker_1"); publisherOne.send(std::move(pubMsg)); @@ -1027,11 +1158,11 @@ TEST_CASE("pubsub example using PUB socket (SUB client)", "[broker][pubsub_subcl broker.processMessages(); - // publisher 2 sends a notification for /cooking.indian.tikkas + // publisher 2 sends a notification for Indian tikkas { auto pubMsg = createWorkerMessage(mdp::Command::Notify); - pubMsg.serviceName = "second.service"; - pubMsg.endpoint = mdp::Message::URI("/cooking.indian.tikkas"); + pubMsg.serviceName = "/second.service"; + pubMsg.topic = mdp::Message::URI("/second.service?origin=indian&dish=tikkas"); pubMsg.data = IoBuffer("Sizzling tikkas in our Restaurant!"); pubMsg.rbac = IoBuffer("rbac_worker_2"); publisherTwo.send(std::move(pubMsg)); @@ -1045,11 +1176,11 @@ TEST_CASE("pubsub example using PUB socket (SUB client)", "[broker][pubsub_subcl const auto reply = subscriber.tryReadOne(); REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); - REQUIRE(reply->sourceId == "/cooking.indian*"); + REQUIRE(reply->sourceId == "/second.service?origin=indian"); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "second.service"); + REQUIRE(reply->serviceName == "/second.service"); REQUIRE(reply->clientRequestID.empty()); - REQUIRE(reply->endpoint.str() == "/cooking.indian.tikkas"); + REQUIRE(reply->topic == mdp::Message::URI("/second.service?origin=indian&dish=tikkas")); REQUIRE(reply->data.asString() == "Sizzling tikkas in our Restaurant!"); REQUIRE(reply->error.empty()); REQUIRE(reply->rbac.asString() == "rbac_worker_2"); @@ -1057,19 +1188,19 @@ TEST_CASE("pubsub example using PUB socket (SUB client)", "[broker][pubsub_subcl } TEST_CASE("BasicWorker connects to non-existing broker", "[worker]") { - const zmq::Context context; - BasicWorker<"a.service"> worker(URI<>("inproc:/doesnotexist"), TestIntHandler(10), context); + const zmq::Context context; + BasicWorker<"/a.service"> worker(URI<>("inproc:/doesnotexist"), TestIntHandler(10), context); worker.run(); // returns immediately on connection failure } TEST_CASE("BasicWorker run loop quits when broker quits", "[worker]") { - const zmq::Context context; - Broker broker("testbroker", testSettings()); - BasicWorker<"a.service"> worker(broker, TestIntHandler(10)); + const zmq::Context context; + Broker broker("/testbroker", testSettings()); + BasicWorker<"/a.service"> worker(broker, TestIntHandler(10)); - RunInThread brokerRun(broker); + RunInThread brokerRun(broker); - auto quitBroker = std::jthread([&broker]() { + auto quitBroker = std::jthread([&broker]() { std::this_thread::sleep_for(std::chrono::milliseconds(250)); broker.shutdown(); }); @@ -1092,7 +1223,7 @@ TEST_CASE("BasicWorker connection basics", "[worker][basic_worker_connection]") settings.heartbeatLiveness = 2; settings.workerReconnectInterval = std::chrono::milliseconds(200); - BasicWorker<"a.service"> worker( + BasicWorker<"/a.service"> worker( routerAddress, [](RequestContext &) {}, context, settings); RunInThread workerRun(worker); @@ -1103,7 +1234,7 @@ TEST_CASE("BasicWorker connection basics", "[worker][basic_worker_connection]") const auto ready = brokerRouter.tryReadOne(); REQUIRE(ready.has_value()); REQUIRE(ready->command == mdp::Command::Ready); - REQUIRE(ready->serviceName == "a.service"); + REQUIRE(ready->serviceName == "/a.service"); workerId = ready->sourceId; } @@ -1112,7 +1243,7 @@ TEST_CASE("BasicWorker connection basics", "[worker][basic_worker_connection]") const auto heartbeat = brokerRouter.tryReadOne(settings.heartbeatInterval * 23 / 10); REQUIRE(heartbeat.has_value()); REQUIRE(heartbeat->command == mdp::Command::Heartbeat); - REQUIRE(heartbeat->serviceName == "a.service"); + REQUIRE(heartbeat->serviceName == "/a.service"); REQUIRE(heartbeat->sourceId == workerId); } @@ -1121,7 +1252,7 @@ TEST_CASE("BasicWorker connection basics", "[worker][basic_worker_connection]") const auto ready = brokerRouter.tryReadOne(settings.heartbeatInterval * (settings.heartbeatLiveness + 1) + settings.workerReconnectInterval); REQUIRE(ready.has_value()); REQUIRE(ready->command == mdp::Command::Ready); - REQUIRE(ready->serviceName == "a.service"); + REQUIRE(ready->serviceName == "/a.service"); workerId = ready->sourceId; } @@ -1141,7 +1272,7 @@ TEST_CASE("BasicWorker connection basics", "[worker][basic_worker_connection]") const auto disconnect = brokerRouter.tryReadOne(); REQUIRE(disconnect.has_value()); REQUIRE(disconnect->command == mdp::Command::Disconnect); - REQUIRE(disconnect->serviceName == "a.service"); + REQUIRE(disconnect->serviceName == "/a.service"); REQUIRE(disconnect->sourceId == workerId); } } @@ -1149,9 +1280,9 @@ TEST_CASE("BasicWorker connection basics", "[worker][basic_worker_connection]") TEST_CASE("SET/GET example using the BasicWorker class", "[worker][getset_basic_worker]") { using opencmw::majordomo::Broker; - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); - BasicWorker<"a.service", opencmw::majordomo::description<"API description">> worker(broker, TestIntHandler(10)); + BasicWorker<"/a.service", opencmw::majordomo::description<"API description">> worker(broker, TestIntHandler(10)); REQUIRE(worker.serviceDescription() == "API description"); MessageNode client(broker.context); @@ -1165,9 +1296,9 @@ TEST_CASE("SET/GET example using the BasicWorker class", "[worker][getset_basic_ bool replyReceived = false; while (!replyReceived) { auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "a.service"; + request.serviceName = "/a.service"; request.clientRequestID = IoBuffer("1"); - request.endpoint = mdp::Message::URI("/topic"); + request.topic = mdp::Message::URI("/topic"); request.rbac = IoBuffer("rbacToken"); client.send(std::move(request)); @@ -1181,8 +1312,8 @@ TEST_CASE("SET/GET example using the BasicWorker class", "[worker][getset_basic_ if (!reply->error.empty()) { REQUIRE(reply->error.find("error 501") != std::string_view::npos); } else { - REQUIRE(reply->serviceName == "a.service"); - REQUIRE(reply->endpoint.str() == "/topic"); + REQUIRE(reply->serviceName == "/a.service"); + REQUIRE(reply->topic.str() == "/topic"); REQUIRE(reply->data.asString() == "10"); REQUIRE(reply->error.empty()); REQUIRE(reply->rbac.asString() == "rbacToken"); @@ -1192,9 +1323,9 @@ TEST_CASE("SET/GET example using the BasicWorker class", "[worker][getset_basic_ { auto request = createClientMessage(mdp::Command::Set); - request.serviceName = "a.service"; + request.serviceName = "/a.service"; request.clientRequestID = IoBuffer("2"); - request.endpoint = mdp::Message::URI("/topic"); + request.topic = mdp::Message::URI("/topic"); request.data = IoBuffer("42"); request.rbac = IoBuffer("rbacToken"); @@ -1212,9 +1343,9 @@ TEST_CASE("SET/GET example using the BasicWorker class", "[worker][getset_basic_ { auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "a.service"; + request.serviceName = "/a.service"; request.clientRequestID = IoBuffer("3"); - request.endpoint = mdp::Message::URI("/topic"); + request.topic = mdp::Message::URI("/topic"); request.rbac = IoBuffer("rbacToken"); client.send(std::move(request)); @@ -1224,7 +1355,7 @@ TEST_CASE("SET/GET example using the BasicWorker class", "[worker][getset_basic_ REQUIRE(reply->protocolName == mdp::clientProtocol); REQUIRE(reply->command == mdp::Command::Final); REQUIRE(reply->clientRequestID.asString() == "3"); - REQUIRE(reply->endpoint.str() == "/topic"); + REQUIRE(reply->topic.str() == "/topic"); REQUIRE(reply->data.asString() == "42"); REQUIRE(reply->error.empty()); } @@ -1235,14 +1366,14 @@ TEST_CASE("BasicWorker SET/GET example with RBAC permission handling", "[worker] using READER = Role<"READER", Permission::RO>; using opencmw::majordomo::description; - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); BasicWorker<"/a.service", description<"API description">, rbac> worker(broker, TestIntHandler(10)); REQUIRE(worker.serviceDescription() == "API description"); RunInThread brokerRun(broker); RunInThread workerRun(worker); - REQUIRE(waitUntilServiceAvailable(broker.context, "/a.service")); + REQUIRE(waitUntilWorkerServiceAvailable(broker.context, worker)); MessageNode writer(broker.context); REQUIRE(writer.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); @@ -1252,7 +1383,7 @@ TEST_CASE("BasicWorker SET/GET example with RBAC permission handling", "[worker] auto set = createClientMessage(mdp::Command::Set); set.serviceName = "/a.service"; set.clientRequestID = IoBuffer("1"); - set.endpoint = mdp::Message::URI("/topic"); + set.topic = mdp::Message::URI("/topic"); set.data = IoBuffer("42"); set.rbac = IoBuffer("RBAC=WRITER,1234"); @@ -1270,7 +1401,7 @@ TEST_CASE("BasicWorker SET/GET example with RBAC permission handling", "[worker] auto get = createClientMessage(mdp::Command::Get); get.serviceName = "/a.service"; get.clientRequestID = IoBuffer("2"); - get.endpoint = mdp::Message::URI("/topic"); + get.topic = mdp::Message::URI("/topic"); get.rbac = IoBuffer("RBAC=WRITER,1234"); writer.send(std::move(get)); @@ -1290,7 +1421,7 @@ TEST_CASE("BasicWorker SET/GET example with RBAC permission handling", "[worker] auto set = createClientMessage(mdp::Command::Set); set.serviceName = "/a.service"; set.clientRequestID = IoBuffer("1"); - set.endpoint = mdp::Message::URI("/topic"); + set.topic = mdp::Message::URI("/topic"); set.data = IoBuffer("42"); set.rbac = IoBuffer("RBAC=READER,1234"); @@ -1308,7 +1439,7 @@ TEST_CASE("BasicWorker SET/GET example with RBAC permission handling", "[worker] auto get = createClientMessage(mdp::Command::Get); get.serviceName = "/a.service"; get.clientRequestID = IoBuffer("2"); - get.endpoint = mdp::Message::URI("/topic"); + get.topic = mdp::Message::URI("/topic"); get.rbac = IoBuffer("RBAC=READER,1234"); reader.send(std::move(get)); @@ -1328,7 +1459,7 @@ TEST_CASE("BasicWorker SET/GET example with RBAC permission handling", "[worker] auto set = createClientMessage(mdp::Command::Set); set.serviceName = "/a.service"; set.clientRequestID = IoBuffer("1"); - set.endpoint = mdp::Message::URI("/topic"); + set.topic = mdp::Message::URI("/topic"); set.data = IoBuffer("42"); set.rbac = IoBuffer("RBAC=ADMIN,1234"); @@ -1346,7 +1477,7 @@ TEST_CASE("BasicWorker SET/GET example with RBAC permission handling", "[worker] auto get = createClientMessage(mdp::Command::Get); get.serviceName = "/a.service"; get.clientRequestID = IoBuffer("2"); - get.endpoint = mdp::Message::URI("/topic"); + get.topic = mdp::Message::URI("/topic"); get.rbac = IoBuffer("RBAC=ADMIN,1234"); admin.send(std::move(get)); @@ -1363,11 +1494,14 @@ TEST_CASE("NOTIFY example using the BasicWorker class", "[worker][notify_basic_w using opencmw::majordomo::Broker; using namespace std::literals; - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); - BasicWorker<"beverages"> worker(broker, TestIntHandler(10)); + BasicWorker<"/beverages"> worker(broker, TestIntHandler(10)); + broker.addFilter>("fridge"); + broker.addFilter>("iwant"); + broker.addFilter>("origin"); - BrokerMessageNode client(broker.context, ZMQ_XSUB); + BrokerMessageNode client(broker.context, ZMQ_XSUB); REQUIRE(client.connect(opencmw::majordomo::INTERNAL_ADDRESS_PUBLISHER)); RunInThread brokerRun(broker); @@ -1378,30 +1512,29 @@ TEST_CASE("NOTIFY example using the BasicWorker class", "[worker][notify_basic_w REQUIRE(client.sendRawFrame("\x1")); REQUIRE(client.sendRawFrame("\x0"s)); - // subscribe to /wine* and /beer* - REQUIRE(client.sendRawFrame("\x1/wine*")); - REQUIRE(client.sendRawFrame("\x1/beer*")); + REQUIRE(client.sendRawFrame("\x1/beverages?iwant=wine")); + REQUIRE(client.sendRawFrame("\x1/beverages?iwant=beer")); bool seenNotification = false; // we have a potential race here: the worker might not have processed the - // subscribe yet and thus discarding the notification. Send notifications + // subscribe yet and thus discard the notification. Send notifications // in a loop until one gets through. while (!seenNotification) { { mdp::Message notify; - notify.endpoint = mdp::Message::URI("/beer.time"); - notify.data = IoBuffer("Have a beer"); + notify.topic = mdp::Message::URI("/beverages?iwant=beer"); + notify.data = IoBuffer("Have a beer"); REQUIRE(worker.notify(std::move(notify))); } { const auto notification = client.tryReadOne(std::chrono::milliseconds(20)); - if (notification && notification->serviceName != "mmi.service") { + if (notification && notification->serviceName != "/mmi.service") { seenNotification = true; REQUIRE(notification->protocolName == mdp::clientProtocol); REQUIRE(notification->command == mdp::Command::Final); - REQUIRE(notification->sourceId == "/beer*"); - REQUIRE(notification->endpoint.str() == "/beer.time"); + REQUIRE(notification->sourceId == "/beverages?iwant=beer"); + REQUIRE(notification->topic == mdp::Message::URI("/beverages?iwant=beer")); REQUIRE(notification->data.asString() == "Have a beer"); } } @@ -1409,8 +1542,8 @@ TEST_CASE("NOTIFY example using the BasicWorker class", "[worker][notify_basic_w { mdp::Message notify; - notify.endpoint = mdp::Message::URI("/beer.error"); - notify.error = "Fridge empty!"; + notify.topic = mdp::Message::URI("/beverages?iwant=beer&fridge=empty"); + notify.error = "Fridge empty!"; REQUIRE(worker.notify(std::move(notify))); } @@ -1421,24 +1554,24 @@ TEST_CASE("NOTIFY example using the BasicWorker class", "[worker][notify_basic_w continue; // there might be extra messages from above, ignore them - if (notification->endpoint.str() == "/beer.time") { + if (notification->topic == mdp::Message::URI("/beverages?iwant=beer")) { continue; } REQUIRE(notification->protocolName == mdp::clientProtocol); REQUIRE(notification->command == mdp::Command::Final); - REQUIRE(notification->sourceId == "/beer*"); - REQUIRE(notification->endpoint.str() == "/beer.error"); + REQUIRE(notification->sourceId == "/beverages?iwant=beer"); + REQUIRE(notification->topic == mdp::Message::URI("/beverages?iwant=beer&fridge=empty")); REQUIRE(notification->error == "Fridge empty!"); seenError = true; } { - // as the subscribe for wine* was sent before the beer* one, this should be - // race-free now (as know the beer* subscribe was processed by everyone) + // as the subscribe for wine was sent before the beer one, this should be + // race-free now (as know the beer subscribe was processed by everyone) mdp::Message notify; - notify.endpoint = mdp::Message::URI("/wine.italian"); - notify.data = IoBuffer("Try our Chianti!"); + notify.topic = mdp::Message::URI("/beverages?iwant=wine&origin=italian"); + notify.data = IoBuffer("Try our Chianti!"); REQUIRE(worker.notify(std::move(notify))); } @@ -1447,61 +1580,64 @@ TEST_CASE("NOTIFY example using the BasicWorker class", "[worker][notify_basic_w REQUIRE(notification.has_value()); REQUIRE(notification->protocolName == mdp::clientProtocol); REQUIRE(notification->command == mdp::Command::Final); - REQUIRE(notification->sourceId == "/wine*"); - REQUIRE(notification->endpoint.str() == "/wine.italian"); + REQUIRE(notification->sourceId == "/beverages?iwant=wine"); + REQUIRE(notification->topic == mdp::Message::URI("/beverages?iwant=wine&origin=italian")); REQUIRE(notification->data.asString() == "Try our Chianti!"); } // unsubscribe from /beer* - REQUIRE(client.sendRawFrame("\x0/beer*"s)); + REQUIRE(client.sendRawFrame("\x0/beverages?iwant=beer"s)); // loop until we get two consecutive messages about wine, it means that the beer unsubscribe was processed while (true) { { mdp::Message notify; - notify.endpoint = mdp::Message::URI("/wine.portuguese"); - notify.data = IoBuffer("New Vinho Verde arrived."); + notify.topic = mdp::Message::URI("/beverages?iwant=wine&origin=portuguese"); + notify.data = IoBuffer("New Vinho Verde arrived."); REQUIRE(worker.notify(std::move(notify))); } { mdp::Message notify; - notify.endpoint = mdp::Message::URI("/beer.offer"); - notify.data = IoBuffer("Get our pilsner now!"); + notify.topic = mdp::Message::URI("/beverages?iwant=beer"); + notify.data = IoBuffer("Get our pilsner now!"); REQUIRE(worker.notify(std::move(notify))); } { mdp::Message notify; - notify.endpoint = mdp::Message::URI("/wine.portuguese"); - notify.data = IoBuffer("New Vinho Verde arrived."); + notify.topic = mdp::Message::URI("/beverages?iwant=wine&origin=portuguese"); + notify.data = IoBuffer("New Vinho Verde arrived."); REQUIRE(worker.notify(std::move(notify))); } const auto msg1 = client.tryReadOne(); REQUIRE(msg1.has_value()); - REQUIRE(msg1->sourceId == "/wine*"); + REQUIRE(msg1->sourceId == "/beverages?iwant=wine"); const auto msg2 = client.tryReadOne(); REQUIRE(msg2.has_value()); - if (msg2->sourceId == "/wine*") { + if (msg2->sourceId == "/beverages?iwant=wine") { break; } - REQUIRE(msg2->sourceId == "/beer*"); + REQUIRE(msg2->sourceId == "/beverages?iwant=beer"); const auto msg3 = client.tryReadOne(); REQUIRE(msg3.has_value()); - REQUIRE(msg3->sourceId == "/wine*"); + REQUIRE(msg3->sourceId == "/beverages?iwant=wine"); } } TEST_CASE("NOTIFY example using the BasicWorker class (via ROUTER socket)", "[worker][notify_basic_worker_router]") { using opencmw::majordomo::Broker; - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); + broker.addFilter>("fridge"); + broker.addFilter>("iwant"); + broker.addFilter>("origin"); - BasicWorker<"beverages"> worker(broker, TestIntHandler(10)); + BasicWorker<"/beverages"> worker(broker, TestIntHandler(10)); - MessageNode client(broker.context); + MessageNode client(broker.context); REQUIRE(client.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); RunInThread brokerRun(broker); @@ -1509,14 +1645,14 @@ TEST_CASE("NOTIFY example using the BasicWorker class (via ROUTER socket)", "[wo { auto subscribe = createClientMessage(mdp::Command::Subscribe); - subscribe.serviceName = "beverages"; - subscribe.endpoint = mdp::Message::URI("/wine"); + subscribe.serviceName = "/beverages"; + subscribe.topic = mdp::Message::URI("/beverages?iwant=wine"); client.send(std::move(subscribe)); } { auto subscribe = createClientMessage(mdp::Command::Subscribe); - subscribe.serviceName = "beverages"; - subscribe.endpoint = mdp::Message::URI("/beer"); + subscribe.serviceName = "/beverages"; + subscribe.topic = mdp::Message::URI("/beverages?iwant=beer"); client.send(std::move(subscribe)); } @@ -1528,17 +1664,17 @@ TEST_CASE("NOTIFY example using the BasicWorker class (via ROUTER socket)", "[wo while (!seenNotification) { { mdp::Message notify; - notify.endpoint = mdp::Message::URI("/beer"); - notify.data = IoBuffer("Have a beer"); + notify.topic = mdp::Message::URI("/beverages?iwant=beer"); + notify.data = IoBuffer("Have a beer"); REQUIRE(worker.notify(std::move(notify))); } { const auto notification = client.tryReadOne(std::chrono::milliseconds(20)); - if (notification && notification->serviceName != "mmi.service") { + if (notification && notification->serviceName != "/mmi.service") { seenNotification = true; REQUIRE(notification->protocolName == mdp::clientProtocol); REQUIRE(notification->command == mdp::Command::Final); - REQUIRE(notification->endpoint.str() == "/beer"); + REQUIRE(notification->topic == mdp::Message::URI("/beverages?iwant=beer")); REQUIRE(notification->data.asString() == "Have a beer"); } } @@ -1548,8 +1684,8 @@ TEST_CASE("NOTIFY example using the BasicWorker class (via ROUTER socket)", "[wo // as the subscribe for /wine was sent before the /beer one, this should be // race-free now (as know the /beer subscribe was processed by everyone) mdp::Message notify; - notify.endpoint = mdp::Message::URI("/wine"); - notify.data = IoBuffer("Try our Chianti!"); + notify.topic = mdp::Message::URI("/beverages?iwant=wine"); + notify.data = IoBuffer("Try our Chianti!"); REQUIRE(worker.notify(std::move(notify))); } @@ -1558,15 +1694,15 @@ TEST_CASE("NOTIFY example using the BasicWorker class (via ROUTER socket)", "[wo REQUIRE(notification.has_value()); REQUIRE(notification->protocolName == mdp::clientProtocol); REQUIRE(notification->command == mdp::Command::Final); - REQUIRE(notification->endpoint.str() == "/wine"); + REQUIRE(notification->topic == mdp::Message::URI("/beverages?iwant=wine")); REQUIRE(notification->data.asString() == "Try our Chianti!"); } // unsubscribe from /beer { auto unsubscribe = createClientMessage(mdp::Command::Unsubscribe); - unsubscribe.serviceName = "beverages"; - unsubscribe.endpoint = mdp::Message::URI("/beer"); + unsubscribe.serviceName = "/beverages"; + unsubscribe.topic = mdp::Message::URI("/beverages?iwant=beer"); client.send(std::move(unsubscribe)); } @@ -1574,37 +1710,37 @@ TEST_CASE("NOTIFY example using the BasicWorker class (via ROUTER socket)", "[wo while (true) { { mdp::Message notify; - notify.endpoint = mdp::Message::URI("/wine"); - notify.data = IoBuffer("New Vinho Verde arrived."); + notify.topic = mdp::Message::URI("/beverages?iwant=wine"); + notify.data = IoBuffer("New Vinho Verde arrived."); REQUIRE(worker.notify(std::move(notify))); } { mdp::Message notify; - notify.endpoint = mdp::Message::URI("/beer"); - notify.data = IoBuffer("Get our pilsner now!"); + notify.topic = mdp::Message::URI("/beverages?iwant=beer"); + notify.data = IoBuffer("Get our pilsner now!"); REQUIRE(worker.notify(std::move(notify))); } { mdp::Message notify; - notify.endpoint = mdp::Message::URI("/wine"); - notify.data = IoBuffer("New Vinho Verde arrived."); + notify.topic = mdp::Message::URI("/beverages?iwant=wine"); + notify.data = IoBuffer("New Vinho Verde arrived."); REQUIRE(worker.notify(std::move(notify))); } const auto msg1 = client.tryReadOne(); REQUIRE(msg1.has_value()); - REQUIRE(msg1->endpoint.str() == "/wine"); + REQUIRE(msg1->topic == mdp::Message::URI("/beverages?iwant=wine")); const auto msg2 = client.tryReadOne(); REQUIRE(msg2.has_value()); - if (msg2->endpoint.str() == "/wine") { + if (msg2->topic == mdp::Message::URI("/beverages?iwant=wine")) { break; } - REQUIRE(msg2->endpoint.str() == "/beer"); + REQUIRE(msg2->topic == mdp::Message::URI("/beverages?iwant=beer")); const auto msg3 = client.tryReadOne(); REQUIRE(msg3.has_value()); - REQUIRE(msg3->endpoint.str() == "/wine"); + REQUIRE(msg3->topic == mdp::Message::URI("/beverages?iwant=wine")); } } @@ -1612,7 +1748,7 @@ TEST_CASE("SET/GET example using a lambda as the worker's request handler", "[wo using opencmw::majordomo::Broker; using opencmw::majordomo::MockClient; - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); auto handleInt = [](RequestContext &requestContext) { static int value = 100; @@ -1637,31 +1773,31 @@ TEST_CASE("SET/GET example using a lambda as the worker's request handler", "[wo } }; - BasicWorker<"a.service"> worker(broker, std::move(handleInt)); + BasicWorker<"/a.service"> worker(broker, std::move(handleInt)); - MockClient client(broker.context); + MockClient client(broker.context); REQUIRE(client.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); RunInThread brokerRun(broker); RunInThread workerRun(worker); - REQUIRE(waitUntilServiceAvailable(broker.context, "a.service")); + REQUIRE(waitUntilWorkerServiceAvailable(broker.context, worker)); - client.get("a.service", {}, [](auto &&message) { + client.get("/a.service", {}, [](auto &&message) { REQUIRE(message.error == ""); REQUIRE(message.data.asString() == "100"); }); REQUIRE(client.tryRead(std::chrono::seconds(3))); - client.set("a.service", IoBuffer("42"), [](auto &&message) { + client.set("/a.service", IoBuffer("42"), [](auto &&message) { REQUIRE(message.error == ""); REQUIRE(message.data.asString() == "Value set. All good!"); }); REQUIRE(client.tryRead(std::chrono::seconds(3))); - client.get("a.service", {}, [](auto &&message) { + client.get("/a.service", {}, [](auto &&message) { REQUIRE(message.error == ""); REQUIRE(message.data.asString() == "42"); }); @@ -1673,24 +1809,24 @@ TEST_CASE("Worker's request handler throws an exception", "[worker][handler_exce using opencmw::majordomo::Broker; using opencmw::majordomo::MockClient; - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); auto handleRequest = [](RequestContext &) { throw std::runtime_error("Something went wrong!"); }; - BasicWorker<"a.service"> worker(broker, std::move(handleRequest)); + BasicWorker<"/a.service"> worker(broker, std::move(handleRequest)); - MockClient client(broker.context); + MockClient client(broker.context); REQUIRE(client.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); RunInThread brokerRun(broker); RunInThread workerRun(worker); - REQUIRE(waitUntilServiceAvailable(broker.context, "a.service")); + REQUIRE(waitUntilWorkerServiceAvailable(broker.context, worker)); - client.get("a.service", {}, [](auto &&message) { - REQUIRE(message.error == "Caught exception for service 'a.service'\nrequest message: \nexception: Something went wrong!"); + client.get("/a.service", {}, [](auto &&message) { + REQUIRE(message.error == "Caught exception for service '/a.service'\nrequest message: \nexception: Something went wrong!"); }); REQUIRE(client.tryRead(std::chrono::seconds(3))); @@ -1700,24 +1836,24 @@ TEST_CASE("Worker's request handler throws an unexpected exception", "[worker][h using opencmw::majordomo::Broker; using opencmw::majordomo::MockClient; - Broker broker("testbroker", testSettings()); + Broker broker("/testbroker", testSettings()); auto handleRequest = [](RequestContext &) { throw std::string("Something went wrong!"); }; - BasicWorker<"a.service"> worker(broker, std::move(handleRequest)); + BasicWorker<"/a.service"> worker(broker, std::move(handleRequest)); - MockClient client(broker.context); + MockClient client(broker.context); REQUIRE(client.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); RunInThread brokerRun(broker); RunInThread workerRun(worker); - REQUIRE(waitUntilServiceAvailable(broker.context, "a.service")); + REQUIRE(waitUntilWorkerServiceAvailable(broker.context, worker)); - client.get("a.service", {}, [](auto &&message) { - REQUIRE(message.error == "Caught unexpected exception for service 'a.service'\nrequest message: "); + client.get("/a.service", {}, [](auto &&message) { + REQUIRE(message.error == "Caught unexpected exception for service '/a.service'\nrequest message: "); }); REQUIRE(client.tryRead(std::chrono::seconds(3))); diff --git a/src/majordomo/test/majordomoworker_rest_tests.cpp b/src/majordomo/test/majordomoworker_rest_tests.cpp index e71d793e..7e44b6bd 100644 --- a/src/majordomo/test/majordomoworker_rest_tests.cpp +++ b/src/majordomo/test/majordomoworker_rest_tests.cpp @@ -12,31 +12,98 @@ #include #include +#include #include #include // Concepts and tests use common types #include -std::jthread makeGetRequestResponseCheckerThread(const std::string &address, const std::string &requiredResponse, [[maybe_unused]] std::source_location location = std::source_location::current()) { +std::jthread makeGetRequestResponseCheckerThread(const std::string &address, const std::vector &requiredResponses, const std::vector &requiredStatusCodes = {}, [[maybe_unused]] std::source_location location = std::source_location::current()) { return std::jthread([=] { httplib::Client http("localhost", majordomo::DEFAULT_REST_PORT); + http.set_follow_location(true); http.set_keep_alive(true); - const auto response = http.Get(address.data()); - #define requireWithSource(arg) \ if (!(arg)) opencmw::zmq::debug::withLocation(location) << "<- call got a failed requirement:"; \ REQUIRE(arg) - requireWithSource(response); - requireWithSource(response->status == 200); - requireWithSource(response->body.find(requiredResponse) != std::string::npos); + for (std::size_t i = 0; i < requiredResponses.size(); ++i) { + const auto response = http.Get(address); + requireWithSource(response); + const auto requiredStatusCode = i < requiredStatusCodes.size() ? requiredStatusCodes[i] : 200; + requireWithSource(response->status == requiredStatusCode); + requireWithSource(response->body.find(requiredResponses[i]) != std::string::npos); + } #undef requireWithSource }); } +struct ColorContext { + bool red = false; + bool green = false; + bool blue = false; + opencmw::MIME::MimeType contentType = opencmw::MIME::JSON; +}; + +ENABLE_REFLECTION_FOR(ColorContext, red, green, blue, contentType) + +struct SingleString { + std::string value; +}; +ENABLE_REFLECTION_FOR(SingleString, value) + +template +class ColorWorker : public majordomo::Worker { + std::jthread notifyThread; + +public: + using super_t = majordomo::Worker; + + template + explicit ColorWorker(const BrokerType &broker, std::vector notificationContexts) + : super_t(broker, {}) { + notifyThread = std::jthread([this, contexts = std::move(notificationContexts)]() { + int counter = 0; + for (const auto &context : contexts) { + std::this_thread::sleep_for(150ms); + super_t::notify(context, { std::to_string(counter) }); + counter++; + } + }); + } +}; + +struct PathContext { + opencmw::MIME::MimeType contentType = opencmw::MIME::JSON; +}; + +ENABLE_REFLECTION_FOR(PathContext, contentType) + +template +class PathWorker : public majordomo::Worker { +public: + using super_t = majordomo::Worker; + + template + explicit PathWorker(const BrokerType &broker) + : super_t(broker, {}) { + super_t::setCallback([this](majordomo::RequestContext &rawCtx, const PathContext &inCtx, const majordomo::Empty &, PathContext &outCtx, SingleString &out) { + outCtx = inCtx; + const auto endpointPath = rawCtx.request.topic.path().value_or(""); + std::string_view v(endpointPath); + if (v.starts_with(this->name)) { + v.remove_prefix(this->name.size()); + out.value = fmt::format("You requested path='{}'\'n", v); + } else { + throw std::invalid_argument(fmt::format("Invalid endpoint '{}' (must start with '{}')", endpointPath, this->name)); + } + }); + } +}; + TEST_CASE("Simple MajordomoWorker example showing its usage", "[majordomo][majordomoworker][simple_example]") { // We run both broker and worker inproc - majordomo::Broker broker("TestBroker", testSettings()); + majordomo::Broker broker("/TestBroker", testSettings()); auto fs = cmrc::assets::get_filesystem(); FileServerRestBackend rest(broker, fs); RunInThread restServerRun(rest); @@ -50,25 +117,25 @@ TEST_CASE("Simple MajordomoWorker example showing its usage", "[majordomo][major opencmw::query::registerTypes(SimpleContext(), broker); // Create MajordomoWorker with our domain objects, and our TestHandler. - majordomo::Worker<"addressbook", SimpleContext, AddressRequest, AddressEntry> worker(broker, TestAddressHandler()); + majordomo::Worker<"/addressbook", SimpleContext, AddressRequest, AddressEntry> worker(broker, TestAddressHandler()); // Run worker and broker in separate threads RunInThread brokerRun(broker); RunInThread workerRun(worker); - REQUIRE(waitUntilServiceAvailable(broker.context, "addressbook")); + REQUIRE(waitUntilWorkerServiceAvailable(broker.context, worker)); SECTION("request Address information as JSON and as HTML") { - auto httpThreadJSON = makeGetRequestResponseCheckerThread("/addressbook?ctx=FAIR.SELECTOR.ALL&contentType=application%2Fjavascript", "Santa Claus"); + auto httpThreadJSON = makeGetRequestResponseCheckerThread("/addressbook?ctx=FAIR.SELECTOR.ALL&contentType=application%2Fjavascript", { "Santa Claus" }); - auto httpThreadHTML = makeGetRequestResponseCheckerThread("/addressbook?ctx=FAIR.SELECTOR.ALL&contentType=text%2Fhtml", "Elf Road"); + auto httpThreadHTML = makeGetRequestResponseCheckerThread("/addressbook?ctx=FAIR.SELECTOR.ALL&contentType=text%2Fhtml", { "Elf Road" }); } SECTION("post data") { httplib::Client postData{ "http://localhost:8080" }; postData.Post("/addressbook?ctx=FAIR.SELECTOR.ALL&contentType=application%json", "{\"streetNumber\": 1882}", "application/json"); - auto httpThreadJSON = makeGetRequestResponseCheckerThread("/addressbook?ctx=FAIR.SELECTOR.ALL&contentType=application%json", "1882"); + auto httpThreadJSON = makeGetRequestResponseCheckerThread("/addressbook?ctx=FAIR.SELECTOR.ALL&contentType=application%json", { "1882" }); } SECTION("post data as multipart") { @@ -95,8 +162,86 @@ TEST_CASE("Simple MajordomoWorker example showing its usage", "[majordomo][major CAPTURE(r->body); REQUIRE(r->status == 200); - auto httpThreadJSON = makeGetRequestResponseCheckerThread("/addressbook?ctx=FAIR.SELECTOR.ALL&contentType=application%2Fjavascript", "Kalle"); + auto httpThreadJSON = makeGetRequestResponseCheckerThread("/addressbook?ctx=FAIR.SELECTOR.ALL&contentType=application%2Fjavascript", { "Kalle" }); } }; } } +TEST_CASE("Invalid paths", "[majordomo][majordomoworker][rest]") { + majordomo::Broker broker("/TestBroker", testSettings()); + auto fs = cmrc::assets::get_filesystem(); + FileServerRestBackend rest(broker, fs); + RunInThread restServerRun(rest); + + opencmw::query::registerTypes(PathContext(), broker); + + PathWorker<"/paths"> worker(broker); + + RunInThread brokerRun(broker); + RunInThread workerRun(worker); + + REQUIRE(waitUntilWorkerServiceAvailable(broker.context, worker)); + + auto space = makeGetRequestResponseCheckerThread("/paths/with%20space", { "Invalid service name" }, { 500 }); + auto invalidSubscription = makeGetRequestResponseCheckerThread("/p-a-t-h-s/?LongPollIdx=Next", { "Invalid service name" }, { 500 }); +} + +TEST_CASE("Get/Set with subpaths", "[majordomo][majordomoworker][rest]") { + majordomo::Broker broker("/TestBroker", testSettings()); + auto fs = cmrc::assets::get_filesystem(); + FileServerRestBackend rest(broker, fs); + RunInThread restServerRun(rest); + + opencmw::query::registerTypes(PathContext(), broker); + + PathWorker<"/paths"> worker(broker); + + RunInThread brokerRun(broker); + RunInThread workerRun(worker); + + REQUIRE(waitUntilWorkerServiceAvailable(broker.context, worker)); + + auto empty = makeGetRequestResponseCheckerThread("/paths", { "path=''" }); + auto one = makeGetRequestResponseCheckerThread("/paths/a", { "path='\\/a'" }); + auto two = makeGetRequestResponseCheckerThread("/paths/a/b", { "path='\\/a\\/b'" }); +} + +TEST_CASE("Subscriptions", "[majordomo][majordomoworker][subscription]") { + majordomo::Broker broker("/TestBroker", testSettings()); + auto fs = cmrc::assets::get_filesystem(); + FileServerRestBackend rest(broker, fs); + RunInThread restServerRun(rest); + + opencmw::query::registerTypes(ColorContext(), broker); + + constexpr auto red = ColorContext{ .red = true }; + constexpr auto green = ColorContext{ .green = true }; + constexpr auto blue = ColorContext{ .blue = true }; + constexpr auto magenta = ColorContext{ .red = true, .blue = true }; + constexpr auto yellow = ColorContext{ .red = true, .green = true }; + constexpr auto black = ColorContext{}; + constexpr auto white = ColorContext{ .red = true, .green = true, .blue = true }; + + ColorWorker<"/colors"> worker(broker, { red, green, blue, magenta, yellow, black, white }); + + RunInThread brokerRun(broker); + RunInThread workerRun(worker); + + REQUIRE(waitUntilWorkerServiceAvailable(broker.context, worker)); + + auto allListener = makeGetRequestResponseCheckerThread("/colors?LongPollingIdx=Next", { "0", "1", "2", "3", "4", "5", "6" }); + auto redListener = makeGetRequestResponseCheckerThread("/colors?LongPollingIdx=Next&red", { "0", "3", "4", "6" }); + auto yellowListener = makeGetRequestResponseCheckerThread("/colors?LongPollingIdx=Next&red&green", { "4", "6" }); + auto whiteListener1 = makeGetRequestResponseCheckerThread("/colors?LongPollingIdx=Next&red&green&blue", { "6" }); + auto whiteListener2 = makeGetRequestResponseCheckerThread("/colors?LongPollingIdx=Next&green&red&blue", { "6" }); + auto whiteListener3 = makeGetRequestResponseCheckerThread("/colors?LongPollingIdx=Next&blue&green&red", { "6" }); + + std::this_thread::sleep_for(50ms); // give time for subscriptions to happen + + std::vector subscriptions; + for (const auto &subscription : worker.activeSubscriptions()) { + subscriptions.push_back(subscription.toZmqTopic()); + } + std::ranges::sort(subscriptions); + REQUIRE(subscriptions == std::vector{ "/colors", "/colors?blue&green&red", "/colors?green&red", "/colors?red" }); +} diff --git a/src/majordomo/test/majordomoworker_tests.cpp b/src/majordomo/test/majordomoworker_tests.cpp index 83a0735f..6565d8f3 100644 --- a/src/majordomo/test/majordomoworker_tests.cpp +++ b/src/majordomo/test/majordomoworker_tests.cpp @@ -21,7 +21,7 @@ using opencmw::majordomo::Broker; using opencmw::majordomo::BrokerMessage; using opencmw::majordomo::Settings; using opencmw::majordomo::Worker; -using opencmw::mdp::SubscriptionTopic; +using opencmw::mdp::Topic; /* * This test serves as example on how MajordomoWorker is to be used. @@ -106,7 +106,7 @@ mdp::Message createClientMessage(mdp::Command command) { TEST_CASE("Simple MajordomoWorker example showing its usage", "[majordomo][majordomoworker][simple_example]") { // We run both broker and worker inproc - Broker broker("TestBroker", testSettings()); + Broker broker("/TestBroker", testSettings()); // For subscription matching, it is necessary that broker knows how to handle the query params "ctx" and "contentType". // ("ctx" needs to use the TimingCtxFilter, and "contentType" compare the mime types (currently simply a string comparison)) @@ -117,13 +117,13 @@ TEST_CASE("Simple MajordomoWorker example showing its usage", "[majordomo][major opencmw::query::registerTypes(TestContext(), broker); // Create MajordomoWorker with our domain objects, and our TestHandler. - Worker<"addressbook", TestContext, AddressQueryRequest, AddressEntry, opencmw::majordomo::description<"An Addressbook service">> worker(broker, TestHandler()); + Worker<"/addressbook", TestContext, AddressQueryRequest, AddressEntry, opencmw::majordomo::description<"An Addressbook service">> worker(broker, TestHandler()); // Run worker and broker in separate threads RunInThread brokerRun(broker); RunInThread workerRun(worker); - REQUIRE(waitUntilServiceAvailable(broker.context, "addressbook")); + REQUIRE(waitUntilWorkerServiceAvailable(broker.context, worker)); // The client used here is a simple test client, operating on raw messages. // Later, a client class analog to MajordomoWorker, sending AddressQueryRequest, and receiving AddressEntry, could be used. @@ -132,8 +132,8 @@ TEST_CASE("Simple MajordomoWorker example showing its usage", "[majordomo][major { // Make sure the API description is returned auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "mmi.openapi"; - request.data = IoBuffer("addressbook"); + request.serviceName = "/mmi.openapi"; + request.data = IoBuffer("/addressbook"); client.send(std::move(request)); const auto reply = client.tryReadOne(); @@ -141,7 +141,7 @@ TEST_CASE("Simple MajordomoWorker example showing its usage", "[majordomo][major REQUIRE(reply.has_value()); REQUIRE(reply->protocolName == mdp::clientProtocol); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "mmi.openapi"); + REQUIRE(reply->serviceName == "/mmi.openapi"); REQUIRE(reply->data.asString() == "An Addressbook service"); REQUIRE(reply->error == ""); } @@ -149,9 +149,9 @@ TEST_CASE("Simple MajordomoWorker example showing its usage", "[majordomo][major { // Send a request for address with ID 42 auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "addressbook"; + request.serviceName = "/addressbook"; request.clientRequestID = IoBuffer("1"); - request.endpoint = mdp::Message::URI("/addresses?ctx=FAIR.SELECTOR.ALL;contentType=application/json"); + request.topic = mdp::Message::URI("/addressbook?ctx=FAIR.SELECTOR.ALL;contentType=application/json"); request.data = IoBuffer("{ \"id\": 42 }"); client.send(std::move(request)); @@ -160,38 +160,38 @@ TEST_CASE("Simple MajordomoWorker example showing its usage", "[majordomo][major // Assert that the correct reply is received, containing the serialised Address Entry return by TestHandler. REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "addressbook"); + REQUIRE(reply->serviceName == "/addressbook"); REQUIRE(reply->clientRequestID.asString() == "1"); REQUIRE(reply->error == ""); - REQUIRE(reply->endpoint.str() == "/addresses?contentType=application%2Fjson&ctx=FAIR.SELECTOR.ALL"); + REQUIRE(reply->topic == mdp::Message::URI("/addressbook?contentType=application%2Fjson&ctx=FAIR.SELECTOR.ALL")); REQUIRE(reply->data.asString() == "{\n\"name\": \"Santa Claus\",\n\"street\": \"Elf Road\",\n\"streetNumber\": 123,\n\"postalCode\": \"88888\",\n\"city\": \"North Pole\",\n\"isCurrent\": false\n}"); } } TEST_CASE("MajordomoWorker test using raw messages", "[majordomo][majordomoworker][plain_client][rbac]") { using namespace opencmw::majordomo; - Broker broker("TestBroker", testSettings()); + Broker broker("/TestBroker", testSettings()); opencmw::query::registerTypes(TestContext(), broker); - Worker<"addressbook", TestContext, AddressQueryRequest, AddressEntry, rbac, description<"API description">> worker(broker, TestHandler()); + Worker<"/addressbook", TestContext, AddressQueryRequest, AddressEntry, rbac, description<"API description">> worker(broker, TestHandler()); REQUIRE(worker.serviceDescription() == "API description"); RunInThread brokerRun(broker); RunInThread workerRun(worker); - REQUIRE(waitUntilServiceAvailable(broker.context, "addressbook")); + REQUIRE(waitUntilWorkerServiceAvailable(broker.context, worker)); MessageNode client(broker.context); BrokerMessageNode subClient(broker.context, ZMQ_SUB); REQUIRE(client.connect(opencmw::majordomo::INTERNAL_ADDRESS_BROKER)); REQUIRE(subClient.connect(opencmw::majordomo::INTERNAL_ADDRESS_PUBLISHER)); - REQUIRE(subClient.subscribe(mdp::SubscriptionTopic("/newAddress?ctx=FAIR.SELECTOR.C%3D1"))); + REQUIRE(subClient.subscribe("/addressbook?ctx=FAIR.SELECTOR.C%3D1")); { auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "addressbook"; + request.serviceName = "/addressbook"; request.clientRequestID = IoBuffer("1"); - request.endpoint = mdp::Message::URI("/addresses?ctx=FAIR.SELECTOR.ALL;contentType=application/json"); + request.topic = mdp::Message::URI("/addressbook?ctx=FAIR.SELECTOR.ALL;contentType=application/json"); request.data = IoBuffer("{ \"id\": 42 }"); request.rbac = IoBuffer("RBAC=ADMIN,1234"); client.send(std::move(request)); @@ -199,19 +199,19 @@ TEST_CASE("MajordomoWorker test using raw messages", "[majordomo][majordomoworke const auto reply = client.tryReadOne(); REQUIRE(reply.has_value()); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "addressbook"); + REQUIRE(reply->serviceName == "/addressbook"); REQUIRE(reply->clientRequestID.asString() == "1"); REQUIRE(reply->error == ""); - REQUIRE(reply->endpoint.str() == "/addresses?contentType=application%2Fjson&ctx=FAIR.SELECTOR.ALL"); + REQUIRE(reply->topic == mdp::Message::URI("/addressbook?contentType=application%2Fjson&ctx=FAIR.SELECTOR.ALL")); REQUIRE(reply->data.asString() == "{\n\"name\": \"Santa Claus\",\n\"street\": \"Elf Road\",\n\"streetNumber\": 123,\n\"postalCode\": \"88888\",\n\"city\": \"North Pole\",\n\"isCurrent\": false\n}"); } // GET with unknown role or empty role fails for (const auto &role : { "UNKNOWN", "" }) { auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "addressbook"; + request.serviceName = "/addressbook"; request.clientRequestID = IoBuffer("1"); - request.endpoint = mdp::Message::URI("/addresses?ctx=FAIR.SELECTOR.ALL;contentType=application/json"); + request.topic = mdp::Message::URI("/addressbook?ctx=FAIR.SELECTOR.ALL;contentType=application/json"); request.data = IoBuffer("{ \"id\": 42 }"); const auto body = fmt::format("RBAC={},1234", role); request.rbac = IoBuffer(body.data(), body.size()); @@ -220,7 +220,7 @@ TEST_CASE("MajordomoWorker test using raw messages", "[majordomo][majordomoworke const auto reply = client.tryReadOne(); REQUIRE(reply.has_value()); REQUIRE(reply->command == mdp::Command::Final); - REQUIRE(reply->serviceName == "addressbook"); + REQUIRE(reply->serviceName == "/addressbook"); REQUIRE(reply->clientRequestID.asString() == "1"); REQUIRE(reply->error == fmt::format("GET access denied to role '{}'", role)); REQUIRE(reply->data.asString() == ""); @@ -229,9 +229,9 @@ TEST_CASE("MajordomoWorker test using raw messages", "[majordomo][majordomoworke // request non-existing entry { auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "addressbook"; + request.serviceName = "/addressbook"; request.clientRequestID = IoBuffer("2"); - request.endpoint = mdp::Message::URI("/addresses?ctx=FAIR.SELECTOR.ALL;contentType=application/json"); + request.topic = mdp::Message::URI("/addressbook?ctx=FAIR.SELECTOR.ALL;contentType=application/json"); request.data = IoBuffer("{ \"id\": 4711 }"); request.rbac = IoBuffer("RBAC=ADMIN,1234"); client.send(std::move(request)); @@ -249,9 +249,9 @@ TEST_CASE("MajordomoWorker test using raw messages", "[majordomo][majordomoworke // send empty request { auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "addressbook"; + request.serviceName = "/addressbook"; request.clientRequestID = IoBuffer("3"); - request.endpoint = mdp::Message::URI("/addresses?ctx=FAIR.SELECTOR.ALL;contentType=application/json"); + request.topic = mdp::Message::URI("/addressbook?ctx=FAIR.SELECTOR.ALL;contentType=application/json"); request.data = IoBuffer(""); request.rbac = IoBuffer("RBAC=ADMIN,1234"); client.send(std::move(request)); @@ -269,9 +269,9 @@ TEST_CASE("MajordomoWorker test using raw messages", "[majordomo][majordomoworke // send request with invalid JSON { auto request = createClientMessage(mdp::Command::Get); - request.serviceName = "addressbook"; + request.serviceName = "/addressbook"; request.clientRequestID = IoBuffer("4"); - request.endpoint = mdp::Message::URI("/addresses?ctx=FAIR.SELECTOR.ALL;contentType=application/json"); + request.topic = mdp::Message::URI("/addressbook?ctx=FAIR.SELECTOR.ALL;contentType=application/json"); request.data = IoBuffer("{ \"id\": 42 ]"); request.rbac = IoBuffer("RBAC=ADMIN,1234"); client.send(std::move(request)); @@ -296,7 +296,7 @@ TEST_CASE("MajordomoWorker test using raw messages", "[majordomo][majordomoworke .city = "Sahara", .isCurrent = true }; - REQUIRE(worker.notify("/newAddress", TestContext{ .ctx = opencmw::TimingCtx(-1, 1, -1, -1), .contentType = opencmw::MIME::JSON }, entry)); + REQUIRE(worker.notify(TestContext{ .ctx = opencmw::TimingCtx(-1, 1, -1, -1), .contentType = opencmw::MIME::JSON }, entry)); } { @@ -309,19 +309,19 @@ TEST_CASE("MajordomoWorker test using raw messages", "[majordomo][majordomoworke .city = "Easter Island", .isCurrent = true }; - REQUIRE(worker.notify("/newAddress", TestContext{ .ctx = opencmw::TimingCtx(1), .contentType = opencmw::MIME::JSON }, entry)); - REQUIRE(worker.activeSubscriptions().size() == 1); - REQUIRE(worker.activeSubscriptions().begin()->service() == "addressbook"); - REQUIRE(worker.activeSubscriptions().begin()->path() == "/newAddress"); - REQUIRE(worker.activeSubscriptions().begin()->params() == SubscriptionTopic::map{ { "ctx", "FAIR.SELECTOR.C=1" } }); + REQUIRE(worker.notify(TestContext{ .ctx = opencmw::TimingCtx(1), .contentType = opencmw::MIME::JSON }, entry)); + const auto activeSubscriptions = worker.activeSubscriptions(); + REQUIRE(activeSubscriptions.size() == 1); + REQUIRE(activeSubscriptions.begin()->service() == "/addressbook"); + REQUIRE(activeSubscriptions.begin()->params() == Topic::Params{ { "ctx", "FAIR.SELECTOR.C=1" } }); } { const auto notify = subClient.tryReadOne(); REQUIRE(notify.has_value()); REQUIRE(notify->command == mdp::Command::Final); - REQUIRE(notify->sourceId == "/newAddress?ctx=FAIR.SELECTOR.C%3D1"); - REQUIRE(notify->endpoint.str() == "/newAddress"); + REQUIRE(notify->sourceId == "/addressbook?ctx=FAIR.SELECTOR.C%3D1"); + REQUIRE(notify->topic == mdp::Message::URI("/addressbook?contentType=application%2Fjson&ctx=FAIR.SELECTOR.C%3D1")); REQUIRE(notify->error.empty()); REQUIRE(notify->data.asString() == "{\n\"name\": \"Easter Bunny\",\n\"street\": \"Carrot Road\",\n\"streetNumber\": 123,\n\"postalCode\": \"88888\",\n\"city\": \"Easter Island\",\n\"isCurrent\": true\n}"); } diff --git a/src/majordomo/test/subscriptionmatcher_tests.cpp b/src/majordomo/test/subscriptionmatcher_tests.cpp index 1f19ceb6..66f9658e 100644 --- a/src/majordomo/test/subscriptionmatcher_tests.cpp +++ b/src/majordomo/test/subscriptionmatcher_tests.cpp @@ -10,35 +10,27 @@ #include using opencmw::majordomo::SubscriptionMatcher; -using opencmw::mdp::SubscriptionTopic; +using opencmw::mdp::Topic; -struct SubscriptionUriMatcher : SubscriptionMatcher { +struct SubscriptionStringMatcher : SubscriptionMatcher { bool operator()(const auto ¬ified, const auto &subscriber) const { - return SubscriptionMatcher::operator()(SubscriptionTopic::fromURI(notified), SubscriptionTopic::fromURI(subscriber)); + return SubscriptionMatcher::operator()(Topic::fromString(notified), Topic::fromString(subscriber)); } }; TEST_CASE("Test path-only topics", "[subscription_matcher][path_only]") { - using URI = opencmw::URI; - SubscriptionUriMatcher matcher; - - REQUIRE(matcher(URI("/property"), URI("/property"))); - REQUIRE_FALSE(matcher(URI("/property/A"), URI("/property"))); - REQUIRE(matcher(URI("/property"), URI(""))); - REQUIRE(matcher(URI("/property/A"), URI(""))); - REQUIRE(matcher(URI("/property/A"), URI("/property*"))); - REQUIRE(matcher(URI("/property/A/B"), URI("/property*"))); - REQUIRE_FALSE(matcher(URI("/property"), URI("/property2"))); - REQUIRE_FALSE(matcher(URI("/property"), URI("/property2"))); - REQUIRE(matcher(URI("/property?testQuery"), URI("/property"))); - REQUIRE(matcher(URI("/property?testQuery"), URI("/property*"))); + SubscriptionStringMatcher matcher; + + REQUIRE(matcher("/service", "/service")); + REQUIRE_FALSE(matcher("/service/A", "/service")); + REQUIRE_FALSE(matcher("/service", "/service2")); + REQUIRE_FALSE(matcher("/service", "/service2")); + REQUIRE(matcher("/service?testQuery", "/service")); // no filter configuration -> ignores query and matches only path - REQUIRE(matcher(URI("/property?testQuery"), URI("/property?testQuery"))); - REQUIRE(matcher(URI("/property?testQuery"), URI("/property*?testQuery"))); - REQUIRE(matcher(URI("/property/A?testQuery"), URI("/property*?testQuery"))); - REQUIRE(matcher(URI("/property"), URI("/property?testQuery"))); - REQUIRE(matcher(URI("/property"), URI("/property*?testQuery"))); + REQUIRE(matcher("/service?testQuery", "/service?testQuery")); + REQUIRE(matcher("/service/A?testQuery", "/service/A?testQuery")); + REQUIRE(matcher("/service", "/service?testQuery")); } struct Int { @@ -67,46 +59,46 @@ TEST_CASE("Test path and query", "[subscription_matcher][path_and_query]") { using TestFilter2 = DomainFilter; using TestFilter3 = DomainFilter; using TestFilter4 = opencmw::NumberFilter; - using URI = opencmw::URI; - SubscriptionUriMatcher matcher; + SubscriptionStringMatcher matcher; matcher.addFilter("testKey1"); matcher.addFilter("testKey2"); matcher.addFilter("testKey3"); matcher.addFilter("testKey4"); - - REQUIRE_FALSE(matcher(URI("/property1?testKey1"), URI("/property2?testKey1"))); - REQUIRE(matcher(URI("/property?testKey1"), URI("/property?testKey1"))); - REQUIRE(matcher(URI("/property?testKey1&testKey2"), URI("/property?testKey1&testKey2"))); - REQUIRE(matcher(URI("/property?testKey1&testKey2"), URI("/property?testKey2&testKey1"))); - REQUIRE(matcher(URI("/property?testKey1=42&testKey2=24"), URI("/property?testKey2=24&testKey1=42"))); - REQUIRE_FALSE(matcher(URI("/property?testKey1=41"), URI("/property*?testKey1=4711"))); - REQUIRE(matcher(URI("/property/A?testKey1=41"), URI("/property*?testKey1=41"))); - REQUIRE(matcher(URI("/property"), URI("/property?testQuery"))); // ignore unknown ctx filter on subscription side - REQUIRE(matcher(URI("/property"), URI("/property*?testQuery"))); // ignore unknown ctx filter on subscription side - REQUIRE(matcher(URI("/property?testKey1"), URI("/property*"))); - REQUIRE(matcher(URI("/property?testKey1"), URI("/property?TestKey1"))); // N.B. key is case sensitive - REQUIRE_FALSE(matcher(URI("/property?testKey1"), URI("/property?testKey1=42"))); - REQUIRE_FALSE(matcher(URI("/property"), URI("/property?testKey1=42"))); - REQUIRE(matcher(URI("/property?testKey3=abc"), URI("/property?testKey3=bcd"))); - REQUIRE_FALSE(matcher(URI("/property?testKey3=bcd"), URI("/property?testKey3=abc"))); - REQUIRE(matcher(URI("/property?testKey4=3.33"), URI("/property?testKey4=3.33"))); - REQUIRE_FALSE(matcher(URI("/property?testKey4=3.33"), URI("/property?testKey4=3.44"))); + matcher.addFilter("testFlag1"); + matcher.addFilter("testFlag2"); + + REQUIRE_FALSE(matcher("/service1?testKey1", "/service2?testKey1")); + REQUIRE(matcher("/service?testKey1", "/service?testKey1")); + REQUIRE(matcher("/service?testKey1&testKey2", "/service?testKey1&testKey2")); + REQUIRE(matcher("/service?testKey1&testKey2", "/service?testKey2&testKey1")); + REQUIRE(matcher("/service?testKey1=42&testKey2=24", "/service?testKey2=24&testKey1=42")); + REQUIRE_FALSE(matcher("/service?testKey1=41", "/service?testKey1=4711")); + REQUIRE(matcher("/service/A?testKey1=41", "/service/A?testKey1=41")); + REQUIRE(matcher("/service", "/service?testQuery")); // ignore unknown ctx filter on subscription side + REQUIRE(matcher("/service?testKey1", "/service")); + REQUIRE(matcher("/service?testKey1", "/service?TestKey1")); // N.B. key is case sensitive + REQUIRE_FALSE(matcher("/service?testKey1", "/service?testKey1=42")); + REQUIRE_FALSE(matcher("/service", "/service?testKey1=42")); + REQUIRE(matcher("/service?testKey3=abc", "/service?testKey3=bcd")); + REQUIRE_FALSE(matcher("/service?testKey3=bcd", "/service?testKey3=abc")); + REQUIRE(matcher("/service?testKey4=3.33", "/service?testKey4=3.33")); + REQUIRE_FALSE(matcher("/service?testKey4=3.33", "/service?testKey4=3.44")); + REQUIRE_FALSE(matcher("/service?testFlag1", "/service?testFlag1&testFlag2")); } TEST_CASE("Test timing and context type matching", "[subscription_matcher]") { - using URI = opencmw::URI; - SubscriptionUriMatcher matcher; + SubscriptionStringMatcher matcher; matcher.addFilter("ctx"); matcher.addFilter("contentType"); - REQUIRE_FALSE(matcher(URI("/property?ctx=FAIR.SELECTOR.ALL"), URI("/property?ctx=FAIR.SELECTOR.C=2"))); - REQUIRE(matcher(URI("/property?ctx=FAIR.SELECTOR.C=2"), URI("/property?ctx=FAIR.SELECTOR.ALL"))); - REQUIRE(matcher(URI("/property?ctx=FAIR.SELECTOR.C=2"), URI("/property?ctx=FAIR.SELECTOR.C=2"))); - REQUIRE(matcher(URI("/property?ctx=FAIR.SELECTOR.C=2:P=1"), URI("/property?ctx=FAIR.SELECTOR.C=2"))); - REQUIRE(matcher(URI("/property?ctx=FAIR.SELECTOR.C=2:P=1"), URI("/property?ctx=FAIR.SELECTOR.C=2:P=1"))); - REQUIRE_FALSE(matcher(URI("/property?ctx=FAIR.SELECTOR.C=2"), URI("/property?ctx=FAIR.SELECTOR.C=2:P=1"))); // notify not specific enough (missing 'P=1') - REQUIRE_FALSE(matcher(URI("/property?ctx=FAIR.SELECTOR.ALL&contentType=text/html"), URI("/property?ctx=FAIR.SELECTOR.C=2&contentType=text/html"))); - REQUIRE_FALSE(matcher(URI("/property?ctx=FAIR.SELECTOR.ALL"), URI("/property?ctx=FAIR.SELECTOR.C=2&contentType=text/html"))); + REQUIRE_FALSE(matcher("/service?ctx=FAIR.SELECTOR.ALL", "/service?ctx=FAIR.SELECTOR.C=2")); + REQUIRE(matcher("/service?ctx=FAIR.SELECTOR.C=2", "/service?ctx=FAIR.SELECTOR.ALL")); + REQUIRE(matcher("/service?ctx=FAIR.SELECTOR.C=2", "/service?ctx=FAIR.SELECTOR.C=2")); + REQUIRE(matcher("/service?ctx=FAIR.SELECTOR.C=2:P=1", "/service?ctx=FAIR.SELECTOR.C=2")); + REQUIRE(matcher("/service?ctx=FAIR.SELECTOR.C=2:P=1", "/service?ctx=FAIR.SELECTOR.C=2:P=1")); + REQUIRE_FALSE(matcher("/service?ctx=FAIR.SELECTOR.C=2", "/service?ctx=FAIR.SELECTOR.C=2:P=1")); // notify not specific enough (missing 'P=1') + REQUIRE_FALSE(matcher("/service?ctx=FAIR.SELECTOR.ALL&contentType=text/html", "/service?ctx=FAIR.SELECTOR.C=2&contentType=text/html")); + REQUIRE_FALSE(matcher("/service?ctx=FAIR.SELECTOR.ALL", "/service?ctx=FAIR.SELECTOR.C=2&contentType=text/html")); } struct MatcherTest { @@ -116,14 +108,13 @@ struct MatcherTest { ENABLE_REFLECTION_FOR(MatcherTest, ctx, contentType) TEST_CASE("Register filters", "[QuerySerialiser][register_filters]") { - using URI = opencmw::URI; - SubscriptionUriMatcher matcher; + SubscriptionStringMatcher matcher; opencmw::query::registerTypes(MatcherTest(), matcher); // subset of the subscriptionfilter_tests - REQUIRE_FALSE(matcher(URI("/property?ctx=FAIR.SELECTOR.ALL"), URI("/property?ctx=FAIR.SELECTOR.C=2"))); - REQUIRE(matcher(URI("/property?ctx=FAIR.SELECTOR.C=2"), URI("/property?ctx=FAIR.SELECTOR.ALL"))); - REQUIRE(matcher(URI("/property?ctx=FAIR.SELECTOR.C=2"), URI("/property?ctx=FAIR.SELECTOR.C=2"))); - REQUIRE_FALSE(matcher(URI("/property?ctx=FAIR.SELECTOR.ALL&contentType=text/html"), URI("/property?ctx=FAIR.SELECTOR.C=2&contentType=text/html"))); - REQUIRE_FALSE(matcher(URI("/property?ctx=FAIR.SELECTOR.ALL"), URI("/property?ctx=FAIR.SELECTOR.C=2&contentType=text/html"))); + REQUIRE_FALSE(matcher("/service?ctx=FAIR.SELECTOR.ALL", "/service?ctx=FAIR.SELECTOR.C=2")); + REQUIRE(matcher("/service?ctx=FAIR.SELECTOR.C=2", "/service?ctx=FAIR.SELECTOR.ALL")); + REQUIRE(matcher("/service?ctx=FAIR.SELECTOR.C=2", "/service?ctx=FAIR.SELECTOR.C=2")); + REQUIRE_FALSE(matcher("/service?ctx=FAIR.SELECTOR.ALL&contentType=text/html", "/service?ctx=FAIR.SELECTOR.C=2&contentType=text/html")); + REQUIRE_FALSE(matcher("/service?ctx=FAIR.SELECTOR.ALL", "/service?ctx=FAIR.SELECTOR.C=2&contentType=text/html")); } diff --git a/src/majordomo/test/testapp.cpp b/src/majordomo/test/testapp.cpp index 5e1f5f60..c23ee049 100644 --- a/src/majordomo/test/testapp.cpp +++ b/src/majordomo/test/testapp.cpp @@ -73,7 +73,7 @@ int main(int argc, char **argv) { using opencmw::majordomo::Broker; using opencmw::majordomo::Settings; - static constexpr auto propertyStoreService = units::basic_fixed_string("property_store"); + static constexpr auto propertyStoreService = units::basic_fixed_string("/property_store"); if (argc < 2) { std::cerr << "Usage: majordomo_testapp \n\n" @@ -102,7 +102,7 @@ int main(int argc, char **argv) { const auto pubEndpoint = argc > 3 ? std::optional(parseUriOrExit(argv[3])) : std::optional{}; Context context; - auto broker = Broker("test_broker", testSettings()); + auto broker = Broker("/test_broker", testSettings()); const auto routerAddress = broker.bind(routerEndpoint); if (!routerAddress) { std::cerr << fmt::format("Could not bind to '{}'\n", routerEndpoint); @@ -130,11 +130,11 @@ int main(int argc, char **argv) { auto brokerThread = std::jthread([&broker] { broker.run(); - }); + }); auto workerThread = std::jthread([&worker] { worker.run(); - }); + }); brokerThread.join(); workerThread.join(); diff --git a/src/services/include/services/dns.hpp b/src/services/include/services/dns.hpp index 9ae1e0a7..5174e589 100644 --- a/src/services/include/services/dns.hpp +++ b/src/services/include/services/dns.hpp @@ -14,7 +14,7 @@ namespace opencmw::service::dns { -using DnsWorkerType = majordomo::Worker<"dns", Context, FlatEntryList, FlatEntryList, majordomo::description<"Register and Query Signals">>; +using DnsWorkerType = majordomo::Worker<"/dns", Context, FlatEntryList, FlatEntryList, majordomo::description<"Register and Query Signals">>; class DnsHandler { protected: diff --git a/src/services/test/dns_tests.cpp b/src/services/test/dns_tests.cpp index 28fb91bf..d445c3b3 100644 --- a/src/services/test/dns_tests.cpp +++ b/src/services/test/dns_tests.cpp @@ -193,7 +193,7 @@ TEST_CASE("data storage - Renewing Entries") { TEST_CASE("run services", "[DNS]") { FileDeleter fd; - majordomo::Broker<> broker{ "Broker", {} }; + majordomo::Broker<> broker{ "/Broker", {} }; std::string rootPath{ "./" }; auto fs = cmrc::assets::get_filesystem(); majordomo::RestBackend rest_backend{ broker, fs }; @@ -206,7 +206,7 @@ TEST_CASE("run services", "[DNS]") { TEST_CASE("client", "[DNS]") { FileDeleter fd; - majordomo::Broker<> broker{ "Broker", {} }; + majordomo::Broker<> broker{ "/Broker", {} }; std::string rootPath{ "./" }; auto fs = cmrc::assets::get_filesystem(); majordomo::RestBackend rest_backend{ broker, fs }; @@ -250,7 +250,7 @@ TEST_CASE("client", "[DNS]") { TEST_CASE("query", "[DNS]") { FileDeleter fd; - majordomo::Broker<> broker{ "Broker", {} }; + majordomo::Broker<> broker{ "/Broker", {} }; auto fs = cmrc::assets::get_filesystem(); majordomo::RestBackend rest_backend{ broker, fs }; DnsWorkerType dnsWorker{ broker, DnsHandler{} }; @@ -259,7 +259,7 @@ TEST_CASE("query", "[DNS]") { RunInThread brokerThread(broker); RunInThread dnsThread(dnsWorker); - REQUIRE(waitUntilServiceAvailable(broker.context, "dns")); + REQUIRE(waitUntilWorkerServiceAvailable(broker.context, dnsWorker)); SECTION("query") { auto services = querySignals(); diff --git a/src/zmq/include/zmq/ZmqUtils.hpp b/src/zmq/include/zmq/ZmqUtils.hpp index 0bb92146..d48724fb 100644 --- a/src/zmq/include/zmq/ZmqUtils.hpp +++ b/src/zmq/include/zmq/ZmqUtils.hpp @@ -248,7 +248,7 @@ class MessageFrame { } } - MessageFrame(const MessageFrame &other) = delete; + MessageFrame(const MessageFrame &other) = delete; MessageFrame &operator=(const MessageFrame &other) = delete; MessageFrame(MessageFrame &&other) noexcept @@ -369,7 +369,7 @@ template zmsg.frame(ZmqMessage::Frame::Command) = MessageFrame::fromStaticData(commandStrings[static_cast(message.command)]); zmsg.frame(ZmqMessage::Frame::ServiceName) = MessageFrame{ std::move(message.serviceName) }; zmsg.frame(ZmqMessage::Frame::ClientRequestId) = MessageFrame{ std::move(message.clientRequestID) }; - zmsg.frame(ZmqMessage::Frame::Topic) = MessageFrame{ std::string(message.endpoint.str()) }; + zmsg.frame(ZmqMessage::Frame::Topic) = MessageFrame{ std::string(message.topic.str()) }; zmsg.frame(ZmqMessage::Frame::Body) = MessageFrame{ std::move(message.data) }; zmsg.frame(ZmqMessage::Frame::Error) = MessageFrame{ std::move(message.error) }; zmsg.frame(ZmqMessage::Frame::RBAC) = MessageFrame{ std::move(message.rbac) }; @@ -432,7 +432,7 @@ template msg.command = static_cast(commandStr[0]); msg.serviceName = std::string(zmsg.frame(ZmqMessage::Frame::ServiceName).data()); msg.clientRequestID = IoBuffer(clientRequestId.data(), clientRequestId.size()); - msg.endpoint = mdp::Message::URI(std::string(zmsg.frame(ZmqMessage::Frame::Topic).data())); + msg.topic = mdp::Message::URI(std::string(zmsg.frame(ZmqMessage::Frame::Topic).data())); msg.data = IoBuffer(data.data(), data.size()); msg.error = std::string(zmsg.frame(ZmqMessage::Frame::Error).data()); msg.rbac = IoBuffer(rbac.data(), rbac.size());