pytq-cxx 1.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ include _yatq/traits.h
2
+ recursive-include _yatq/include *.h
3
+ recursive-include setup_helpers *.cmake
4
+ global-exclude */__pycache__/*
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.1
2
+ Name: pytq-cxx
3
+ Version: 1.0.2
4
+ Summary: single-threaded timer demultiplexer backed by C++ implementation
5
+ Author-email: Nikita Vaganov <nikita.e.vaganov@gmail.com>
6
+ Project-URL: Homepage, https://github.com/vaganov/yatq
7
+ Project-URL: Bug Tracker, https://github.com/vaganov/yatq/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
10
+ Requires-Python: >=3.7
11
+ Description-Content-Type: text/markdown
12
+
13
+ # pytq-cxx
14
+ **pytq-cxx** is a _python_ wrapper for **yatq** -- a _C++_ template library. For the documentation and examples please
15
+ see [yatq homepage](https://github.com/vaganov/yatq)
16
+
17
+ **NB:** This package has nothing to do with [pytq package](https://pypi.org/project/pytq)
18
+
19
+ **NB:** Module name is, however, **pytq**
20
+
21
+ from pytq import ThreadPool, TimerQueue, AwaitableFuture
@@ -0,0 +1,9 @@
1
+ # pytq-cxx
2
+ **pytq-cxx** is a _python_ wrapper for **yatq** -- a _C++_ template library. For the documentation and examples please
3
+ see [yatq homepage](https://github.com/vaganov/yatq)
4
+
5
+ **NB:** This package has nothing to do with [pytq package](https://pypi.org/project/pytq)
6
+
7
+ **NB:** Module name is, however, **pytq**
8
+
9
+ from pytq import ThreadPool, TimerQueue, AwaitableFuture
@@ -0,0 +1,72 @@
1
+ #define PYBIND11_DETAILED_ERROR_MESSAGES
2
+ #include <pybind11/pybind11.h>
3
+ #include <pybind11/chrono.h>
4
+ #include <pybind11/functional.h>
5
+
6
+ #ifndef YATQ_DISABLE_FUTURES
7
+ #define BOOST_THREAD_PROVIDES_FUTURE
8
+ #define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION
9
+ #include <boost/thread/future.hpp>
10
+ #endif
11
+
12
+ #include "yatq/thread_pool.h"
13
+ #include "yatq/timer_queue.h"
14
+ #include "yatq/version.h"
15
+
16
+ #include "traits.h"
17
+
18
+ namespace py = pybind11;
19
+
20
+ using result_type = py::object;
21
+ using Future = boost::future<result_type>;
22
+ using Executable = std::function<result_type(void)>;
23
+ using ThreadPool = yatq::ThreadPool<Executable>;
24
+ using TimerQueue = yatq::TimerQueue<ThreadPool>;
25
+
26
+ PYBIND11_MODULE(_yatq, m) {
27
+ #ifndef YATQ_DISABLE_FUTURES
28
+ auto boost_submodule = m.def_submodule("boost");
29
+ py::class_<Future>(boost_submodule, "future")
30
+ .def("get", &Future::get, py::call_guard<py::gil_scoped_release>())
31
+ .def("wait", &Future::wait, py::call_guard<py::gil_scoped_release>())
32
+ .def("is_ready", py::overload_cast<>(&Future::is_ready, py::const_));
33
+ #endif
34
+
35
+ #ifndef YATQ_DISABLE_PTHREAD
36
+ auto utils_submodule = m.def_submodule("utils");
37
+ py::enum_<yatq::utils::priority_t>(utils_submodule, "priority_t")
38
+ .value("min_priority", yatq::utils::min_priority)
39
+ .value("max_priority", yatq::utils::max_priority)
40
+ .export_values();
41
+ #endif
42
+
43
+ py::class_<ThreadPool>(m, "ThreadPool")
44
+ .def(py::init<>())
45
+ .def("start", &ThreadPool::start, py::arg("num_threads"))
46
+ .def("stop", &ThreadPool::stop)
47
+ .def("execute", &ThreadPool::execute, py::arg("job"));
48
+
49
+ py::class_<TimerQueue::TimerHandle>(m, "TimerHandle")
50
+ .def_readwrite("uid", &TimerQueue::TimerHandle::uid)
51
+ .def_readwrite("deadline", &TimerQueue::TimerHandle::deadline)
52
+ #ifndef YATQ_DISABLE_FUTURES
53
+ .def_readonly("result", &TimerQueue::TimerHandle::result)
54
+ #endif
55
+ ;
56
+
57
+ py::class_<TimerQueue>(m, "TimerQueue")
58
+ .def(py::init<ThreadPool*>(), py::arg("executor"))
59
+ .def("start", py::overload_cast<>(&TimerQueue::start))
60
+ #ifndef YATQ_DISABLE_PTHREAD
61
+ .def("start", py::overload_cast<int, yatq::utils::priority_t>(&TimerQueue::start), py::arg("sched_policy"), py::arg("priority") = yatq::utils::max_priority)
62
+ .def("start", py::overload_cast<int, int>(&TimerQueue::start), py::arg("sched_policy"), py::arg("priority"))
63
+ #endif
64
+ .def("stop", &TimerQueue::stop)
65
+ .def("enqueue", &TimerQueue::enqueue, py::arg("deadline"), py::arg("job"))
66
+ .def("cancel", &TimerQueue::cancel, py::arg("uid"))
67
+ .def("clear", &TimerQueue::clear)
68
+ .def("purge", &TimerQueue::purge)
69
+ .def("in_queue", &TimerQueue::in_queue, py::arg("uid"));
70
+
71
+ m.attr("__version__") = YATQ_VERSION;
72
+ }
@@ -0,0 +1,61 @@
1
+ #ifndef _YATQ_INTERNAL_CONCEPTS_H
2
+ #define _YATQ_INTERNAL_CONCEPTS_H
3
+
4
+ #include <chrono>
5
+ #include <concepts>
6
+
7
+ namespace yatq::internal {
8
+
9
+ // template<typename Clock>
10
+ // concept ClockGeneric = std::chrono::is_clock<Clock>;
11
+
12
+ // 'std::chrono::is_clock' seems to be not yet implemented for all architectures
13
+ // ok, we only these from 'Clock'
14
+ template<typename Clock>
15
+ concept ClockGeneric = requires (Clock::time_point t1, Clock::time_point t2) {
16
+ typename Clock::duration;
17
+ typename Clock::time_point;
18
+ { Clock::now() } -> std::convertible_to<typename Clock::time_point>;
19
+ { t1 > t2 } -> std::convertible_to<bool>;
20
+ { t1 <= t2 } -> std::convertible_to<bool>;
21
+ #ifndef YATQ_DISABLE_LOGGING
22
+ t1.time_since_epoch().count();
23
+ #endif
24
+ };
25
+
26
+ template<typename Executable>
27
+ concept ExecutableGeneric = requires {
28
+ typename Executable::result_type;
29
+ std::invocable<Executable>;
30
+ std::movable<Executable>;
31
+ };
32
+
33
+ #ifndef YATQ_DISABLE_FUTURES
34
+ template<typename Future, typename result_type>
35
+ concept ChainableFutureGeneric = requires(Future future, void (*then) (Future)) {
36
+ std::movable<Future>;
37
+ { future.get() } -> std::convertible_to<result_type>;
38
+ future.then(then);
39
+ };
40
+ #endif
41
+
42
+ template<typename Executor>
43
+ concept ExecutorGeneric =
44
+ #ifndef YATQ_DISABLE_FUTURES
45
+ ChainableFutureGeneric<typename Executor::Future, typename Executor::Executable::result_type> &&
46
+ #endif
47
+ requires(Executor executor, Executor::Executable job) {
48
+ typename Executor::Executable;
49
+ typename Executor::Executable::result_type;
50
+ std::movable<typename Executor::Executable>;
51
+ #ifndef YATQ_DISABLE_FUTURES
52
+ typename Executor::Future;
53
+ { executor.execute(job) } -> std::convertible_to<typename Executor::Future>;
54
+ #else
55
+ executor.execute(job);
56
+ #endif
57
+ };
58
+
59
+ }
60
+
61
+ #endif
@@ -0,0 +1,23 @@
1
+ #ifndef _YATQ_INTERNAL_LOG4CXX_PROXY_H
2
+ #define _YATQ_INTERNAL_LOG4CXX_PROXY_H
3
+
4
+ #ifndef YATQ_DISABLE_LOGGING
5
+
6
+ #include <log4cxx/logger.h>
7
+ #include <log4cxx/mdc.h>
8
+
9
+ #define SET_THREAD_TAG(tag) log4cxx::MDC::put("threadTag", tag)
10
+
11
+ #else
12
+
13
+ #define LOG4CXX_TRACE(...)
14
+ #define LOG4CXX_DEBUG(...)
15
+ #define LOG4CXX_INFO(...)
16
+ #define LOG4CXX_WARN(...)
17
+ #define LOG4CXX_ERROR(...)
18
+
19
+ #define SET_THREAD_TAG(...)
20
+
21
+ #endif
22
+
23
+ #endif
@@ -0,0 +1,32 @@
1
+ #ifndef _YATQ_INTERNAL_PROMISE_UTILS_H
2
+ #define _YATQ_INTERNAL_PROMISE_UTILS_H
3
+
4
+ #include <type_traits>
5
+
6
+ namespace yatq::internal {
7
+
8
+ template<typename result_type, typename Job, typename Promise>
9
+ std::enable_if_t<!std::is_void_v<result_type>> run_and_set_value(Job job, Promise promise) {
10
+ promise.set_value(job());
11
+ }
12
+
13
+ template<typename result_type, typename Job, typename Promise>
14
+ std::enable_if_t<std::is_void_v<result_type>> run_and_set_value(Job job, Promise promise) {
15
+ job();
16
+ promise.set_value();
17
+ }
18
+
19
+ template<typename result_type, typename Future, typename Promise>
20
+ std::enable_if_t<!std::is_void_v<result_type>> get_and_set_value(Future future, Promise promise) {
21
+ promise.set_value(future.get());
22
+ }
23
+
24
+ template<typename result_type, typename Future, typename Promise>
25
+ std::enable_if_t<std::is_void_v<result_type>> get_and_set_value(Future future, Promise promise) {
26
+ future.get();
27
+ promise.set_value();
28
+ }
29
+
30
+ }
31
+
32
+ #endif
@@ -0,0 +1,156 @@
1
+ #ifndef _YATQ_THREAD_POOL_H
2
+ #define _YATQ_THREAD_POOL_H
3
+
4
+ #include <condition_variable>
5
+ #include <cstdlib>
6
+ #include <deque>
7
+ #include <functional>
8
+ #include <mutex>
9
+ #include <string>
10
+ #include <thread>
11
+ #include <vector>
12
+
13
+ #ifndef YATQ_DISABLE_FUTURES
14
+ #define BOOST_THREAD_PROVIDES_FUTURE
15
+ #define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION
16
+ #include <boost/thread/future.hpp>
17
+ #endif
18
+
19
+ #include "yatq/internal/concepts.h"
20
+ #include "yatq/internal/promise_utils.h"
21
+ #include "yatq/internal/log4cxx_proxy.h"
22
+
23
+ namespace yatq {
24
+
25
+ using internal::ExecutableGeneric;
26
+
27
+ template<ExecutableGeneric _Executable = std::function<void(void)>>
28
+ class ThreadPool {
29
+ public:
30
+ using Executable = _Executable;
31
+ using result_type = Executable::result_type;
32
+ #ifndef YATQ_DISABLE_FUTURES
33
+ using Future = boost::future<result_type>;
34
+ #endif
35
+
36
+ private:
37
+ #ifndef YATQ_DISABLE_FUTURES
38
+ using Promise = boost::promise<result_type>;
39
+ #endif
40
+
41
+ typedef struct {
42
+ Executable job;
43
+ #ifndef YATQ_DISABLE_FUTURES
44
+ Promise promise;
45
+ #endif
46
+ } QueueEntry;
47
+
48
+ bool _running;
49
+ std::mutex _lock;
50
+ std::condition_variable _cond;
51
+ std::deque<QueueEntry> _queue;
52
+ std::vector<std::thread> _pool;
53
+
54
+ public:
55
+ /**
56
+ * create thread pool
57
+ */
58
+ ThreadPool(): _running(false) {}
59
+
60
+ /**
61
+ * start thread pool
62
+ * @param num_threads number of threads
63
+ */
64
+ void start(std::size_t num_threads) {
65
+ if (!_running) {
66
+ _running = true;
67
+ for (int i = 0; i < num_threads; ++i) {
68
+ std::string thread_tag = "pool thread #" + std::to_string(i);
69
+ auto thread = std::thread(&ThreadPool::thread_routine, this, thread_tag);
70
+ _pool.push_back(std::move(thread));
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * stop thread pool and join all the threads
77
+ */
78
+ void stop() {
79
+ if (_running) {
80
+ _running = false;
81
+ _cond.notify_all();
82
+ for (auto&& thread: _pool) {
83
+ if (thread.joinable()) {
84
+ thread.join();
85
+ }
86
+ }
87
+ _pool.clear();
88
+ }
89
+ }
90
+
91
+ /**
92
+ * execute job in a thread
93
+ * @param job job to execute
94
+ * @return future object. use it to obtain job result
95
+ */
96
+ #ifndef YATQ_DISABLE_FUTURES
97
+ Future
98
+ #else
99
+ void
100
+ #endif
101
+ execute(Executable job) {
102
+ #ifndef YATQ_DISABLE_FUTURES
103
+ Promise promise;
104
+ auto future = promise.get_future();
105
+ #endif
106
+ {
107
+ std::lock_guard<std::mutex> guard(_lock);
108
+ _queue.push_back({
109
+ std::move(job)
110
+ #ifndef YATQ_DISABLE_FUTURES
111
+ , std::move(promise)
112
+ #endif
113
+ });
114
+ }
115
+ _cond.notify_one();
116
+ #ifndef YATQ_DISABLE_FUTURES
117
+ return std::move(future);
118
+ #endif
119
+ }
120
+
121
+ private:
122
+ void thread_routine(const std::string& thread_tag) {
123
+ #ifndef YATQ_DISABLE_LOGGING
124
+ static auto logger = log4cxx::Logger::getLogger("yatq.thread_pool");
125
+ #endif
126
+
127
+ SET_THREAD_TAG(thread_tag);
128
+ LOG4CXX_INFO(logger, "Start");
129
+
130
+ while (_running) {
131
+ QueueEntry queue_entry;
132
+ {
133
+ std::unique_lock<std::mutex> guard(_lock);
134
+ _cond.wait(guard, [this] () { return !_queue.empty() || !_running; });
135
+ if (!_running) {
136
+ break;
137
+ }
138
+ queue_entry = std::move(_queue.front());
139
+ _queue.pop_front();
140
+ }
141
+ LOG4CXX_TRACE(logger, "Start job");
142
+ #ifndef YATQ_DISABLE_FUTURES
143
+ internal::run_and_set_value<result_type>(std::move(queue_entry.job), std::move(queue_entry.promise));
144
+ #else
145
+ queue_entry.job();
146
+ #endif
147
+ LOG4CXX_TRACE(logger, "Job complete");
148
+ }
149
+
150
+ LOG4CXX_INFO(logger, "Stop");
151
+ }
152
+ };
153
+
154
+ }
155
+
156
+ #endif
@@ -0,0 +1,368 @@
1
+ #ifndef _YATQ_TIMER_QUEUE_H
2
+ #define _YATQ_TIMER_QUEUE_H
3
+
4
+ #include <algorithm>
5
+ #include <chrono>
6
+ #include <condition_variable>
7
+ #include <mutex>
8
+ #include <thread>
9
+ #include <unordered_map>
10
+ #include <vector>
11
+
12
+ #ifndef YATQ_DISABLE_FUTURES
13
+ #define BOOST_THREAD_PROVIDES_FUTURE
14
+ #define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION
15
+ #include <boost/thread/future.hpp>
16
+ #endif
17
+
18
+ #include "yatq/internal/concepts.h"
19
+ #include "yatq/internal/promise_utils.h"
20
+ #include "yatq/internal/log4cxx_proxy.h"
21
+ #include "yatq/utils/logging_utils.h"
22
+ #ifndef YATQ_DISABLE_PTHREAD
23
+ #include "yatq/utils/sched_utils.h"
24
+ #endif
25
+ #include "yatq/thread_pool.h"
26
+
27
+ namespace yatq {
28
+
29
+ using internal::ClockGeneric;
30
+ using internal::ExecutorGeneric;
31
+
32
+ template<ExecutorGeneric _Executor = ThreadPool<>, ClockGeneric _Clock = std::chrono::system_clock>
33
+ class TimerQueue {
34
+ public:
35
+ using Clock = _Clock;
36
+ using Executor = _Executor;
37
+ using Executable = Executor::Executable;
38
+ using result_type = Executable::result_type;
39
+ #ifndef YATQ_DISABLE_FUTURES
40
+ using Future = boost::future<result_type>; // NB: doesn't have to match 'Executor::Future'
41
+ #endif
42
+
43
+ using uid_t = unsigned int;
44
+
45
+ typedef struct {
46
+ /**
47
+ * opaque timer uid. use it to cancel the timer or to check whether it is still in queue
48
+ */
49
+ uid_t uid;
50
+ /**
51
+ * scheduled execution timepoint. added for the sake of convenience
52
+ */
53
+ Clock::time_point deadline;
54
+ #ifndef YATQ_DISABLE_FUTURES
55
+ /**
56
+ * future object. use it to obtain job result
57
+ */
58
+ Future result;
59
+ #endif
60
+ } TimerHandle;
61
+
62
+ private:
63
+ #ifndef YATQ_DISABLE_FUTURES
64
+ using Promise = boost::promise<result_type>;
65
+ #endif
66
+
67
+ typedef struct {
68
+ Executable job;
69
+ #ifndef YATQ_DISABLE_FUTURES
70
+ Promise promise;
71
+ #endif
72
+ } MapEntry;
73
+
74
+ typedef struct {
75
+ uid_t uid;
76
+ Clock::time_point deadline;
77
+ } HeapEntry;
78
+
79
+ bool _running;
80
+ uid_t _next_uid;
81
+ mutable std::mutex _lock;
82
+ std::condition_variable _cond;
83
+ std::unordered_map<uid_t, MapEntry> _jobs;
84
+ std::vector<HeapEntry> _heap;
85
+ Executor* const _executor;
86
+ std::thread _thread;
87
+
88
+ public:
89
+ /**
90
+ * create timer queue
91
+ * @param executor raw pointer to the job executor; cannot be \a nullptr. ownership not taken
92
+ */
93
+ explicit TimerQueue(Executor* executor): _running(false), _next_uid(0), _executor(executor) {}
94
+
95
+ /**
96
+ * start timer queue thread with default scheduling parameters
97
+ */
98
+ void start() {
99
+ if (!_running) {
100
+ _running = true;
101
+ _thread = std::thread(&TimerQueue::demux, this);
102
+ }
103
+ }
104
+
105
+ #ifndef YATQ_DISABLE_PTHREAD
106
+ /**
107
+ * start timer queue thread with specified scheduling policy and priority
108
+ * @param sched_policy \a SCHED_OTHER | \a SCHED_RR | \a SCHED_FIFO
109
+ * @param priority \a yatq::utils::max_priority | \a yatq::utils::min_priority
110
+ */
111
+ void start(int sched_policy, utils::priority_t priority = utils::max_priority) {
112
+ start();
113
+ utils::set_sched_params(_thread.native_handle(), sched_policy, priority, "timer_queue");
114
+ }
115
+
116
+ /**
117
+ * start timer queue thread with specified scheduling policy and priority
118
+ * @param sched_policy \a SCHED_OTHER | \a SCHED_RR | \a SCHED_FIFO
119
+ * @param priority explicit priority
120
+ */
121
+ void start(int sched_policy, int priority) {
122
+ start();
123
+ utils::set_sched_params(_thread.native_handle(), sched_policy, priority, "timer_queue");
124
+ }
125
+ #endif
126
+
127
+ /**
128
+ * stop timer queue thread
129
+ */
130
+ void stop() {
131
+ if (_running) {
132
+ _running = false;
133
+ _cond.notify_one();
134
+ if (_thread.joinable()) {
135
+ _thread.join();
136
+ }
137
+ }
138
+ }
139
+
140
+ /**
141
+ * add timed job to the queue
142
+ * @param deadline scheduled execution timepoint
143
+ * @param job job to execute
144
+ * @return timer handle to obtain result or cancel
145
+ */
146
+ TimerHandle enqueue(const Clock::time_point& deadline, Executable job) {
147
+ #ifndef YATQ_DISABLE_LOGGING
148
+ static auto logger = log4cxx::Logger::getLogger("yatq.timer_queue");
149
+ #endif
150
+
151
+ #ifndef YATQ_DISABLE_FUTURES
152
+ Promise promise;
153
+ auto future = promise.get_future();
154
+ #endif
155
+ uid_t uid;
156
+ bool is_first;
157
+ {
158
+ std::lock_guard<std::mutex> guard(_lock);
159
+ uid = _next_uid++;
160
+ MapEntry map_entry {
161
+ std::move(job)
162
+ #ifndef YATQ_DISABLE_FUTURES
163
+ , std::move(promise)
164
+ #endif
165
+ };
166
+ _jobs.insert(std::make_pair(uid, std::move(map_entry)));
167
+ _heap.push_back(HeapEntry {uid, deadline});
168
+ std::push_heap(_heap.begin(), _heap.end(), TimerQueue::heap_cmp);
169
+ is_first = (_heap[0].uid == uid);
170
+ }
171
+ if (is_first) {
172
+ _cond.notify_one();
173
+ }
174
+ LOG4CXX_DEBUG(logger, "New timer uid=" + std::to_string(uid));
175
+ return {
176
+ uid
177
+ , deadline
178
+ #ifndef YATQ_DISABLE_FUTURES
179
+ , std::move(future)
180
+ #endif
181
+ };
182
+ }
183
+
184
+ /**
185
+ * cancel timed job
186
+ * @param uid timer uid
187
+ * @return \a true if timer was present in the queue; \a false otherwise
188
+ */
189
+ bool cancel(uid_t uid) {
190
+ #ifndef YATQ_DISABLE_LOGGING
191
+ static auto logger = log4cxx::Logger::getLogger("yatq.timer_queue");
192
+ #endif
193
+
194
+ bool was_removed;
195
+ bool was_first;
196
+ {
197
+ std::lock_guard<std::mutex> guard(_lock);
198
+ auto i = _jobs.find(uid);
199
+ if (i != _jobs.end()) {
200
+ LOG4CXX_DEBUG(logger, "Canceling timer uid=" + std::to_string(uid));
201
+ _jobs.erase(i);
202
+ was_removed = true;
203
+ was_first = (_heap[0].uid == uid);
204
+ }
205
+ else {
206
+ was_removed = false;
207
+ }
208
+ }
209
+ if (was_removed && was_first) {
210
+ _cond.notify_one();
211
+ }
212
+ return was_removed;
213
+ }
214
+
215
+ /**
216
+ * delete all jobs from the queue
217
+ */
218
+ void clear() {
219
+ #ifndef YATQ_DISABLE_LOGGING
220
+ static auto logger = log4cxx::Logger::getLogger("yatq.timer_queue");
221
+ #endif
222
+
223
+ std::size_t total_jobs;
224
+ std::size_t total_timers;
225
+ {
226
+ std::lock_guard<std::mutex> guard(_lock);
227
+ total_jobs = _jobs.size();
228
+ _jobs.clear();
229
+ total_timers = _heap.size();
230
+ _heap.clear();
231
+ }
232
+ if (total_jobs > 0) {
233
+ _cond.notify_one();
234
+ }
235
+ auto canceled_timers = total_timers - total_jobs;
236
+ LOG4CXX_DEBUG(logger, "Cleared " + std::to_string(total_jobs) + " timers and " + std::to_string(canceled_timers) + " canceled timers");
237
+ }
238
+
239
+ /**
240
+ * delete all canceled timers from the queue
241
+ */
242
+ void purge() {
243
+ #ifndef YATQ_DISABLE_LOGGING
244
+ static auto logger = log4cxx::Logger::getLogger("yatq.timer_queue");
245
+ #endif
246
+
247
+ std::size_t canceled_timers;
248
+ {
249
+ std::lock_guard<std::mutex> guard(_lock);
250
+ auto total_jobs = _jobs.size();
251
+ auto total_timers = _heap.size();
252
+ canceled_timers = total_timers - total_jobs;
253
+ if (total_timers > total_jobs) {
254
+ std::vector<HeapEntry> heap(total_jobs);
255
+ auto i = heap.begin();
256
+ for (auto&& heap_entry: _heap) {
257
+ if (_jobs.contains(heap_entry.uid)) {
258
+ *i++ = std::move(heap_entry);
259
+ }
260
+ else {
261
+ LOG4CXX_DEBUG(logger, "Timer uid=" + std::to_string(heap_entry.uid) + " has been canceled");
262
+ }
263
+ }
264
+ std::make_heap(heap.begin(), heap.end(), TimerQueue::heap_cmp);
265
+ _heap.swap(heap);
266
+ }
267
+ }
268
+ // NB: 'demux()' never waits on a canceled timer => no need to notify
269
+ LOG4CXX_DEBUG(logger, "Purged " + std::to_string(canceled_timers) + " canceled timers");
270
+ }
271
+
272
+ /**
273
+ * check whether a job is still in the queue
274
+ * @param uid timer uid
275
+ */
276
+ bool in_queue(uid_t uid) const {
277
+ std::lock_guard<std::mutex> guard(_lock);
278
+ return _jobs.contains(uid);
279
+ }
280
+
281
+ private:
282
+ static bool heap_cmp(const HeapEntry& lhs, const HeapEntry& rhs) {
283
+ return lhs.deadline > rhs.deadline; // NB: '>'
284
+ }
285
+
286
+ void demux() {
287
+ #ifndef YATQ_DISABLE_LOGGING
288
+ static auto logger = log4cxx::Logger::getLogger("yatq.timer_queue");
289
+ #endif
290
+
291
+ SET_THREAD_TAG("timer_queue");
292
+ LOG4CXX_INFO(logger, "Start");
293
+
294
+ std::unique_lock<std::mutex> guard(_lock);
295
+ while (_running) {
296
+ bool deadline_expired = false;
297
+ while (!_heap.empty()) {
298
+ auto current_uid = _heap[0].uid;
299
+ auto i = _jobs.find(current_uid);
300
+ if (i == _jobs.end()) {
301
+ LOG4CXX_DEBUG(logger, "Timer uid=" + std::to_string(current_uid) + " has been canceled");
302
+ std::pop_heap(_heap.begin(), _heap.end(), TimerQueue::heap_cmp);
303
+ _heap.pop_back();
304
+ deadline_expired = false;
305
+ continue;
306
+ }
307
+ if (!deadline_expired) {
308
+ auto now = Clock::now(); // NB: system call => context switch
309
+ deadline_expired = (_heap[0].deadline <= now);
310
+ }
311
+ if (deadline_expired) {
312
+ LOG4CXX_DEBUG(logger, "Executing timer uid=" + std::to_string(current_uid));
313
+ auto node = _jobs.extract(i);
314
+ std::pop_heap(_heap.begin(), _heap.end(), TimerQueue::heap_cmp);
315
+ _heap.pop_back();
316
+ auto map_entry = std::move(node.mapped());
317
+
318
+ guard.unlock();
319
+ #ifndef YATQ_DISABLE_FUTURES
320
+ auto future =
321
+ #endif
322
+ _executor->execute(std::move(map_entry.job));
323
+ guard.lock();
324
+
325
+ #ifndef YATQ_DISABLE_FUTURES
326
+ future.then( // future chaining -- this is why we use 'boost::future' instead of 'std::future'
327
+ boost::launch::sync, // FIXME: does not match the concept
328
+ [promise = std::move(map_entry.promise)]
329
+ (Executor::Future future) mutable
330
+ { internal::get_and_set_value<result_type>(std::move(future), std::move(promise)); }
331
+ );
332
+ #endif
333
+ deadline_expired = false;
334
+ }
335
+ else {
336
+ // NB: without this explicit cast duration type may be deduced incorrectly
337
+ // on Linux this leads to waiting for a random time point
338
+ std::chrono::time_point<Clock, typename Clock::duration> deadline = _heap[0].deadline;
339
+ LOG4CXX_TRACE(logger, "Wait until " + utils::time_point_to_string(deadline));
340
+ bool notified = _cond.wait_until(
341
+ guard,
342
+ deadline,
343
+ [this, current_uid] () {
344
+ return !_jobs.contains(current_uid) || (_heap[0].uid != current_uid) || !_running;
345
+ }
346
+ );
347
+ LOG4CXX_TRACE(logger, "Wake-up");
348
+ if (!_running) {
349
+ LOG4CXX_WARN(logger, "Stopping timer queue with unprocessed timers");
350
+ return;
351
+ }
352
+ if (!notified) { // => timeout
353
+ deadline_expired = true;
354
+ }
355
+ }
356
+ }
357
+ LOG4CXX_TRACE(logger, "Wait");
358
+ _cond.wait(guard, [this] () { return !_heap.empty() || !_running; });
359
+ LOG4CXX_TRACE(logger, "Wake-up");
360
+ }
361
+
362
+ LOG4CXX_INFO(logger, "Stop");
363
+ }
364
+ };
365
+
366
+ }
367
+
368
+ #endif
@@ -0,0 +1,51 @@
1
+ #ifndef _YATQ_UTILS_LOGGING_UTILS_H
2
+ #define _YATQ_UTILS_LOGGING_UTILS_H
3
+
4
+ #include <chrono>
5
+ #include <iomanip>
6
+ #include <locale>
7
+ #include <sstream>
8
+
9
+ #include <sched.h>
10
+
11
+ namespace yatq::utils {
12
+
13
+ template<typename time_point>
14
+ std::string time_point_to_string(const time_point& t) {
15
+ return std::to_string(t.time_since_epoch().count());
16
+ }
17
+
18
+ template<>
19
+ inline std::string time_point_to_string(const std::chrono::system_clock::time_point& time_point) {
20
+ std::stringstream stream;
21
+
22
+ std::time_t timestamp = std::chrono::system_clock::to_time_t(time_point);
23
+ std::tm* tm_info_p = std::localtime(&timestamp); // static initialization, no need for memory management
24
+ stream << std::put_time(tm_info_p, "%F %T");
25
+
26
+ // locale-aware decimal point
27
+ static const char decimal_point = std::use_facet<std::numpunct<char>>(std::locale("")).decimal_point();
28
+
29
+ auto time_point_ms = std::chrono::time_point_cast<std::chrono::milliseconds>(time_point);
30
+ auto milliseconds = time_point_ms.time_since_epoch().count() % 1000;
31
+ stream << decimal_point << std::setfill('0') << std::setw(3) << milliseconds;
32
+
33
+ return stream.str();
34
+ }
35
+
36
+ inline std::string sched_policy_to_string(int sched_policy) {
37
+ switch (sched_policy) {
38
+ case SCHED_OTHER:
39
+ return "SCHED_OTHER";
40
+ case SCHED_FIFO:
41
+ return "SCHED_FIFO";
42
+ case SCHED_RR:
43
+ return "SCHED_RR";
44
+ default:
45
+ return std::to_string(sched_policy);
46
+ }
47
+ }
48
+
49
+ }
50
+
51
+ #endif
@@ -0,0 +1,50 @@
1
+ #ifndef _YATQ_UTILS_SCHED_UTILS_H
2
+ #define _YATQ_UTILS_SCHED_UTILS_H
3
+
4
+ #include <cerrno>
5
+ #include <string>
6
+
7
+ #include <pthread.h>
8
+
9
+ #include "yatq/internal/log4cxx_proxy.h"
10
+ #include "yatq/utils/logging_utils.h"
11
+
12
+ namespace yatq::utils {
13
+
14
+ typedef enum {min_priority = 0, max_priority = -1} priority_t;
15
+
16
+ inline bool set_sched_params(pthread_t handle, int sched_policy, int priority, const std::string& thread_tag = "unspecified") {
17
+ #ifndef YATQ_DISABLE_LOGGING
18
+ static auto logger = log4cxx::Logger::getLogger("yatq.utils.sched");
19
+ #endif
20
+
21
+ int prev_sched_policy; // Linux implementation crashes on 'nullptr'
22
+ sched_param sched;
23
+ pthread_getschedparam(handle, &prev_sched_policy, &sched);
24
+ sched.sched_priority = priority;
25
+ if (pthread_setschedparam(handle, sched_policy, &sched) == 0) {
26
+ LOG4CXX_INFO(logger, "Set sched params thread=" + thread_tag + " policy=" + sched_policy_to_string(sched_policy) + " priority=" + std::to_string(priority));
27
+ return true;
28
+ }
29
+ else {
30
+ LOG4CXX_WARN(logger, "Failed to set sched params thread=" + thread_tag + ": " + std::string(std::strerror(errno)));
31
+ return false;
32
+ }
33
+ }
34
+
35
+ inline bool set_sched_params(pthread_t handle, int sched_policy, priority_t priority_tag, const std::string& thread_tag = "unspecified") {
36
+ int priority;
37
+ switch (priority_tag) {
38
+ case min_priority:
39
+ priority = sched_get_priority_min(sched_policy);
40
+ break;
41
+ case max_priority:
42
+ priority = sched_get_priority_max(sched_policy);
43
+ break;
44
+ }
45
+ return set_sched_params(handle, sched_policy, priority, thread_tag);
46
+ }
47
+
48
+ }
49
+
50
+ #endif
@@ -0,0 +1,6 @@
1
+ #ifndef _YATQ_VERSION_H
2
+ #define _YATQ_VERSION_H
3
+
4
+ #define YATQ_VERSION "1.0.2"
5
+
6
+ #endif
@@ -0,0 +1,19 @@
1
+ #ifndef _YATQ_BINDINGS_PYTHON_YATQ_TRAITS_H
2
+ #define _YATQ_BINDINGS_PYTHON_YATQ_TRAITS_H
3
+
4
+ #define PYBIND11_DETAILED_ERROR_MESSAGES
5
+ #include <pybind11/pybind11.h>
6
+
7
+ #define BOOST_THREAD_PROVIDES_FUTURE
8
+ #define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION
9
+ #include <boost/thread/future.hpp>
10
+
11
+ namespace pybind11::detail {
12
+
13
+ // only needed for 'result_type = void'
14
+ template <>
15
+ struct is_move_constructible<boost::future<void>>: std::true_type {};
16
+
17
+ }
18
+
19
+ #endif
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["setuptools >= 65.0.0", "pybind11 >= 2.13.6", "wheel >= 0.44.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pytq-cxx"
7
+ version = "1.0.2"
8
+ authors = [{name="Nikita Vaganov", email="nikita.e.vaganov@gmail.com"}]
9
+ description = "single-threaded timer demultiplexer backed by C++ implementation"
10
+ readme = "README_pytq.md"
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3",
13
+ "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
14
+ ]
15
+ requires-python = ">=3.7"
16
+
17
+ [project.urls]
18
+ "Homepage" = "https://github.com/vaganov/yatq"
19
+ "Bug Tracker" = "https://github.com/vaganov/yatq/issues"
@@ -0,0 +1,5 @@
1
+ from _yatq import __version__, TimerQueue, TimerHandle, ThreadPool, utils
2
+ from .awaitable_future import AwaitableFuture
3
+
4
+
5
+ __all__ = ['__version__', 'TimerQueue', 'TimerHandle', 'ThreadPool', 'utils', 'AwaitableFuture']
@@ -0,0 +1,19 @@
1
+ import asyncio
2
+
3
+
4
+ __all__ = ['AwaitableFuture']
5
+
6
+
7
+ class AwaitableFuture:
8
+ def __init__(self, future):
9
+ self._future = future
10
+
11
+ def __await__(self):
12
+ while not self._future.is_ready():
13
+ yield
14
+ return self._future.get() # NB: return, not yield
15
+
16
+ async def get(self):
17
+ while not self._future.is_ready():
18
+ await asyncio.sleep(0)
19
+ return self._future.get()
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.1
2
+ Name: pytq-cxx
3
+ Version: 1.0.2
4
+ Summary: single-threaded timer demultiplexer backed by C++ implementation
5
+ Author-email: Nikita Vaganov <nikita.e.vaganov@gmail.com>
6
+ Project-URL: Homepage, https://github.com/vaganov/yatq
7
+ Project-URL: Bug Tracker, https://github.com/vaganov/yatq/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
10
+ Requires-Python: >=3.7
11
+ Description-Content-Type: text/markdown
12
+
13
+ # pytq-cxx
14
+ **pytq-cxx** is a _python_ wrapper for **yatq** -- a _C++_ template library. For the documentation and examples please
15
+ see [yatq homepage](https://github.com/vaganov/yatq)
16
+
17
+ **NB:** This package has nothing to do with [pytq package](https://pypi.org/project/pytq)
18
+
19
+ **NB:** Module name is, however, **pytq**
20
+
21
+ from pytq import ThreadPool, TimerQueue, AwaitableFuture
@@ -0,0 +1,22 @@
1
+ MANIFEST.in
2
+ README_pytq.md
3
+ pyproject.toml
4
+ setup.py
5
+ _yatq/bind.cpp
6
+ _yatq/traits.h
7
+ _yatq/include/yatq/thread_pool.h
8
+ _yatq/include/yatq/timer_queue.h
9
+ _yatq/include/yatq/version.h
10
+ _yatq/include/yatq/internal/concepts.h
11
+ _yatq/include/yatq/internal/log4cxx_proxy.h
12
+ _yatq/include/yatq/internal/promise_utils.h
13
+ _yatq/include/yatq/utils/logging_utils.h
14
+ _yatq/include/yatq/utils/sched_utils.h
15
+ pytq/__init__.py
16
+ pytq/awaitable_future.py
17
+ pytq_cxx.egg-info/PKG-INFO
18
+ pytq_cxx.egg-info/SOURCES.txt
19
+ pytq_cxx.egg-info/dependency_links.txt
20
+ pytq_cxx.egg-info/top_level.txt
21
+ setup_helpers/find_dep_headers.cmake
22
+ setup_helpers/find_dep_libs.cmake
@@ -0,0 +1,2 @@
1
+ _yatq
2
+ pytq
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,60 @@
1
+ from pathlib import Path
2
+ import subprocess
3
+ import sys
4
+
5
+ from setuptools import setup
6
+ from pybind11.setup_helpers import Pybind11Extension, build_ext
7
+
8
+
9
+ def ugly_helper(): # https://github.com/vaganov/yatq/issues/2
10
+ dirpath = str(Path(__file__).parent / 'setup_helpers')
11
+
12
+ args = ['cmake', '-P', 'find_dep_headers.cmake']
13
+ result = subprocess.run(args, cwd=dirpath, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
14
+ if result.returncode != 0:
15
+ print(result.stderr.decode(), end='', file=sys.stderr, flush=True)
16
+ raise RuntimeError(f"{' '.join(args)} returned {result.returncode}")
17
+
18
+ _inc_dirs = set(line.strip() for line in result.stderr.decode().split()) # sic! 'stderr'
19
+ inc_dirs = list(_inc_dirs)
20
+
21
+ args = ['cmake', '-P', 'find_dep_libs.cmake']
22
+ result = subprocess.run(args, cwd=dirpath, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
23
+ if result.returncode != 0:
24
+ print(result.stderr.decode(), end='', file=sys.stderr, flush=True)
25
+ raise RuntimeError(f"{' '.join(args)} returned {result.returncode}")
26
+
27
+ lib_prefix = 'lib'
28
+ lib_prefix_len = len(lib_prefix)
29
+
30
+ _lib_dirs = set()
31
+ libs = []
32
+ for line in result.stderr.decode().split(): # sic! 'stderr'
33
+ path = Path(line.strip())
34
+ _lib_dirs.add(str(path.parent))
35
+ libname = path.stem
36
+ if libname.startswith(lib_prefix):
37
+ libname = libname[lib_prefix_len:]
38
+ libs.append(libname)
39
+ lib_dirs = list(_lib_dirs)
40
+
41
+ return {'inc_dirs': inc_dirs, 'lib_dirs': lib_dirs, 'libs': libs}
42
+
43
+
44
+ HELPER_FLAGS = ugly_helper()
45
+
46
+
47
+ setup(
48
+ packages=['pytq'],
49
+ ext_modules=[
50
+ Pybind11Extension(
51
+ '_yatq',
52
+ ['_yatq/bind.cpp'],
53
+ include_dirs=HELPER_FLAGS['inc_dirs'] + ['_yatq/include'], # https://github.com/vaganov/yatq/issues/4
54
+ library_dirs=HELPER_FLAGS['lib_dirs'],
55
+ libraries=HELPER_FLAGS['libs'],
56
+ cxx_std=20,
57
+ ),
58
+ ],
59
+ cmdclass=dict(build_ext=build_ext),
60
+ )
@@ -0,0 +1,9 @@
1
+ #! cmake -P
2
+ cmake_minimum_required(VERSION 3.22)
3
+
4
+ set(INCLUDE_PATHS /usr/include /usr/local/include)
5
+
6
+ find_path(boost_include_dir boost PATHS ${INCLUDE_PATHS} REQUIRED)
7
+ message("${boost_include_dir}")
8
+ find_path(log4cxx_include_dir log4cxx PATHS ${INCLUDE_PATHS} REQUIRED)
9
+ message("${log4cxx_include_dir}")
@@ -0,0 +1,9 @@
1
+ #! cmake -P
2
+ cmake_minimum_required(VERSION 3.22)
3
+
4
+ set(LIB_PATHS /lib /lib/x86_64-linux-gnu /usr/lib /usr/local/lib)
5
+
6
+ find_library(boost_thread_lib NAMES boost_thread boost_thread-mt PATHS ${LIB_PATHS} REQUIRED)
7
+ message("${boost_thread_lib}")
8
+ find_library(log4cxx_lib log4cxx PATHS ${LIB_PATHS} REQUIRED)
9
+ message("${log4cxx_lib}")