Skip to content

Commit

Permalink
Asynchronous script cache reloading functionality (#488)
Browse files Browse the repository at this point in the history
* Implement async reload functionality
* Move map tokenizer and getter to ElunaConfig
  • Loading branch information
Foereaper authored Jul 19, 2024
1 parent fa6587f commit 12ac896
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 84 deletions.
26 changes: 26 additions & 0 deletions ElunaConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ void ElunaConfig::Initialize()
SetConfig(CONFIG_ELUNA_ONLY_ON_MAPS, "Eluna.OnlyOnMaps", "");
SetConfig(CONFIG_ELUNA_REQUIRE_PATH_EXTRA, "Eluna.RequirePaths", "");
SetConfig(CONFIG_ELUNA_REQUIRE_CPATH_EXTRA, "Eluna.RequireCPaths", "");

// tokenize OnlyOnMaps
m_requiredMaps.clear();
std::istringstream maps(GetConfig(CONFIG_ELUNA_ONLY_ON_MAPS));
while (maps.good())
{
std::string mapIdStr;
std::getline(maps, mapIdStr, ',');
if (maps.fail() || maps.bad())
break;
try {
uint32 mapId = std::stoul(mapIdStr);
m_requiredMaps.emplace_back(mapId);
}
catch (std::exception&) {
ELUNA_LOG_ERROR("[Eluna]: Error tokenizing Eluna.OnlyOnMaps, invalid config value '%s'", mapIdStr.c_str());
}
}
}

void ElunaConfig::SetConfig(ElunaConfigBoolValues index, char const* fieldname, bool defvalue)
Expand Down Expand Up @@ -69,3 +87,11 @@ bool ElunaConfig::IsElunaCompatibilityMode()
{
return GetConfig(CONFIG_ELUNA_COMPATIBILITY_MODE);
}

bool ElunaConfig::ShouldMapLoadEluna(uint32 id)
{
if (!m_requiredMaps.size())
return true;

return (std::find(m_requiredMaps.begin(), m_requiredMaps.end(), id) != m_requiredMaps.end());
}
3 changes: 3 additions & 0 deletions ElunaConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,16 @@ class ElunaConfig

bool IsElunaEnabled();
bool IsElunaCompatibilityMode();
bool ShouldMapLoadEluna(uint32 mapId);

private:
bool _configBoolValues[CONFIG_ELUNA_BOOL_COUNT];
std::string _configStringValues[CONFIG_ELUNA_STRING_COUNT];

void SetConfig(ElunaConfigBoolValues index, char const* fieldname, bool defvalue);
void SetConfig(ElunaConfigStringValues index, char const* fieldname, std::string defvalue);

std::list<uint32> m_requiredMaps;
};

#define sElunaConfig ElunaConfig::instance()
Expand Down
137 changes: 79 additions & 58 deletions ElunaLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "ElunaIncludes.h"
#include <fstream>
#include <sstream>
#include <thread>

#ifdef USING_BOOST
#include <boost/filesystem.hpp>
Expand Down Expand Up @@ -53,7 +54,7 @@ void ElunaUpdateListener::handleFileAction(efsw::WatchID /*watchid*/, std::strin
}
#endif

ElunaLoader::ElunaLoader()
ElunaLoader::ElunaLoader() : m_cacheState(SCRIPT_CACHE_NONE)
{
#ifdef TRINITY
lua_scriptWatcher = -1;
Expand All @@ -68,6 +69,10 @@ ElunaLoader* ElunaLoader::instance()

ElunaLoader::~ElunaLoader()
{
// join any previously created reload thread so it can exit cleanly
if (m_reloadThread.joinable())
m_reloadThread.join();

#ifdef TRINITY
if (lua_scriptWatcher >= 0)
{
Expand All @@ -77,29 +82,58 @@ ElunaLoader::~ElunaLoader()
#endif
}

void ElunaLoader::ReloadScriptCache()
{
// if the internal cache state is anything other than ready, we return
if (m_cacheState != SCRIPT_CACHE_READY)
{
ELUNA_LOG_DEBUG("[Eluna]: Script cache not ready, skipping reload");
return;
}

// try to join any previous thread before starting a new one, just in case
if (m_reloadThread.joinable())
m_reloadThread.join();

// set the internal cache state to reinit
m_cacheState = SCRIPT_CACHE_REINIT;

// create new thread to load scripts asynchronously
m_reloadThread = std::thread(&ElunaLoader::LoadScripts, this);
ELUNA_LOG_DEBUG("[Eluna]: Script cache reload thread started");
}

void ElunaLoader::LoadScripts()
{
lua_folderpath = sElunaConfig->GetConfig(CONFIG_ELUNA_SCRIPT_PATH);
const std::string& lua_path_extra = sElunaConfig->GetConfig(CONFIG_ELUNA_REQUIRE_PATH_EXTRA);
const std::string& lua_cpath_extra = sElunaConfig->GetConfig(CONFIG_ELUNA_REQUIRE_CPATH_EXTRA);
// only reload the cache if it is either in a reinit state or not loaded at all
if (m_cacheState != SCRIPT_CACHE_REINIT && m_cacheState != SCRIPT_CACHE_NONE)
return;

// set the cache state to loading
m_cacheState = SCRIPT_CACHE_LOADING;

uint32 oldMSTime = ElunaUtil::GetCurrTime();
lua_scripts.clear();
lua_extensions.clear();
combined_scripts.clear();

std::string lua_folderpath = sElunaConfig->GetConfig(CONFIG_ELUNA_SCRIPT_PATH);
const std::string& lua_path_extra = sElunaConfig->GetConfig(CONFIG_ELUNA_REQUIRE_PATH_EXTRA);
const std::string& lua_cpath_extra = sElunaConfig->GetConfig(CONFIG_ELUNA_REQUIRE_CPATH_EXTRA);

#ifndef ELUNA_WINDOWS
if (lua_folderpath[0] == '~')
if (const char* home = getenv("HOME"))
lua_folderpath.replace(0, 1, home);
#endif

ELUNA_LOG_INFO("[Eluna]: Searching for scripts in `%s`", lua_folderpath.c_str());
lua_requirepath.clear();
lua_requirecpath.clear();

// open a new temporary Lua state to compile bytecode in
lua_State* L = luaL_newstate();
luaL_openlibs(L);

// clear all cache variables
m_requirePath.clear();
m_requirecPath.clear();

// read and compile all scripts
ReadFiles(L, lua_folderpath);

Expand All @@ -111,35 +145,22 @@ void ElunaLoader::LoadScripts()

// append our custom require paths and cpaths if the config variables are not empty
if (!lua_path_extra.empty())
lua_requirepath += lua_path_extra;
m_requirePath += lua_path_extra;

if (!lua_cpath_extra.empty())
lua_requirecpath += lua_cpath_extra;
m_requirecPath += lua_cpath_extra;

// Erase last ;
if (!lua_requirepath.empty())
lua_requirepath.erase(lua_requirepath.end() - 1);
if (!m_requirePath.empty())
m_requirePath.erase(m_requirePath.end() - 1);

if (!lua_requirecpath.empty())
lua_requirecpath.erase(lua_requirecpath.end() - 1);
if (!m_requirecPath.empty())
m_requirecPath.erase(m_requirecPath.end() - 1);

ELUNA_LOG_INFO("[Eluna]: Loaded and precompiled %u scripts in %u ms", uint32(combined_scripts.size()), ElunaUtil::GetTimeDiff(oldMSTime));
requiredMaps.clear();
std::istringstream maps(sElunaConfig->GetConfig(CONFIG_ELUNA_ONLY_ON_MAPS));
while (maps.good())
{
std::string mapIdStr;
std::getline(maps, mapIdStr, ',');
if (maps.fail() || maps.bad())
break;
try {
uint32 mapId = std::stoul(mapIdStr);
requiredMaps.emplace_back(mapId);
}
catch (std::exception&) {
ELUNA_LOG_ERROR("[Eluna]: Error tokenizing Eluna.OnlyOnMaps, invalid config value '%s'", mapIdStr.c_str());
}
}
ELUNA_LOG_INFO("[Eluna]: Loaded and precompiled %u scripts in %u ms", uint32(m_scriptCache.size()), ElunaUtil::GetTimeDiff(oldMSTime));

// set the cache state to ready
m_cacheState = SCRIPT_CACHE_READY;
}

int ElunaLoader::LoadBytecodeChunk(lua_State* /*L*/, uint8* bytes, size_t len, BytecodeBuffer* buffer)
Expand All @@ -153,19 +174,21 @@ int ElunaLoader::LoadBytecodeChunk(lua_State* /*L*/, uint8* bytes, size_t len, B
// Finds lua script files from given path (including subdirectories) and pushes them to scripts
void ElunaLoader::ReadFiles(lua_State* L, std::string path)
{
std::string lua_folderpath = sElunaConfig->GetConfig(CONFIG_ELUNA_SCRIPT_PATH);

ELUNA_LOG_DEBUG("[Eluna]: ReadFiles from path `%s`", path.c_str());

fs::path someDir(path);
fs::directory_iterator end_iter;

if (fs::exists(someDir) && fs::is_directory(someDir) && !fs::is_empty(someDir))
{
lua_requirepath +=
m_requirePath +=
path + "/?.lua;" +
path + "/?.ext;" +
path + "/?.moon;";

lua_requirecpath +=
m_requirecPath +=
path + "/?.dll;" +
path + "/?.so;";

Expand Down Expand Up @@ -284,25 +307,26 @@ void ElunaLoader::ProcessScript(lua_State* L, std::string filename, const std::s
return;

if (extension)
lua_extensions.push_back(script);
m_extensions.push_back(script);
else
lua_scripts.push_back(script);
m_scripts.push_back(script);

ELUNA_LOG_DEBUG("[Eluna]: ProcessScript processed `%s` successfully", fullpath.c_str());
}

#ifdef TRINITY
void ElunaLoader::InitializeFileWatcher()
{
std::string lua_folderpath = sElunaConfig->GetConfig(CONFIG_ELUNA_SCRIPT_PATH);

lua_scriptWatcher = lua_fileWatcher.addWatch(lua_folderpath, &elunaUpdateListener, true);
if (lua_scriptWatcher >= 0)
{
ELUNA_LOG_INFO("[Eluna]: Script reloader is listening on `%s`.",
lua_folderpath.c_str());
ELUNA_LOG_INFO("[Eluna]: Script reloader is listening on `%s`.", lua_folderpath.c_str());
}
else
{
ELUNA_LOG_INFO("[Eluna]: Failed to initialize the script reloader on `%s`.",
lua_folderpath.c_str());
ELUNA_LOG_INFO("[Eluna]: Failed to initialize the script reloader on `%s`.", lua_folderpath.c_str());
}

lua_fileWatcher.watch();
Expand All @@ -316,35 +340,32 @@ static bool ScriptPathComparator(const LuaScript& first, const LuaScript& second

void ElunaLoader::CombineLists()
{
lua_extensions.sort(ScriptPathComparator);
lua_scripts.sort(ScriptPathComparator);
combined_scripts.insert(combined_scripts.end(), lua_extensions.begin(), lua_extensions.end());
combined_scripts.insert(combined_scripts.end(), lua_scripts.begin(), lua_scripts.end());
}
m_extensions.sort(ScriptPathComparator);
m_scripts.sort(ScriptPathComparator);

bool ElunaLoader::ShouldMapLoadEluna(uint32 id)
{
if (!requiredMaps.size())
return true;
m_scriptCache.clear();
m_scriptCache.insert(m_scriptCache.end(), m_extensions.begin(), m_extensions.end());
m_scriptCache.insert(m_scriptCache.end(), m_scripts.begin(), m_scripts.end());

return (std::find(requiredMaps.begin(), requiredMaps.end(), id) != requiredMaps.end());
m_extensions.clear();
m_scripts.clear();
}

void ElunaLoader::ReloadElunaForMap(int mapId)
{
// If a mapid is provided but does not match any map or reserved id then only script storage is loaded
LoadScripts();
// reload the script cache asynchronously
ReloadScriptCache();

// If a mapid is provided but does not match any map or reserved id then only script storage is loaded
if (mapId != RELOAD_CACHE_ONLY)
{
if (mapId == RELOAD_GLOBAL_STATE || mapId == RELOAD_ALL_STATES)
#ifdef TRINITY
if (sWorld->GetEluna())
sWorld->GetEluna()->ReloadEluna();
if (Eluna* e = sWorld->GetEluna())
#else
if (sWorld.GetEluna())
sWorld.GetEluna()->ReloadEluna();
if (Eluna* e = sWorld.GetEluna())
#endif
e->ReloadEluna();

#ifdef TRINITY
sMapMgr->DoForAllMaps([&](Map* map)
Expand All @@ -353,8 +374,8 @@ void ElunaLoader::ReloadElunaForMap(int mapId)
#endif
{
if (mapId == RELOAD_ALL_STATES || mapId == static_cast<int>(map->GetId()))
if (map->GetEluna())
map->GetEluna()->ReloadEluna();
if (Eluna* e = map->GetEluna())
e->ReloadEluna();
}
);
}
Expand Down
46 changes: 29 additions & 17 deletions ElunaLoader.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ enum ElunaReloadActions
RELOAD_GLOBAL_STATE = -1
};

enum ElunaScriptCacheState
{
SCRIPT_CACHE_NONE = 0,
SCRIPT_CACHE_REINIT = 1,
SCRIPT_CACHE_LOADING = 2,
SCRIPT_CACHE_READY = 3
};

struct LuaScript;

class ElunaLoader
Expand All @@ -41,33 +49,37 @@ class ElunaLoader
ElunaLoader& operator= (ElunaLoader const&) = delete;
ElunaLoader& operator= (ElunaLoader&&) = delete;
static ElunaLoader* instance();

void LoadScripts();
void ReadFiles(lua_State* L, std::string path);
void CombineLists();
void ProcessScript(lua_State* L, std::string filename, const std::string& fullpath, int32 mapId);
bool ShouldMapLoadEluna(uint32 mapId);
bool CompileScript(lua_State* L, LuaScript& script);
static int LoadBytecodeChunk(lua_State* L, uint8* bytes, size_t len, BytecodeBuffer* buffer);
void ReloadElunaForMap(int mapId);

// Lua script folder path
std::string lua_folderpath;
// lua path variable for require() function
std::string lua_requirepath;
std::string lua_requirecpath;

typedef std::list<LuaScript> ScriptList;
ScriptList lua_scripts;
ScriptList lua_extensions;
std::vector<LuaScript> combined_scripts;
std::list<uint32> requiredMaps;
uint8 GetCacheState() const { return m_cacheState; }
const std::vector<LuaScript>& GetLuaScripts() const { return m_scriptCache; }
const std::string& GetRequirePath() const { return m_requirePath; }
const std::string& GetRequireCPath() const { return m_requirecPath; }

#ifdef TRINITY
// efsw file watcher
void InitializeFileWatcher();
efsw::FileWatcher lua_fileWatcher;
efsw::WatchID lua_scriptWatcher;
#endif

private:
void ReloadScriptCache();
void ReadFiles(lua_State* L, std::string path);
void CombineLists();
void ProcessScript(lua_State* L, std::string filename, const std::string& fullpath, int32 mapId);
bool CompileScript(lua_State* L, LuaScript& script);
static int LoadBytecodeChunk(lua_State* L, uint8* bytes, size_t len, BytecodeBuffer* buffer);

std::atomic<uint8> m_cacheState;
std::vector<LuaScript> m_scriptCache;
std::string m_requirePath;
std::string m_requirecPath;
std::list<LuaScript> m_scripts;
std::list<LuaScript> m_extensions;
std::thread m_reloadThread;
};

#ifdef TRINITY
Expand Down
Loading

0 comments on commit 12ac896

Please sign in to comment.