Version in base suite: 10.0.2+dfsg1-2 Base version: tango_10.0.2+dfsg1-2 Target version: tango_10.0.2+dfsg1-2+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/t/tango/tango_10.0.2+dfsg1-2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/t/tango/tango_10.0.2+dfsg1-2+deb13u1.dsc changelog | 14 gitlab-ci.yml | 4 patches/0001-EventConsumerKeepAliveThread-send-actual-IDL-when-ex.patch | 61 + patches/0001-catch2_event_old_client-Add-test-to-catch-duplicate-.patch | 422 ++++++++++ patches/0002-Catch2-tests-add-Event-re-subscribes-with-the-same-I.patch | 213 +++++ patches/0002-DServer-zmq_event_subscription_changed-Map-client-id.patch | 113 ++ patches/0003-push_att_conf_events-Use-event-data-to-determine-ver.patch | 42 patches/0004-catch2_event_reconnection-Use-log-from-new-mapping-f.patch | 40 patches/0005-Attribute-set_upd_properties-Handle-no-database-case.patch | 106 ++ patches/series | 9 10 files changed, 1024 insertions(+) diff -Nru tango-10.0.2+dfsg1/debian/changelog tango-10.0.2+dfsg1/debian/changelog --- tango-10.0.2+dfsg1/debian/changelog 2025-05-15 15:27:43.000000000 +0000 +++ tango-10.0.2+dfsg1/debian/changelog 2025-10-28 14:49:21.000000000 +0000 @@ -1,3 +1,17 @@ +tango (10.0.2+dfsg1-2+deb13u1) trixie; urgency=medium + + * Team upload. + * Fix broken communication between major versions: libtango9 cannot receive + events from libtango10 (Closes: #1118207) + * d/gitlab-ci.yml (Salsa CI): + - Set RELEASE to trixie in d/gitlab-ci.yml to explicitly trigger + trixie-based pipelines. + - Disable the reprotest job. Releases older than unstable are not very + well supported by the Salsa CI's reprotest job, and this failing without + a good reason. + + -- Santiago Ruano Rincón Tue, 28 Oct 2025 11:49:21 -0300 + tango (10.0.2+dfsg1-2) unstable; urgency=low * Team upload. diff -Nru tango-10.0.2+dfsg1/debian/gitlab-ci.yml tango-10.0.2+dfsg1/debian/gitlab-ci.yml --- tango-10.0.2+dfsg1/debian/gitlab-ci.yml 2025-02-10 21:07:39.000000000 +0000 +++ tango-10.0.2+dfsg1/debian/gitlab-ci.yml 2025-10-28 14:46:18.000000000 +0000 @@ -1,3 +1,7 @@ include: - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml + +variables: + RELEASE: 'trixie' + SALSA_CI_DISABLE_REPROTEST: 'true' diff -Nru tango-10.0.2+dfsg1/debian/patches/0001-EventConsumerKeepAliveThread-send-actual-IDL-when-ex.patch tango-10.0.2+dfsg1/debian/patches/0001-EventConsumerKeepAliveThread-send-actual-IDL-when-ex.patch --- tango-10.0.2+dfsg1/debian/patches/0001-EventConsumerKeepAliveThread-send-actual-IDL-when-ex.patch 1970-01-01 00:00:00.000000000 +0000 +++ tango-10.0.2+dfsg1/debian/patches/0001-EventConsumerKeepAliveThread-send-actual-IDL-when-ex.patch 2025-10-27 13:47:24.000000000 +0000 @@ -0,0 +1,61 @@ +From f4ebed11ff59e7cca3ceced373e6f008959f669f Mon Sep 17 00:00:00 2001 +From: matveyev +Date: Mon, 11 Aug 2025 16:23:38 +0200 +Subject: [PATCH 1/2] EventConsumerKeepAliveThread: send actual IDL when + execute ZmqEventSubscriptionChange + +When we executed ZmqEventSubscriptionChange we sent as client IDL 0, which means for the server, "Guess my IDL", as it was introduced in 01ffb8f4. + +After IDL6 release, due to IDL6 devices subscribing to ...idl5_... events, they were re-subscribed as IDL5 devices. + +Now we send the actual maximum IDL, supported by the client as we do at the subscription process +--- + src/client/eventkeepalive.cpp | 18 +++++++++++++++--- + 1 file changed, 15 insertions(+), 3 deletions(-) + +diff --git a/lib/cpp/src/client/eventkeepalive.cpp b/lib/cpp/src/client/eventkeepalive.cpp +index 487cdddd3..824adb2e3 100644 +--- a/lib/cpp/src/client/eventkeepalive.cpp ++++ b/lib/cpp/src/client/eventkeepalive.cpp +@@ -200,7 +200,11 @@ bool EventConsumerKeepAliveThread::reconnect_to_zmq_channel(const EvChanIte &ipo + subscriber_info.push_back(epos->second.obj_name); + subscriber_info.emplace_back("subscribe"); + subscriber_info.push_back(epos->second.event_name); +- subscriber_info.emplace_back("0"); ++ ++ // maximum IDL version which client supports ++ std::stringstream ss; ++ ss << DevVersion; ++ subscriber_info.push_back(ss.str()); + + subscriber_in << subscriber_info; + +@@ -1039,7 +1043,12 @@ void EventConsumerKeepAliveThread::confirm_subscription(ZmqEventConsumer *event_ + subscriber_info.push_back(cmd_params[(loop * 3) + 1]); + subscriber_info.emplace_back("subscribe"); + subscriber_info.push_back(cmd_params[(loop * 3) + 2]); +- subscriber_info.emplace_back("0"); ++ ++ // maximum IDL version which client supports ++ std::stringstream ss; ++ ss << DevVersion; ++ subscriber_info.push_back(ss.str()); ++ + subscriber_in << subscriber_info; + + try +@@ -1470,7 +1479,10 @@ void EventConsumerKeepAliveThread::re_subscribe_after_reconnect( + subscriber_info.push_back(epos->second.event_name); + if(ipos->second.channel_type == ZMQ) + { +- subscriber_info.emplace_back("0"); ++ // maximum IDL version which client supports ++ std::stringstream ss; ++ ss << DevVersion; ++ subscriber_info.push_back(ss.str()); + } + subscriber_in << subscriber_info; + +-- +2.39.5 + diff -Nru tango-10.0.2+dfsg1/debian/patches/0001-catch2_event_old_client-Add-test-to-catch-duplicate-.patch tango-10.0.2+dfsg1/debian/patches/0001-catch2_event_old_client-Add-test-to-catch-duplicate-.patch --- tango-10.0.2+dfsg1/debian/patches/0001-catch2_event_old_client-Add-test-to-catch-duplicate-.patch 1970-01-01 00:00:00.000000000 +0000 +++ tango-10.0.2+dfsg1/debian/patches/0001-catch2_event_old_client-Add-test-to-catch-duplicate-.patch 2025-10-27 13:47:24.000000000 +0000 @@ -0,0 +1,422 @@ +From 92e8c8ec490628d75052c6d8029e7107148de6d4 Mon Sep 17 00:00:00 2001 +From: Thomas Ives +Date: Thu, 28 Aug 2025 15:06:03 +0100 +Subject: [PATCH 1/5] catch2_event_old_client: Add test to catch duplicate + event issue + +This test reproduces the issue described in #1521. + +There are three test cases added here which cover different places where +we have the duplicate events issue. + +The first test case covers the case where events are being pushed +manually both with and without change detection. This finds the +duplicate event issue in the +`ZmqEventSupplier::detect_and_push__event` methods along with the +`ZmqEventSupplier::push_event_loop` method. + +The second test case covers events sent by the polling loop, this also +triggers the issue with the +`ZmqEventSupplier::detect_and_push__event` and is included mostly so +that we can check periodic events work. + +The final test case covers attribute configuration events, which +triggers the duplicate event issue with the +`EventSupplier::push_att_config_event` method. +--- + tests/CMakeLists.txt | 1 + + tests/catch2_event_old_client.cpp | 374 ++++++++++++++++++++++++++++++ + 2 files changed, 375 insertions(+) + create mode 100644 tests/catch2_event_old_client.cpp + +Index: tango/lib/cpp/tests/catch2_event_old_client.cpp +=================================================================== +--- /dev/null ++++ tango/lib/cpp/tests/catch2_event_old_client.cpp +@@ -0,0 +1,374 @@ ++#include "catch2_common.h" ++ ++#include "tango/internal/utils.h" ++ ++#include ++ ++using CallbackMockType = TangoTest::CallbackMock; ++ ++constexpr const char *k_attr_name = "basicAttr"; ++constexpr const char *k_no_detect_attr_name = "noDetectAttr"; ++constexpr static Tango::DevLong k_polling_period = TANGO_TEST_CATCH2_DEFAULT_POLL_PERIOD; ++ ++void simulate_old_clients_subscription(Tango::DeviceProxy *admin, ++ Tango::EventType ev_type, ++ int idlver, ++ std::string_view device_name, ++ std::string_view attr_name) ++{ ++ // Here we trick the device server into thinking that an old client has ++ // subscribed by calling ZmqEventSubscriptionChange. The order here ++ // matters, we want to subscribe to idl5 first, otherwise the duplicates ++ // will get discarded on the client side. ++ for(int oldver = idlver - 1; oldver >= 4; --oldver) ++ { ++ std::vector request; ++ request.reserve(5); ++ request.emplace_back(device_name); ++ request.emplace_back(attr_name); ++ request.emplace_back("subscribe"); ++ { ++ std::stringstream ss; ++ if(oldver >= 5) ++ { ++ ss << "idl5_"; ++ } ++ ss << Tango::EventName[ev_type]; ++ request.emplace_back(ss.str()); ++ } ++ { ++ std::stringstream ss; ++ ss << oldver; ++ request.emplace_back(ss.str()); ++ } ++ Tango::DeviceData din; ++ din << request; ++ ++ REQUIRE_NOTHROW(admin->command_inout("ZmqEventSubscriptionChange", din)); ++ } ++} ++ ++template ++class DuplicatePushedEvent : public Base ++{ ++ public: ++ using Base::Base; ++ ++ ~DuplicatePushedEvent() override { } ++ ++ void init_device() override ++ { ++ value = 0; ++ } ++ ++ void read_attr(Tango::Attribute &att) override ++ { ++ att.set_value(&value); ++ } ++ ++ void push_change() ++ { ++ value += 1; ++ this->push_change_event(k_attr_name, &value); ++ this->push_change_event(k_no_detect_attr_name, &value); ++ } ++ ++ void push_archive() ++ { ++ value += 1; ++ this->push_archive_event(k_attr_name, &value); ++ this->push_archive_event(k_no_detect_attr_name, &value); ++ } ++ ++ void push_user() ++ { ++ value += 1; ++ std::vector filter_names; ++ std::vector filter_values; ++ this->push_event(k_attr_name, filter_names, filter_values, &value); ++ this->push_event(k_no_detect_attr_name, filter_names, filter_values, &value); ++ } ++ ++ static void attribute_factory(std::vector &attrs) ++ { ++ using Attr = TangoTest::AutoAttr<&DuplicatePushedEvent::read_attr>; ++ Tango::UserDefaultAttrProp props; ++ props.set_abs_change("1"); ++ props.set_archive_abs_change("1"); ++ ++ auto attr = new Attr(k_attr_name, Tango::DEV_LONG); ++ attr->set_default_properties(props); ++ attr->set_change_event(true, true); ++ attr->set_archive_event(true, true); ++ attrs.push_back(attr); ++ ++ auto no_detect_attr = new Attr(k_no_detect_attr_name, Tango::DEV_LONG); ++ no_detect_attr->set_change_event(true, false); ++ no_detect_attr->set_archive_event(true, false); ++ attrs.push_back(no_detect_attr); ++ } ++ ++ static void command_factory(std::vector &cmds) ++ { ++ cmds.push_back(new TangoTest::AutoCommand<&DuplicatePushedEvent::push_change>("push_change")); ++ cmds.push_back(new TangoTest::AutoCommand<&DuplicatePushedEvent::push_archive>("push_archive")); ++ cmds.push_back(new TangoTest::AutoCommand<&DuplicatePushedEvent::push_user>("push_user_event")); ++ } ++ ++ private: ++ Tango::DevLong value{0}; ++}; ++ ++TANGO_TEST_AUTO_DEV_TMPL_INSTANTIATE(DuplicatePushedEvent, 6) ++ ++SCENARIO("Manually pushed events are not duplicated when old clients are connected") ++{ ++ int idlver = GENERATE(TangoTest::idlversion(6)); ++ GIVEN("a device proxy to a simple IDLv" << idlver << " device") ++ { ++ TangoTest::Context ctx{"duplicate_event", "DuplicatePushedEvent", idlver}; ++ std::shared_ptr device = ctx.get_proxy(); ++ ++ REQUIRE(idlver == device->get_idl_version()); ++ ++ std::string attr_name = GENERATE(k_attr_name, k_no_detect_attr_name); ++ Tango::EventType ev_type = GENERATE(Tango::CHANGE_EVENT, Tango::ARCHIVE_EVENT, Tango::USER_EVENT); ++ ++ WHEN("some old clients subscribe to " << Tango::EventName[ev_type] << " events for " << attr_name) ++ { ++ std::unique_ptr admin = ctx.get_admin_proxy(); ++ ++ simulate_old_clients_subscription(admin.get(), ev_type, idlver, device->dev_name(), attr_name); ++ ++ AND_WHEN("we subscribe to " << Tango::EventName[ev_type] << " events for " << attr_name) ++ { ++ CallbackMockType callback; ++ ++ device->subscribe_event(attr_name, ev_type, &callback); ++ ++ auto maybe_initial_event = callback.pop_next_event(); ++ REQUIRE(maybe_initial_event != std::nullopt); ++ maybe_initial_event = callback.pop_next_event(); ++ ++ AND_WHEN("the server sends an single event") ++ { ++ { ++ std::stringstream ss; ++ ss << "push_" << Tango::EventName[ev_type]; ++ REQUIRE_NOTHROW(device->command_inout(ss.str())); ++ } ++ ++ THEN("we only receive one event") ++ { ++ using TangoTest::AnyLikeContains; ++ ++ auto maybe_event = callback.pop_next_event(); ++ REQUIRE(maybe_event != std::nullopt); ++ REQUIRE(maybe_event->event == Tango::EventName[ev_type]); ++ REQUIRE_THAT(*maybe_event->attr_value, AnyLikeContains(static_cast(1))); ++ ++ maybe_event = callback.pop_next_event(); ++ REQUIRE(maybe_event == std::nullopt); ++ } ++ } ++ } ++ } ++ } ++} ++ ++template ++class DuplicatePollingEvent : public Base ++{ ++ public: ++ using Base::Base; ++ ++ ~DuplicatePollingEvent() override { } ++ ++ void init_device() override ++ { ++ value = 0; ++ } ++ ++ void read_attr(Tango::Attribute &att) override ++ { ++ value += 1; ++ att.set_value(&value); ++ } ++ ++ static void attribute_factory(std::vector &attrs) ++ { ++ using Attr = TangoTest::AutoAttr<&DuplicatePollingEvent::read_attr>; ++ Tango::UserDefaultAttrProp props; ++ { ++ std::stringstream ss; ++ ss << k_polling_period; ++ props.set_period(ss.str().c_str()); ++ } ++ props.set_abs_change("1"); ++ props.set_archive_abs_change("1"); ++ ++ auto attr = new Attr(k_attr_name, Tango::DEV_LONG); ++ attr->set_default_properties(props); ++ attr->set_polling_period(k_polling_period); ++ attrs.push_back(attr); ++ } ++ ++ private: ++ Tango::DevLong value{0}; ++}; ++ ++TANGO_TEST_AUTO_DEV_TMPL_INSTANTIATE(DuplicatePollingEvent, 6) ++ ++SCENARIO("Periodic events are not duplicated when old clients are connected") ++{ ++ int idlver = GENERATE(TangoTest::idlversion(6)); ++ GIVEN("a device proxy to a simple IDLv" << idlver << " device") ++ { ++ TangoTest::Context ctx{"duplicate_event", "DuplicatePollingEvent", idlver}; ++ std::shared_ptr device = ctx.get_proxy(); ++ ++ REQUIRE(idlver == device->get_idl_version()); ++ ++ Tango::EventType ev_type = GENERATE(Tango::CHANGE_EVENT, Tango::ARCHIVE_EVENT, Tango::PERIODIC_EVENT); ++ ++ WHEN("some old clients subscribe to " << Tango::EventName[ev_type] << " events") ++ { ++ std::unique_ptr admin = ctx.get_admin_proxy(); ++ ++ simulate_old_clients_subscription(admin.get(), ev_type, idlver, device->dev_name(), k_attr_name); ++ ++ AND_WHEN("we subscribe to " << Tango::EventName[ev_type] << " events") ++ { ++ CallbackMockType callback; ++ ++ device->subscribe_event(k_attr_name, ev_type, &callback); ++ ++ // We only require one event for the initial subscription, we ++ // don't want to consume any events from the Zmq interface. ++ // Not quite sure why this matters for polling and not when ++ // pushing. I think the de-duplicate on the client side means ++ // we only see the duplicate event for the first events that are ++ // pushed. ++ auto maybe_initial_event = callback.pop_next_event(); ++ REQUIRE(maybe_initial_event != std::nullopt); ++ ++ AND_WHEN("we wait for three events") ++ { ++ // We wait for three events here as if there are duplicates ++ // two of these must have the same value, but we don't ++ // know which two. ++ auto maybe_event1 = callback.pop_next_event(); ++ auto maybe_event2 = callback.pop_next_event(); ++ auto maybe_event3 = callback.pop_next_event(); ++ ++ REQUIRE(maybe_event1 != std::nullopt); ++ REQUIRE(maybe_event2 != std::nullopt); ++ REQUIRE(maybe_event3 != std::nullopt); ++ ++ THEN("the events have different values") ++ { ++ Tango::DevLong value1; ++ *maybe_event1->attr_value >> value1; ++ Tango::DevLong value2; ++ *maybe_event2->attr_value >> value2; ++ Tango::DevLong value3; ++ *maybe_event3->attr_value >> value3; ++ ++ REQUIRE(value1 != value2); ++ REQUIRE(value2 != value3); ++ } ++ } ++ } ++ } ++ } ++} ++ ++template ++class DuplicateAttrConfEvent : public Base ++{ ++ public: ++ using Base::Base; ++ ++ ~DuplicateAttrConfEvent() override { } ++ ++ void init_device() override ++ { ++ value = 0; ++ } ++ ++ void read_attr(Tango::Attribute &att) override ++ { ++ value += 1; ++ att.set_value(&value); ++ } ++ ++ static void attribute_factory(std::vector &attrs) ++ { ++ using Attr = TangoTest::AutoAttr<&DuplicateAttrConfEvent::read_attr>; ++ Tango::UserDefaultAttrProp props; ++ props.set_abs_change("1"); ++ ++ auto attr = new Attr(k_attr_name, Tango::DEV_LONG); ++ attr->set_default_properties(props); ++ attrs.push_back(attr); ++ } ++ ++ private: ++ Tango::DevLong value{0}; ++}; ++ ++TANGO_TEST_AUTO_DEV_TMPL_INSTANTIATE(DuplicateAttrConfEvent, 6) ++ ++SCENARIO("Attribute config events are not duplicated when old clients are connected") ++{ ++ int idlver = GENERATE(TangoTest::idlversion(6)); ++ GIVEN("a device proxy to a simple IDLv" << idlver << " device") ++ { ++ TangoTest::Context ctx{"duplicate_event", "DuplicateAttrConfEvent", idlver}; ++ std::shared_ptr device = ctx.get_proxy(); ++ ++ REQUIRE(idlver == device->get_idl_version()); ++ ++ Tango::EventType ev_type = Tango::ATTR_CONF_EVENT; ++ ++ WHEN("some old clients subscribe to attr config") ++ { ++ std::unique_ptr admin = ctx.get_admin_proxy(); ++ ++ simulate_old_clients_subscription(admin.get(), ev_type, idlver, device->dev_name(), k_attr_name); ++ ++ AND_WHEN("we subscribe to attr config events") ++ { ++ TangoTest::CallbackMock callback; ++ ++ device->subscribe_event(k_attr_name, ev_type, &callback); ++ ++ auto maybe_initial_event = callback.pop_next_event(); ++ REQUIRE(maybe_initial_event != std::nullopt); ++ ++ maybe_initial_event = callback.pop_next_event(); ++ ++ AND_WHEN("we configure the attribute") ++ { ++ auto ai = device->attribute_query(k_attr_name); ++ ai.events.ch_event.abs_change = "Not specified"; ++ ++ Tango::AttributeInfoListEx ail; ++ ail.push_back(ai); ++ REQUIRE_NOTHROW(device->set_attribute_config(ail)); ++ ++ THEN("we only get a single attr conf event") ++ { ++ auto maybe_event = callback.pop_next_event(); ++ REQUIRE(maybe_event != std::nullopt); ++ REQUIRE(maybe_event->event == Tango::EventName[ev_type]); ++ REQUIRE(maybe_event->attr_conf != nullptr); ++ ++ maybe_event = callback.pop_next_event(); ++ REQUIRE(maybe_event == std::nullopt); ++ } ++ } ++ } ++ } ++ } ++} +Index: tango/lib/cpp/tests/CMakeLists.txt +=================================================================== +--- tango.orig/lib/cpp/tests/CMakeLists.txt ++++ tango/lib/cpp/tests/CMakeLists.txt +@@ -312,6 +312,7 @@ tango_catch2_tests_create( + catch2_attr_polling.cpp + catch2_cmd_polling.cpp + catch2_connection.cpp ++ catch2_event_old_client.cpp + catch2_event_reconnection.cpp + catch2_test_dtypes.cpp + catch2_dev_state.cpp diff -Nru tango-10.0.2+dfsg1/debian/patches/0002-Catch2-tests-add-Event-re-subscribes-with-the-same-I.patch tango-10.0.2+dfsg1/debian/patches/0002-Catch2-tests-add-Event-re-subscribes-with-the-same-I.patch --- tango-10.0.2+dfsg1/debian/patches/0002-Catch2-tests-add-Event-re-subscribes-with-the-same-I.patch 1970-01-01 00:00:00.000000000 +0000 +++ tango-10.0.2+dfsg1/debian/patches/0002-Catch2-tests-add-Event-re-subscribes-with-the-same-I.patch 2025-10-27 13:47:24.000000000 +0000 @@ -0,0 +1,213 @@ +From e4effb6d670aeb2a02e174691aef36ca821c530c Mon Sep 17 00:00:00 2001 +From: matveyev +Date: Mon, 11 Aug 2025 16:56:28 +0200 +Subject: [PATCH 2/2] Catch2 tests: add "Event re-subscribes with the same IDL" + test + +In test we check, that after server restart EventConsumerKeepAliveThread re-subscribe all events to the same maximum supported by client IDL + +Test does analysis of debug printouts, due to I have not found any other working solution to check, which IDL was requested +--- + tests/CMakeLists.txt | 1 + + tests/catch2_event_reconnection.cpp | 177 ++++++++++++++++++++++++++++ + 2 files changed, 178 insertions(+) + create mode 100644 tests/catch2_event_reconnection.cpp + +diff --git a/lib/cpp/tests/CMakeLists.txt b/lib/cpp/tests/CMakeLists.txt +index cf60db7f5..d40ef041b 100644 +--- a/lib/cpp/tests/CMakeLists.txt ++++ b/lib/cpp/tests/CMakeLists.txt +@@ -312,6 +312,7 @@ tango_catch2_tests_create( + catch2_attr_polling.cpp + catch2_cmd_polling.cpp + catch2_connection.cpp ++ catch2_event_reconnection.cpp + catch2_test_dtypes.cpp + catch2_dev_state.cpp + catch2_internal_utils.cpp +diff --git a/lib/cpp/tests/catch2_event_reconnection.cpp b/lib/cpp/tests/catch2_event_reconnection.cpp +new file mode 100644 +index 000000000..d80064ff9 +--- /dev/null ++++ b/lib/cpp/tests/catch2_event_reconnection.cpp +@@ -0,0 +1,177 @@ ++#include "catch2_common.h" ++#include ++ ++#include ++ ++namespace ++{ ++const std::string TestExceptReason = "Ahhh!"; ++const Tango::DevShort k_inital_short = 5678; ++using CallbackMockType = TangoTest::CallbackMock; ++ ++} // anonymous namespace ++ ++template ++class SimpleEventDevice : public Base ++{ ++ public: ++ using Base::Base; ++ ++ void init_device() override { } ++ ++ void push_change_event(Tango::DevString attr) ++ { ++ if(!strcmp(attr, "Short_attr")) ++ { ++ Base::push_change_event(attr, &short_value); ++ } ++ else ++ { ++ TANGO_THROW_EXCEPTION(TestExceptReason, "This is a test"); ++ } ++ } ++ ++ void read_attribute(Tango::Attribute &att) ++ { ++ if(att.get_name() == "Short_attr") ++ { ++ att.set_value(&short_value); ++ } ++ else ++ { ++ TANGO_THROW_EXCEPTION(TestExceptReason, "This is a test"); ++ } ++ } ++ ++ static void attribute_factory(std::vector &attrs) ++ { ++ auto short_attr = new TangoTest::AutoAttr<&SimpleEventDevice::read_attribute>("Short_attr", Tango::DEV_SHORT); ++ short_attr->set_change_event(true, false); ++ attrs.push_back(short_attr); ++ } ++ ++ static void command_factory(std::vector &cmds) ++ { ++ cmds.push_back(new TangoTest::AutoCommand<&SimpleEventDevice::push_change_event>("PushChangeEvent")); ++ } ++ ++ private: ++ Tango::DevShort short_value{k_inital_short}; ++}; ++ ++/* Returns true if all occurrences of ++ * Attribute::set_client_lib(N,change) ++ * in 'input' use the same N ++ */ ++bool checkSameClientLib(const std::string &input) ++{ ++ std::regex re(R"(Attribute::set_client_lib\(([0-9]+),change\))"); ++ std::smatch match; ++ std::string::const_iterator it = input.cbegin(); ++ ++ bool seenFirst = false; ++ int firstValue = Tango::detail::INVALID_IDL_VERSION; ++ ++ while(std::regex_search(it, input.cend(), match, re)) ++ { ++ int val = parse_as(match[1].str()); ++ ++ if(!seenFirst) ++ { ++ firstValue = val; ++ seenFirst = true; ++ } ++ else if(val != firstValue) ++ { ++ return false; ++ } ++ ++ it = match.suffix().first; ++ } ++ ++ if(!seenFirst) ++ { ++ return false; ++ } ++ ++ return true; ++} ++ ++TANGO_TEST_AUTO_DEV_TMPL_INSTANTIATE(SimpleEventDevice, 4) ++ ++SCENARIO("Event re-subscribes with the same IDL", "[slow]") ++{ ++ const std::string log_path = TangoTest::get_current_log_file_path(); ++ ++ // the previous full contents across GENERATE ++ static std::string prev_contents; ++ ++ int idlver = GENERATE(TangoTest::idlversion(4)); ++ GIVEN("a device proxy to a simple IDLv" << idlver << " device") ++ { ++ TangoTest::Context ctx{"event_reconnection", "SimpleEventDevice", idlver}; ++ std::shared_ptr device = ctx.get_proxy(); ++ ++ REQUIRE(idlver == device->get_idl_version()); ++ ++ AND_GIVEN("an change event subscription to that attribute") ++ { ++ TangoTest::CallbackMock callback; ++ auto event_id = device->subscribe_event("Short_attr", Tango::CHANGE_EVENT, &callback); ++ ++ auto maybe_event = callback.pop_next_event(); ++ REQUIRE(maybe_event != std::nullopt); ++ ++ WHEN("when we restart server") ++ { ++ ctx.stop_server(); ++ ctx.restart_server(); ++ ++ WHEN("an change event is generated after another error event") ++ { ++ using namespace Catch::Matchers; ++ ++ auto maybe_event = callback.pop_next_event(std::chrono::seconds{20}); ++ ++ REQUIRE(maybe_event != std::nullopt); ++ REQUIRE(maybe_event->err); ++ REQUIRE(std::string(Tango::API_EventTimeout) == maybe_event->errors[0].reason.in()); ++ ++ maybe_event = callback.pop_next_event(std::chrono::seconds{20}); ++ ++ REQUIRE(maybe_event != std::nullopt); ++ REQUIRE(maybe_event->event == Tango::EventName[Tango::CHANGE_EVENT]); ++ ++ THEN("an change event is generated after another error event") ++ { ++ std::string all_server_log = load_file(log_path); ++ ++ std::string new_chunk; ++ if(all_server_log.size() > prev_contents.size()) ++ { ++ new_chunk = all_server_log.substr(prev_contents.size()); ++ } ++ else ++ { ++ // log was truncated or rotated, then treat entire file as new ++ new_chunk = all_server_log; ++ } ++ prev_contents = std::move(all_server_log); ++ ++ // sanity-check: we should actually have something new ++ REQUIRE(!new_chunk.empty()); ++ ++ REQUIRE(checkSameClientLib(new_chunk)); ++ ++ std::regex re(R"(Attribute::set_client_lib\(([0-9]+),change\))"); ++ std::smatch m; ++ REQUIRE(std::regex_search(new_chunk, m, re)); ++ int found = parse_as(m[1].str()); ++ CHECK(found == idlver); ++ } ++ } ++ } ++ device->unsubscribe_event(event_id); ++ } ++ } ++} +-- +2.39.5 + diff -Nru tango-10.0.2+dfsg1/debian/patches/0002-DServer-zmq_event_subscription_changed-Map-client-id.patch tango-10.0.2+dfsg1/debian/patches/0002-DServer-zmq_event_subscription_changed-Map-client-id.patch --- tango-10.0.2+dfsg1/debian/patches/0002-DServer-zmq_event_subscription_changed-Map-client-id.patch 1970-01-01 00:00:00.000000000 +0000 +++ tango-10.0.2+dfsg1/debian/patches/0002-DServer-zmq_event_subscription_changed-Map-client-id.patch 2025-10-27 13:40:08.000000000 +0000 @@ -0,0 +1,113 @@ +From d95abde7a0fd89cb979dae9052e3f0e7cc2be128 Mon Sep 17 00:00:00 2001 +From: Thomas Ives +Date: Mon, 1 Sep 2025 09:11:22 +0100 +Subject: [PATCH 2/5] DServer::zmq_event_subscription_changed: Map client idl + versions... + +... to event data versions. + +We didn't introduce new AttributeValue data structures with IDLv6, so we +shouldn't being sending an event for each client version selected. +Instead, we should be sending a ZMQ event for each type of data +structure, one that each client can use. +--- + src/server/eventcmds.cpp | 76 +++++++++++++++++++++++++++++++++++++++- + 1 file changed, 75 insertions(+), 1 deletion(-) + +diff --git a/lib/cpp/src/server/eventcmds.cpp b/lib/cpp/src/server/eventcmds.cpp +index 4f26dd8a8..af96f70b7 100644 +--- a/lib/cpp/src/server/eventcmds.cpp ++++ b/lib/cpp/src/server/eventcmds.cpp +@@ -489,6 +489,79 @@ MulticastParameters + return result; + } + ++namespace ++{ ++int zmq_client_idl_to_event_data_version(int client_lib_version, const std::string &event_name) ++{ ++ TANGO_ASSERT(client_lib_version > 0); ++ TANGO_ASSERT(client_lib_version <= DevVersion); ++ ++ auto *tg = Tango::Util::instance(false); ++ EventType event_type; ++ tg->event_name_2_event_type(event_name, event_type); ++ ++ // The (event_type, ) pair corresponds to which element on ++ // SuppliedEventData is active when we call ZmqEventSupplier::push_event. ++ ++ switch(event_type) ++ { ++ case CHANGE_EVENT: ++ [[fallthrough]]; ++ case ARCHIVE_EVENT: ++ [[fallthrough]]; ++ case USER_EVENT: ++ [[fallthrough]]; ++ case PERIODIC_EVENT: ++ if(client_lib_version >= 5) ++ { ++ // AttributeValue_5 ++ return 5; ++ } ++ else if(client_lib_version == 4) ++ { ++ // AttributeValue_4 ++ return 4; ++ } ++ else ++ { ++ // AttributeValue_3 ++ return 3; ++ } ++ case ATTR_CONF_EVENT: ++ if(client_lib_version >= 5) ++ { ++ // AttributeConfig_5 ++ return 5; ++ } ++ else ++ { ++ // AttributeConfig_3 ++ return 3; ++ } ++ case ALARM_EVENT: ++ TANGO_ASSERT(client_lib_version >= 6); ++ // AttributeValue_5 ++ // TODO(porting to main): Update alarm event handling so that ++ // this can be 5 (#1531). ++ return 6; ++ case DATA_READY_EVENT: ++ // AttDataReady ++ return 1; ++ case INTERFACE_CHANGE_EVENT: ++ // DevIntrChange ++ return 1; ++ case PIPE_EVENT: ++ // DevPipeData ++ return 1; ++ default: ++ TANGO_ASSERT_ON_DEFAULT(event_type); ++ } ++ ++ // unreachable ++ return 0; ++} ++} // namespace ++ + void DServer::store_subscribed_client_info(DeviceImpl &device, + const std::string &object_name, + const std::string &event_name, +@@ -947,7 +1020,8 @@ DevVarLongStringArray *DServer::zmq_event_subscription_change(const Tango::DevVa + // the command fails. + if(action == "subscribe") + { +- store_subscribed_client_info(*dev, obj_name, event, client_release); ++ int event_data_version = zmq_client_idl_to_event_data_version(client_release, event); ++ store_subscribed_client_info(*dev, obj_name, event, event_data_version); + } + + // +-- +2.39.5 + diff -Nru tango-10.0.2+dfsg1/debian/patches/0003-push_att_conf_events-Use-event-data-to-determine-ver.patch tango-10.0.2+dfsg1/debian/patches/0003-push_att_conf_events-Use-event-data-to-determine-ver.patch --- tango-10.0.2+dfsg1/debian/patches/0003-push_att_conf_events-Use-event-data-to-determine-ver.patch 1970-01-01 00:00:00.000000000 +0000 +++ tango-10.0.2+dfsg1/debian/patches/0003-push_att_conf_events-Use-event-data-to-determine-ver.patch 2025-10-27 13:40:08.000000000 +0000 @@ -0,0 +1,42 @@ +From ebb88b7bad674cc9515765d09752ea6a518a72c7 Mon Sep 17 00:00:00 2001 +From: Thomas Ives +Date: Mon, 1 Sep 2025 09:21:17 +0100 +Subject: [PATCH 3/5] push_att_conf_events: Use event data to determine vers + +Do not need to consider the version of the device pushing the event when +deciding if we need to add the idl5_ prefix to the event name. Instead, +the thing that matters is who the event is aimed at. This can be +determined from the SuppliedEventData based on which pointer is not +nullptr. +--- + src/server/eventsupplier.cpp | 14 +++++++++++++- + 1 file changed, 13 insertions(+), 1 deletion(-) + +diff --git a/lib/cpp/src/server/eventsupplier.cpp b/lib/cpp/src/server/eventsupplier.cpp +index 696370dce..e66df4a1b 100644 +--- a/lib/cpp/src/server/eventsupplier.cpp ++++ b/lib/cpp/src/server/eventsupplier.cpp +@@ -2475,7 +2475,19 @@ void EventSupplier::push_att_conf_events(DeviceImpl *device_impl, + // Called for AttributeConfig_3 or AttributeConfig_5 ? + // + +- const int vers = device_impl->get_dev_idl_version(); ++ int vers; ++ if(attr_conf.attr_conf_5 != nullptr) ++ { ++ vers = 5; ++ } ++ else if(attr_conf.attr_conf_3 != nullptr) ++ { ++ vers = 3; ++ } ++ else ++ { ++ vers = 2; ++ } + + // + // Return if there is no client or if the last client subscription is more than 10 mins ago +-- +2.39.5 + diff -Nru tango-10.0.2+dfsg1/debian/patches/0004-catch2_event_reconnection-Use-log-from-new-mapping-f.patch tango-10.0.2+dfsg1/debian/patches/0004-catch2_event_reconnection-Use-log-from-new-mapping-f.patch --- tango-10.0.2+dfsg1/debian/patches/0004-catch2_event_reconnection-Use-log-from-new-mapping-f.patch 1970-01-01 00:00:00.000000000 +0000 +++ tango-10.0.2+dfsg1/debian/patches/0004-catch2_event_reconnection-Use-log-from-new-mapping-f.patch 2025-10-27 13:47:24.000000000 +0000 @@ -0,0 +1,40 @@ +From e6d9b39cf04086eb5d4cd48297a8eb59cdf84a5b Mon Sep 17 00:00:00 2001 +From: Thomas Ives +Date: Mon, 1 Sep 2025 09:38:05 +0100 +Subject: [PATCH 4/5] catch2_event_reconnection: Use log from new mapping + function + +--- + src/server/eventcmds.cpp | 2 ++ + tests/catch2_event_reconnection.cpp | 2 +- + 2 files changed, 3 insertions(+), 1 deletion(-) + +diff --git a/lib/cpp/src/server/eventcmds.cpp b/lib/cpp/src/server/eventcmds.cpp +index af96f70b7..d6bb5cffc 100644 +--- a/lib/cpp/src/server/eventcmds.cpp ++++ b/lib/cpp/src/server/eventcmds.cpp +@@ -496,6 +496,8 @@ int zmq_client_idl_to_event_data_version(int client_lib_version, const std::stri + TANGO_ASSERT(client_lib_version > 0); + TANGO_ASSERT(client_lib_version <= DevVersion); + ++ TANGO_LOG_DEBUG << "zmq_client_idl_to_event_data_version(" << client_lib_version << "," << event_name << ")"; ++ + auto *tg = Tango::Util::instance(false); + EventType event_type; + tg->event_name_2_event_type(event_name, event_type); +diff --git a/lib/cpp/tests/catch2_event_reconnection.cpp b/lib/cpp/tests/catch2_event_reconnection.cpp +index d80064ff9..2bd0a78b6 100644 +--- a/lib/cpp/tests/catch2_event_reconnection.cpp ++++ b/lib/cpp/tests/catch2_event_reconnection.cpp +@@ -163,7 +163,7 @@ SCENARIO("Event re-subscribes with the same IDL", "[slow]") + + REQUIRE(checkSameClientLib(new_chunk)); + +- std::regex re(R"(Attribute::set_client_lib\(([0-9]+),change\))"); ++ std::regex re(R"(zmq_client_idl_to_event_data_version\(([0-9]+),change\))"); + std::smatch m; + REQUIRE(std::regex_search(new_chunk, m, re)); + int found = parse_as(m[1].str()); +-- +2.39.5 + diff -Nru tango-10.0.2+dfsg1/debian/patches/0005-Attribute-set_upd_properties-Handle-no-database-case.patch tango-10.0.2+dfsg1/debian/patches/0005-Attribute-set_upd_properties-Handle-no-database-case.patch --- tango-10.0.2+dfsg1/debian/patches/0005-Attribute-set_upd_properties-Handle-no-database-case.patch 1970-01-01 00:00:00.000000000 +0000 +++ tango-10.0.2+dfsg1/debian/patches/0005-Attribute-set_upd_properties-Handle-no-database-case.patch 2025-10-27 13:40:08.000000000 +0000 @@ -0,0 +1,106 @@ +From 530c3bbb89c0115d58ba7bcc4dd7d15d0182e259 Mon Sep 17 00:00:00 2001 +From: Thomas Braun +Date: Thu, 1 May 2025 14:29:28 +0200 +Subject: [PATCH 5/5] Attribute::set_upd_properties: Handle no database case + gracefully + +When trying to set the attribute configuration without database, we used +to crash inside Attribute::db_access as the returned pointer from get_database() was not valid. + +We now skip the database update similiar to what is done in +DeviceImpl::set_attribute_config. +--- + src/include/tango/server/attribute_templ.h | 5 ++- + tests/CMakeLists.txt | 1 + + tests/catch2_attr_conf_event.cpp | 51 ++++++++++++++++++++++ + 3 files changed, 56 insertions(+), 1 deletion(-) + create mode 100644 tests/catch2_attr_conf_event.cpp + +diff --git a/lib/cpp/src/include/tango/server/attribute_templ.h b/lib/cpp/src/include/tango/server/attribute_templ.h +index b05843a26..e5e98888d 100644 +--- a/lib/cpp/src/include/tango/server/attribute_templ.h ++++ b/lib/cpp/src/include/tango/server/attribute_templ.h +@@ -1357,7 +1357,10 @@ void Attribute::set_upd_properties(const T &conf, const std::string &dev_name, b + + try + { +- upd_database(v_db); ++ if(Tango::Util::instance()->use_db()) ++ { ++ upd_database(v_db); ++ } + } + catch(DevFailed &) + { +diff --git a/lib/cpp/tests/CMakeLists.txt b/lib/cpp/tests/CMakeLists.txt +index 64b957559..83db6fe97 100644 +--- a/lib/cpp/tests/CMakeLists.txt ++++ b/lib/cpp/tests/CMakeLists.txt +@@ -309,6 +309,7 @@ tango_catch2_tests_create( + catch2_alarm.cpp + catch2_attr_manip.cpp + catch2_attr_proxy.cpp ++ catch2_attr_conf_event.cpp + catch2_attr_polling.cpp + catch2_cmd_polling.cpp + catch2_connection.cpp +diff --git a/lib/cpp/tests/catch2_attr_conf_event.cpp b/lib/cpp/tests/catch2_attr_conf_event.cpp +new file mode 100644 +index 000000000..aea73316b +--- /dev/null ++++ b/lib/cpp/tests/catch2_attr_conf_event.cpp +@@ -0,0 +1,51 @@ ++#include "catch2_common.h" ++ ++static constexpr double k_initial_value{1.1234}; ++ ++template ++class AttrConfEventData : public Base ++{ ++ public: ++ using Base::Base; ++ ++ void init_device() override { } ++ ++ void read_attr(Tango::Attribute &att) override ++ { ++ att.set_value(&attr_dq_double); ++ } ++ ++ static void attribute_factory(std::vector &attrs) ++ { ++ attrs.push_back(new TangoTest::AutoAttr<&AttrConfEventData::read_attr>("double_attr", Tango::DEV_DOUBLE)); ++ } ++ ++ private: ++ Tango::DevDouble attr_dq_double{k_initial_value}; ++}; ++ ++TANGO_TEST_AUTO_DEV_TMPL_INSTANTIATE(AttrConfEventData, 1) ++ ++SCENARIO("Setting AttributeConfig works without database") ++{ ++ int idlver = GENERATE(TangoTest::idlversion(1)); ++ GIVEN("a device proxy to a simple IDLv" << idlver << " device") ++ { ++ TangoTest::Context ctx{"double_attr", "AttrConfEventData", idlver}; ++ auto device = ctx.get_proxy(); ++ ++ REQUIRE(idlver == device->get_idl_version()); ++ ++ THEN("we can change the attribute configuration") ++ { ++ std::string attr{"double_attr"}; ++ auto ai = device->attribute_query(attr); ++ ai.events.ch_event.abs_change = "33333"; ++ ai.events.ch_event.rel_change = "99.99"; ++ ++ Tango::AttributeInfoListEx ail; ++ ail.push_back(ai); ++ REQUIRE_NOTHROW(device->set_attribute_config(ail)); ++ } ++ } ++} +-- +2.39.5 + diff -Nru tango-10.0.2+dfsg1/debian/patches/series tango-10.0.2+dfsg1/debian/patches/series --- tango-10.0.2+dfsg1/debian/patches/series 2025-02-24 01:10:39.000000000 +0000 +++ tango-10.0.2+dfsg1/debian/patches/series 2025-10-27 13:47:24.000000000 +0000 @@ -5,3 +5,12 @@ 0001-Fix-timestamp-columns-in-Mariadb-10.11-and-newer.patch 1090173-sphinx-conf.patch + +#1118207 +0001-EventConsumerKeepAliveThread-send-actual-IDL-when-ex.patch +0002-Catch2-tests-add-Event-re-subscribes-with-the-same-I.patch +0001-catch2_event_old_client-Add-test-to-catch-duplicate-.patch +0002-DServer-zmq_event_subscription_changed-Map-client-id.patch +0003-push_att_conf_events-Use-event-data-to-determine-ver.patch +0004-catch2_event_reconnection-Use-log-from-new-mapping-f.patch +0005-Attribute-set_upd_properties-Handle-no-database-case.patch