From ada48d90215fbe5647c16d68d6f1e9b7395d25e0 Mon Sep 17 00:00:00 2001 From: elsid Date: Sat, 22 Mar 2025 14:39:51 +0100 Subject: [PATCH] Reduce a chance to have a deadlock in the AsyncNavMeshUpdater * Do not fail tile generation if debug mesh writing fails. * Mark some functions as noexcept to better crash than have a deadlock. * Unlock tile and remove job if there on exception while processing it. --- .../detournavigator/asyncnavmeshupdater.cpp | 102 ++++++++++++++++++ .../detournavigator/asyncnavmeshupdater.cpp | 45 ++++++-- .../detournavigator/asyncnavmeshupdater.hpp | 4 +- 3 files changed, 138 insertions(+), 13 deletions(-) diff --git a/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp b/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp index ea9efc3df2..3094e1cea6 100644 --- a/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp +++ b/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp @@ -5,7 +5,9 @@ #include #include #include +#include #include +#include #include @@ -372,6 +374,106 @@ namespace } } + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_recast_mesh) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteRecastMeshToFile = true; + const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest"); + mSettings.mRecastMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_TRUE(std::filesystem::exists(dir / "0.0.recastmesh.obj")); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_recast_mesh_with_revision) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteRecastMeshToFile = true; + mSettings.mEnableRecastMeshFileNameRevision = true; + const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest"); + mSettings.mRecastMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_TRUE(std::filesystem::exists(dir / "0.0.recastmesh.1.2.obj")); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, writing_recast_mesh_to_absent_file_should_not_fail_tile_generation) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteRecastMeshToFile = true; + const std::filesystem::path dir = TestingOpenMW::outputDir() / "absent"; + mSettings.mRecastMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_NE(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0u); + EXPECT_FALSE(std::filesystem::exists(dir)); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_navmesh) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteNavMeshToFile = true; + const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest"); + mSettings.mNavMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_TRUE(std::filesystem::exists(dir / "all_tiles_navmesh.bin")); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_navmesh_with_revision) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteNavMeshToFile = true; + mSettings.mEnableNavMeshFileNameRevision = true; + const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest"); + mSettings.mNavMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_TRUE(std::filesystem::exists(dir / "all_tiles_navmesh.1.1.bin")); + } + + TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, writing_navmesh_to_absent_file_should_not_fail_tile_generation) + { + mRecastMeshManager.setWorldspace(mWorldspace, nullptr); + addHeightFieldPlane(mRecastMeshManager); + mSettings.mEnableWriteNavMeshToFile = true; + const std::filesystem::path dir = TestingOpenMW::outputDir() / "absent"; + mSettings.mNavMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; + Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; + AsyncNavMeshUpdater updater(mSettings, mRecastMeshManager, mOffMeshConnectionsManager, nullptr); + const auto navMeshCacheItem = std::make_shared(1, mSettings); + const std::map changedTiles{ { TilePosition{ 0, 0 }, ChangeType::add } }; + updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles); + updater.wait(WaitConditionType::allJobsDone, &mListener); + EXPECT_NE(navMeshCacheItem->lockConst()->getImpl().getTileRefAt(0, 0, 0), 0u); + EXPECT_FALSE(std::filesystem::exists(dir)); + } + struct DetourNavigatorSpatialJobQueueTest : Test { const AgentBounds mAgentBounds{ CollisionShapeType::Aabb, osg::Vec3f(1, 1, 1) }; diff --git a/components/detournavigator/asyncnavmeshupdater.cpp b/components/detournavigator/asyncnavmeshupdater.cpp index 71c8bbc2d3..5f82c85cf9 100644 --- a/components/detournavigator/asyncnavmeshupdater.cpp +++ b/components/detournavigator/asyncnavmeshupdater.cpp @@ -453,9 +453,9 @@ namespace DetourNavigator Misc::setCurrentThreadIdlePriority(); while (!mShouldStop) { - try + if (JobIt job = getNextJob(); job != mJobs.end()) { - if (JobIt job = getNextJob(); job != mJobs.end()) + try { const JobStatus status = processJob(*job); Log(Debug::Debug) << "Processed job " << job->mId << " with status=" << status @@ -480,12 +480,20 @@ namespace DetourNavigator } } } - else - cleanupLastUpdates(); + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to process navmesh job " << job->mId + << " for worldspace=" << job->mWorldspace << " agent=" << job->mAgentBounds + << " changedTile=(" << job->mChangedTile << ")" + << " changeType=" << job->mChangeType + << " by thread=" << std::this_thread::get_id() << ": " << e.what(); + unlockTile(job->mId, job->mAgentBounds, job->mChangedTile); + removeJob(job); + } } - catch (const std::exception& e) + else { - Log(Debug::Error) << "AsyncNavMeshUpdater::process exception: " << e.what(); + cleanupLastUpdates(); } } Log(Debug::Debug) << "Stop navigator jobs processing by thread=" << std::this_thread::get_id(); @@ -493,7 +501,8 @@ namespace DetourNavigator JobStatus AsyncNavMeshUpdater::processJob(Job& job) { - Log(Debug::Debug) << "Processing job " << job.mId << " for agent=(" << job.mAgentBounds << ")" + Log(Debug::Debug) << "Processing job " << job.mId << " for worldspace=" << job.mWorldspace + << " agent=" << job.mAgentBounds << "" << " changedTile=(" << job.mChangedTile << ")" << " changeType=" << job.mChangeType << " by thread=" << std::this_thread::get_id(); @@ -543,7 +552,14 @@ namespace DetourNavigator return JobStatus::Done; } - writeDebugRecastMesh(mSettings, job.mChangedTile, *recastMesh); + try + { + writeDebugRecastMesh(mSettings, job.mChangedTile, *recastMesh); + } + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to write debug recast mesh: " << e.what(); + } NavMeshTilesCache::Value cachedNavMeshData = mNavMeshTilesCache.get(job.mAgentBounds, job.mChangedTile, *recastMesh); @@ -666,12 +682,19 @@ namespace DetourNavigator mPresentTiles.insert(std::make_tuple(job.mAgentBounds, job.mChangedTile)); } - writeDebugNavMesh(mSettings, navMeshCacheItem, navMeshVersion); + try + { + writeDebugNavMesh(mSettings, navMeshCacheItem, navMeshVersion); + } + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to write debug navmesh: " << e.what(); + } return isSuccess(status) ? JobStatus::Done : JobStatus::Fail; } - JobIt AsyncNavMeshUpdater::getNextJob() + JobIt AsyncNavMeshUpdater::getNextJob() noexcept { std::unique_lock lock(mMutex); @@ -746,7 +769,7 @@ namespace DetourNavigator return mJobs.size(); } - void AsyncNavMeshUpdater::cleanupLastUpdates() + void AsyncNavMeshUpdater::cleanupLastUpdates() noexcept { const auto now = std::chrono::steady_clock::now(); diff --git a/components/detournavigator/asyncnavmeshupdater.hpp b/components/detournavigator/asyncnavmeshupdater.hpp index 7877ff8082..95919ed770 100644 --- a/components/detournavigator/asyncnavmeshupdater.hpp +++ b/components/detournavigator/asyncnavmeshupdater.hpp @@ -244,7 +244,7 @@ namespace DetourNavigator inline JobStatus handleUpdateNavMeshStatus(UpdateNavMeshStatus status, const Job& job, const GuardedNavMeshCacheItem& navMeshCacheItem, const RecastMesh& recastMesh); - JobIt getNextJob(); + inline JobIt getNextJob() noexcept; void postThreadJob(JobIt job, std::deque& queue); @@ -254,7 +254,7 @@ namespace DetourNavigator inline std::size_t getTotalJobs() const; - void cleanupLastUpdates(); + inline void cleanupLastUpdates() noexcept; inline void waitUntilJobsDoneForNotPresentTiles(Loading::Listener* listener);