Repository: zeromq/cppzmq
Pull request #657: Add zmq_timers support
Author: login: cacharle
Created: 2025-04-17T18:30:24Z | Merged: 2025-04-23T19:30:24Z

Fixes #575 

This is a draft to add support for the `zmq_timers_*` functions.

Please let me know what are the requirements to get it merged.



Linked issue #575: Support for zmq timers?
Hi,

looking at the library's source, I guess there is no support for timer functions (the ones described in http://api.zeromq.org/master:zmq-timers).
Is there a safe way to use zmq timers while continuing to use cppzmq for networking?

Are there any plans to support them?

Thanks!

Linked issue comments: user: login: gummif
created_at: 2022-10-09T13:33:31Z
body: I personally have no need for them since there are better options in C++, e.g. asio::steady_timer.
author_association: MEMBER
user: login: muxator
created_at: 2025-04-23T21:56:34Z
body: Thanks for implementing this! 😄 
author_association: NONE

Diff:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b45e91d0..3035755d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,7 @@
 cmake_minimum_required(VERSION 3.11)
 
 list (APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
 
 include (DetectCPPZMQVersion)
 
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index e20aaaaf..3821c5c8 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -30,6 +30,7 @@ add_executable(
     codec_multipart.cpp
     monitor.cpp
     utilities.cpp
+    timers.cpp
 )
 
 target_include_directories(unit_tests PUBLIC ${CATCH_MODULE_PATH})
diff --git a/tests/timers.cpp b/tests/timers.cpp
new file mode 100644
index 00000000..4229fd5a
--- /dev/null
+++ b/tests/timers.cpp
@@ -0,0 +1,80 @@
+#include <catch2/catch_all.hpp>
+#include <zmq.hpp>
+
+#include <type_traits>
+#include <thread>
+#include <chrono>
+
+#if defined(ZMQ_CPP11) && defined(ZMQ_HAVE_TIMERS)
+
+static_assert(std::is_default_constructible<zmq::timers>::value);
+static_assert(!std::is_copy_constructible<zmq::timers>::value);
+static_assert(!std::is_copy_assignable<zmq::timers>::value);
+
+TEST_CASE("timers constructor", "[timers]")
+{
+    zmq::timers timers;
+    CHECK(!timers.timeout().has_value());
+}
+
+TEST_CASE("timers add/execute", "[timers]")
+{
+    using namespace std::chrono_literals;
+    zmq::timers timers;
+    bool handler_ran = false;
+    timers.add(4ms, [](auto, void *arg) { *(bool *) arg = true; }, &handler_ran);
+    CHECK(timers.timeout().has_value());
+    CHECK(!handler_ran);
+    std::this_thread::sleep_for(10ms);
+    timers.execute();
+    CHECK(handler_ran);
+}
+
+TEST_CASE("timers add/cancel", "[timers]")
+{
+    using namespace std::chrono_literals;
+    zmq::timers timers;
+    bool handler_ran = false;
+    auto id =
+      timers.add(4ms, [](auto, void *arg) { *(bool *) arg = true; }, &handler_ran);
+    CHECK(timers.timeout().has_value());
+    CHECK(!handler_ran);
+    timers.cancel(id);
+    CHECK(!timers.timeout().has_value());
+    CHECK(!handler_ran);
+}
+
+TEST_CASE("timers set_interval", "[timers]")
+{
+    using namespace std::chrono_literals;
+    zmq::timers timers;
+    bool handler_ran = false;
+    // Interval of 4 hours should never run like this
+    auto id =
+      timers.add(4h, [](auto, void *arg) { *(bool *) arg = true; }, &handler_ran);
+    CHECK(timers.timeout().has_value());
+    CHECK(!handler_ran);
+    // Change the interval to 4ms and wait for it to timeout
+    timers.set_interval(id, 4ms);
+    std::this_thread::sleep_for(10ms);
+    timers.execute();
+    CHECK(handler_ran);
+}
+
+TEST_CASE("timers reset", "[timers]")
+{
+    using namespace std::chrono_literals;
+    zmq::timers timers;
+    bool handler_ran = false;
+    auto id =
+      timers.add(4ms, [](auto, void *arg) { *(bool *) arg = true; }, &handler_ran);
+    CHECK(timers.timeout().has_value());
+    std::this_thread::sleep_for(10ms);
+    // Available to be executed but we reset it
+    timers.reset(id);
+    CHECK(timers.timeout().has_value());
+    CHECK(!handler_ran);
+
+}
+
+#endif // defined(ZMQ_CPP11) && defined(ZMQ_HAVE_TIMERS)
diff --git a/zmq.hpp b/zmq.hpp
index 5b22687e..363f74bf 100644
--- a/zmq.hpp
+++ b/zmq.hpp
@@ -2794,6 +2794,85 @@ inline std::ostream &operator<<(std::ostream &os, const message_t &msg)
     return os << msg.str();
 }
 
+#if defined(ZMQ_CPP11) && defined(ZMQ_HAVE_TIMERS)
+
+class timers
+{
+  public:
+    using id_t = int;
+    using fn_t = zmq_timer_fn;
+
+#if CPPZMQ_HAS_OPTIONAL
+    using timeout_result_t = std::optional<std::chrono::milliseconds>;
+#else
+    using timeout_result_t = detail::trivial_optional<std::chrono::milliseconds>;
+#endif
+
+    timers() : _timers(zmq_timers_new())
+    {
+        if (_timers == nullptr)
+            throw error_t();
+    }
+
+    timers(const timers &other) = delete;
+    timers &operator=(const timers &other) = delete;
+
+    ~timers()
+    {
+        int rc = zmq_timers_destroy(&_timers);
+        ZMQ_ASSERT(rc == 0);
+    }
+
+    id_t add(std::chrono::milliseconds interval, zmq_timer_fn handler, void *arg)
+    {
+        id_t timer_id = zmq_timers_add(_timers, interval.count(), handler, arg);
+        if (timer_id == -1)
+            throw zmq::error_t();
+        return timer_id;
+    }
+
+    void cancel(id_t timer_id)
+    {
+        int rc = zmq_timers_cancel(_timers, timer_id);
+        if (rc == -1)
+            throw zmq::error_t();
+    }
+
+    void set_interval(id_t timer_id, std::chrono::milliseconds interval)
+    {
+        int rc = zmq_timers_set_interval(_timers, timer_id, interval.count());
+        if (rc == -1)
+            throw zmq::error_t();
+    }
+
+    void reset(id_t timer_id)
+    {
+        int rc = zmq_timers_reset(_timers, timer_id);
+        if (rc == -1)
+            throw zmq::error_t();
+    }
+
+    timeout_result_t timeout() const
+    {
+        int timeout = zmq_timers_timeout(_timers);
+        if (timeout == -1)
+            return timeout_result_t{};
+        return std::chrono::milliseconds{timeout};
+    }
+
+    void execute()
+    {
+        int rc = zmq_timers_execute(_timers);
+        if (rc == -1)
+            throw zmq::error_t();
+    }
+
+  private:
+    void *_timers;
+};
+
+#endif // defined(ZMQ_CPP11) && defined(ZMQ_HAVE_TIMERS)
+
 } // namespace zmq
 
 #endif // __ZMQ_HPP_INCLUDED__


Reviews:
user: login: gummif
body: Thanks. What is missing is some minor fixes and some simple tests should be sufficient.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-18T14:46:22Z
user: login: cacharle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T15:42:06Z
user: login: gummif
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T17:42:23Z
user: login: gummif
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T08:49:42Z
user: login: gummif
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T08:51:18Z
user: login: gummif
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T08:54:06Z
user: login: cacharle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T09:24:17Z
user: login: gummif
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T09:36:10Z
user: login: cacharle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T09:55:45Z

Review comments:
diff_hunk: @@ -2794,6 +2794,70 @@ inline std::ostream &operator<<(std::ostream &os, const message_t &msg)
     return os << msg.str();
 }
 
+using timer_id_t = int;
+
+class timers_t
+{
+  public:
+    timers_t() : _timers(zmq_timers_new())
+    {
+        if (_timers == nullptr)
+            throw error_t();
+    }
+
+    timers_t(timers_t &other) = delete;
path: zmq.hpp
user: login: gummif
body: ```suggestion
    timers_t(const timers_t &other) = delete;
```
created_at: 2025-04-18T14:33:15Z
author_association: MEMBER
diff_hunk: @@ -2794,6 +2794,70 @@ inline std::ostream &operator<<(std::ostream &os, const message_t &msg)
     return os << msg.str();
 }
 
+using timer_id_t = int;
+
+class timers_t
+{
+  public:
+    timers_t() : _timers(zmq_timers_new())
+    {
+        if (_timers == nullptr)
+            throw error_t();
+    }
+
+    timers_t(timers_t &other) = delete;
path: zmq.hpp
user: login: gummif
body: Also need to delete the copy assignment operator.
created_at: 2025-04-18T14:34:02Z
author_association: MEMBER
diff_hunk: @@ -2794,6 +2794,70 @@ inline std::ostream &operator<<(std::ostream &os, const message_t &msg)
     return os << msg.str();
 }
 
+using timer_id_t = int;
+
+class timers_t
+{
+  public:
+    timers_t() : _timers(zmq_timers_new())
+    {
+        if (_timers == nullptr)
+            throw error_t();
+    }
+
+    timers_t(timers_t &other) = delete;
+
+    ~timers_t() { ZMQ_ASSERT(zmq_timers_destroy(&_timers) == 0); }
path: zmq.hpp
user: login: gummif
body: Move zmq_timers_destroy out of the assert, and only check the return value in the assert.
created_at: 2025-04-18T14:35:16Z
author_association: MEMBER
diff_hunk: @@ -2794,6 +2794,70 @@ inline std::ostream &operator<<(std::ostream &os, const message_t &msg)
     return os << msg.str();
 }
 
+using timer_id_t = int;
path: zmq.hpp
user: login: gummif
body: This whole diff should be behind a preprocessor if, checking the min required libzmq version. Also requires C++11, but it uses `std::optional` so either require C++17 or use the `trivial_optional` for pre C++17 version (but not sure if `milliseconds` is trivial). Otherwise just require C++17.
created_at: 2025-04-18T14:40:27Z
author_association: MEMBER
diff_hunk: @@ -2794,6 +2794,70 @@ inline std::ostream &operator<<(std::ostream &os, const message_t &msg)
     return os << msg.str();
 }
 
+using timer_id_t = int;
path: zmq.hpp
user: login: gummif
body: Maybe add `using timer_fn_t = zmq_timer_fn;`
created_at: 2025-04-18T14:41:05Z
author_association: MEMBER
diff_hunk: @@ -2794,6 +2794,70 @@ inline std::ostream &operator<<(std::ostream &os, const message_t &msg)
     return os << msg.str();
 }
 
+using timer_id_t = int;
path: zmq.hpp
user: login: cacharle
body: I saw in another issue that someone suggested to use duration<T> instead of chrono::milliseconds

https://github.com/zeromq/cppzmq/issues/642#issuecomment-2485271977

Would you prefer duration<T>?
created_at: 2025-04-18T15:42:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2794,6 +2794,70 @@ inline std::ostream &operator<<(std::ostream &os, const message_t &msg)
     return os << msg.str();
 }
 
+using timer_id_t = int;
path: zmq.hpp
user: login: gummif
body: I think milliseconds is fine as is. But duration would be an improvement.
created_at: 2025-04-18T17:42:23Z
author_association: MEMBER
diff_hunk: @@ -2794,6 +2794,85 @@ inline std::ostream &operator<<(std::ostream &os, const message_t &msg)
     return os << msg.str();
 }
 
+#if defined(ZMQ_CPP11) && defined(ZMQ_HAVE_TIMERS)
+
+class timers
+{
+  public:
+    using id_t = int;
+    using fn_t = zmq_timer_fn;
+
+#if CPPZMQ_HAS_OPTIONAL
+    using timeout_result_t = std::optional<std::chrono::milliseconds>;
+#else
+    using timeout_result_t = std::trivial_optional<std::chrono::milliseconds>;
+#endif
+
+    timers() : _timers(zmq_timers_new())
+    {
+        if (_timers == nullptr)
+            throw error_t();
+    }
+
+    timers(const timers &other) = delete;
+    timers &operator=(const timers &other) = delete;
+
+    ~timers()
+    {
+        int rc = zmq_timers_destroy(&_timers);
+        ZMQ_ASSERT(rc == 0);
+    }
+
+    id_t add(std::chrono::milliseconds interval, zmq_timer_fn handler, void *arg)
+    {
+        id_t timer_id = zmq_timers_add(_timers, interval.count(), handler, arg);
+        if (timer_id == -1)
+            throw zmq::error_t();
+        return timer_id;
+    }
+
+    void cancel(id_t timer_id)
+    {
+        int rc = zmq_timers_cancel(_timers, timer_id);
+        if (rc == -1)
+            throw zmq::error_t();
+    }
+
+    void set_interval(id_t timer_id, std::chrono::milliseconds interval)
+    {
+        int rc = zmq_timers_set_interval(_timers, timer_id, interval.count());
+        if (rc == -1)
+            throw zmq::error_t();
+    }
+
+    void reset(id_t timer_id)
+    {
+        int rc = zmq_timers_reset(_timers, timer_id);
+        if (rc == -1)
+            throw zmq::error_t();
+    }
+
+    timeout_result_t timeout() const
+    {
+        int timeout = zmq_timers_timeout(_timers);
+        if (timeout == -1)
+            return std::nullopt;
path: zmq.hpp
user: login: gummif
body: This needs to be `timeout_result_t{}` instead.
created_at: 2025-04-22T08:49:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,78 @@
+#include <catch2/catch_all.hpp>
+#include <zmq.hpp>
+
+#include <type_traits>
+#include <thread>
+#include <chrono>
+
+#ifdef ZMQ_CPP11
path: tests/timers.cpp
user: login: gummif
body: You should guard all all this file with `#if defined(ZMQ_CPP11) && defined(ZMQ_HAVE_TIMERS)`
created_at: 2025-04-22T08:51:18Z
author_association: MEMBER
diff_hunk: @@ -1,6 +1,7 @@
 cmake_minimum_required(VERSION 3.11)
 
 list (APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
path: CMakeLists.txt
user: login: gummif
body: This can probably be removed.
created_at: 2025-04-22T08:54:06Z
author_association: MEMBER
diff_hunk: @@ -1,6 +1,7 @@
 cmake_minimum_required(VERSION 3.11)
 
 list (APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
path: CMakeLists.txt
user: login: cacharle
body: don't most editors with LSP use build/compile_commands.json? I thought it might be handy for someone else
created_at: 2025-04-22T09:24:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,6 +1,7 @@
 cmake_minimum_required(VERSION 3.11)
 
 list (APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
path: CMakeLists.txt
user: login: gummif
body: Not sure, does this affect the consumer of the library? But it can stay if it is useful.
created_at: 2025-04-22T09:36:10Z
author_association: MEMBER
diff_hunk: @@ -1,6 +1,7 @@
 cmake_minimum_required(VERSION 3.11)
 
 list (APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
path: CMakeLists.txt
user: login: cacharle
body: https://cmake.org/cmake/help/latest/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html

It only generates a `compile_commands.json` file
created_at: 2025-04-22T09:55:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: cacharle
created_at: 2025-04-18T16:57:53Z
body: I've had to rename `zmq::timers_t` to `zmq::timers`, the first one was making the tests crash (I'm guessing a linker name conflict with libzmq)

Do you have any workaround to keep the `_t` convention?
author_association: CONTRIBUTOR
user: login: gummif
created_at: 2025-04-18T17:40:40Z
body: I think `zmq::timers` is fine. Not a fan of the _t convention, also unfortunate we are using the same namespace as libzmq.
author_association: MEMBER
user: login: cacharle
created_at: 2025-04-18T20:14:07Z
body: Alright then I think this PR is ready
author_association: CONTRIBUTOR
user: login: gummif
created_at: 2025-04-23T10:12:17Z
body: Could you try rebasing your branch on latest master, I just fixed the CI runners.
author_association: MEMBER
user: login: coveralls
created_at: 2025-04-23T17:26:02Z
body: ## Pull Request Test Coverage Report for [Build 14624243775](https://coveralls.io/builds/73354074)


### Details

* **33** of **39**   **(84.62%)**  changed or added relevant lines in **1** file are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage decreased (**-0.07%**) to **86.301%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [zmq.hpp](https://coveralls.io/builds/73354074/source?filename=zmq.hpp#L2814) | 33 | 39 | 84.62%
<!-- | **Total:** | **33** | **39** | **84.62%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/73354074/badge)](https://coveralls.io/builds/73354074) |
| :-- | --: |
| Change from base [Build 14615581445](https://coveralls.io/builds/73346162): |  -0.07% |
| Covered Lines: | 882 |
| Relevant Lines: | 1022 |

---
##### 💛  - [Coveralls](https://coveralls.io)

author_association: NONE
user: login: gummif
created_at: 2025-04-23T19:30:03Z
body: Very good. Thanks for this.
author_association: MEMBER