This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-cpp.git


The following commit(s) were added to refs/heads/master by this push:
     new adec06b  fix: bound Ticker callbacks and make policy loading stop 
safely (#282)
adec06b is described below

commit adec06bcb0aadd9392ec9f734fcf25c4b4274a89
Author: Emery <[email protected]>
AuthorDate: Sat Nov 7 10:13:34 2026 +0800

    fix: bound Ticker callbacks and make policy loading stop safely (#282)
---
 casbin/enforcer_synced.cpp   |  5 ++-
 casbin/util/ticker.cpp       | 32 ++++++++++------
 include/casbin/util/ticker.h |  9 ++++-
 tests/CMakeLists.txt         |  1 +
 tests/ticker_test.cpp        | 88 ++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 119 insertions(+), 16 deletions(-)

diff --git a/casbin/enforcer_synced.cpp b/casbin/enforcer_synced.cpp
index cf8c1c7..1c167f9 100644
--- a/casbin/enforcer_synced.cpp
+++ b/casbin/enforcer_synced.cpp
@@ -122,7 +122,8 @@ bool SyncedEnforcer ::IsAutoLoadingRunning() {
 
 // StopAutoLoadPolicy causes the thread to exit
 void SyncedEnforcer ::StopAutoLoadPolicy() {
-    ticker->stop();
+    if (ticker)
+        ticker->stop();
     autoLoadRunning = false;
 }
 
@@ -590,4 +591,4 @@ bool SyncedEnforcer::SyncedEnforceWithMatcher(const 
std::string& matcher, const
 
 } // namespace casbin
 
-#endif // ENFORCER_SYNCED_CPP
\ No newline at end of file
+#endif // ENFORCER_SYNCED_CPP
diff --git a/casbin/util/ticker.cpp b/casbin/util/ticker.cpp
index 90dfeca..45009fc 100644
--- a/casbin/util/ticker.cpp
+++ b/casbin/util/ticker.cpp
@@ -33,29 +33,37 @@ Ticker::~Ticker() {
 }
 
 void Ticker::start() {
+    std::lock_guard<std::mutex> lifecycle(_lifecycleMutex);
     if (_running) return;
+    if (_worker.joinable())
+        _worker.join();
     _running = true;
-    _futures1.push_back(std::async(std::launch::async, &Ticker::timer_loop, 
this));
+    _worker = std::thread(&Ticker::timer_loop, this);
 }
 
 void Ticker::stop() {
-    _running = false;
-    std::lock_guard<std::mutex> lock(_tickIntervalMutex);
-    for (auto&& f : _futures1) {
-        f.wait();
-    }
-    for (auto&& f : _futures2) {
-        f.wait();
+    std::lock_guard<std::mutex> lifecycle(_lifecycleMutex);
+    {
+        std::lock_guard<std::mutex> lock(_tickIntervalMutex);
+        _running = false;
     }
+    _wake.notify_all();
+    if (_worker.joinable() && _worker.get_id() != std::this_thread::get_id())
+        _worker.join();
 }
 
 void Ticker::timer_loop() {
+    std::unique_lock<std::mutex> lock(_tickIntervalMutex);
     while (_running) {
-        {
-            std::lock_guard<std::mutex> lock(_tickIntervalMutex);
-            _futures2.push_back(std::async(std::launch::async, _onTick));
-            std::this_thread::sleep_for(_tickInterval);
+        lock.unlock();
+        try {
+            _onTick();
+        } catch (...) {
+            // The previous async callbacks also kept exceptions in their
+            // futures. Keep a callback failure from terminating the scheduler.
         }
+        lock.lock();
+        _wake.wait_for(lock, _tickInterval, [this] { return !_running.load(); 
});
     }
 }
 
diff --git a/include/casbin/util/ticker.h b/include/casbin/util/ticker.h
index 1d6a12c..f3ed426 100644
--- a/include/casbin/util/ticker.h
+++ b/include/casbin/util/ticker.h
@@ -18,9 +18,13 @@
 #define TICKER_H
 
 #include <chrono>
+#include <atomic>
+#include <condition_variable>
 #include <functional>
 #include <future>
 #include <mutex>
+#include <thread>
+#include <vector>
 
 namespace casbin {
 
@@ -43,9 +47,10 @@ private:
     on_tick_t _onTick;
     tick_interval_t _tickInterval;
     std::atomic_bool _running;
+    std::mutex _lifecycleMutex;
     std::mutex _tickIntervalMutex;
-    future_vec _futures1;
-    future_vec _futures2;
+    std::condition_variable _wake;
+    std::thread _worker;
 };
 
 }; // namespace casbin
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 1b94ae1..3fd5b15 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -24,6 +24,7 @@ if(CASBIN_BUILD_TEST)
 
   set(CASBIN_TEST_SOURCE
     built_in_functions_test.cpp
+    ticker_test.cpp
     config_test.cpp
     enforcer_test.cpp
     enforcer_cached_test.cpp
diff --git a/tests/ticker_test.cpp b/tests/ticker_test.cpp
new file mode 100644
index 0000000..d2fbb8f
--- /dev/null
+++ b/tests/ticker_test.cpp
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include <gtest/gtest.h>
+#include <atomic>
+#include <chrono>
+#include <condition_variable>
+#include <future>
+#include <mutex>
+#include <thread>
+
+#include "casbin/enforcer_synced.h"
+#include "casbin/util/ticker.h"
+
+namespace {
+
+TEST(ReleaseTicker, SlowCallbacksDoNotOverlapAndCanRestart) {
+    std::atomic<int> active{0};
+    std::atomic<int> calls{0};
+    std::atomic<bool> overlap{false};
+    std::mutex progressMutex;
+    std::condition_variable progress;
+    casbin::Ticker ticker([&] {
+        if (active.fetch_add(1) != 0)
+            overlap = true;
+        {
+            std::lock_guard<std::mutex> lock(progressMutex);
+            ++calls;
+        }
+        progress.notify_one();
+        std::this_thread::sleep_for(std::chrono::milliseconds(20));
+        --active;
+    }, std::chrono::milliseconds(1));
+    const auto waitForCalls = [&](int minimum) {
+        std::unique_lock<std::mutex> lock(progressMutex);
+        return progress.wait_for(lock, std::chrono::seconds(5), [&] { return 
calls.load() >= minimum; });
+    };
+    ticker.start();
+    // Wait for observable callback progress, including on busy CI runners.
+    EXPECT_TRUE(waitForCalls(3));
+    ticker.stop();
+    EXPECT_FALSE(overlap);
+    EXPECT_EQ(active.load(), 0);
+    EXPECT_GE(calls.load(), 1);
+    const auto before = calls.load();
+    ticker.start();
+    EXPECT_TRUE(waitForCalls(before + 1));
+    ticker.stop();
+    ticker.stop();
+    EXPECT_GT(calls.load(), before);
+    EXPECT_FALSE(overlap);
+}
+
+TEST(ReleaseTicker, StopInterruptsLongWait) {
+    std::promise<void> entered;
+    casbin::Ticker ticker([&] { entered.set_value(); }, 
std::chrono::seconds(10));
+    ticker.start();
+    auto ready = entered.get_future().wait_for(std::chrono::seconds(2));
+    EXPECT_EQ(ready, std::future_status::ready);
+    const auto before = std::chrono::steady_clock::now();
+    ticker.stop();
+    EXPECT_LT(std::chrono::steady_clock::now() - before, 
std::chrono::seconds(2));
+}
+
+TEST(ReleaseTicker, AutoLoadStopIsSafeBeforeStart) {
+    casbin::SyncedEnforcer enforcer;
+    enforcer.StopAutoLoadPolicy();
+    enforcer.StopAutoLoadPolicy();
+    EXPECT_FALSE(enforcer.IsAutoLoadingRunning());
+}
+
+} // namespace


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to