From 2c4f505309d08b98e6242c494f449c554cb63b22 Mon Sep 17 00:00:00 2001 From: hg <4683435+hgtw@users.noreply.github.com> Date: Sat, 30 May 2020 23:03:01 -0400 Subject: [PATCH] Refactor zone expedition caching This optimizes caching all expeditions by loading dynamic zone data and expedition members in bulk instead of for each expedition separately. This reduces the number of queries from 1+2n to 3 total. Expedition members are now joined in the initial query since empty expeditions aren't cached anyway. Optional internal lockouts for all cached expeditions are loaded in a single bulk query afterwards. Dynamic Zone data is also loaded as a single bulk query afterwards to simplify processing and keep dz database logic separated. It might be worth investigating if joining dz data in the initial expeditions load query is worth refactoring for. --- zone/dynamiczone.cpp | 124 +++++++++++++++++++++------------ zone/dynamiczone.h | 15 ++-- zone/expedition.cpp | 116 ++++++++++++++++++------------- zone/expedition.h | 1 - zone/expedition_database.cpp | 130 +++++++++++++++++++---------------- zone/expedition_database.h | 24 ++++++- 6 files changed, 254 insertions(+), 156 deletions(-) diff --git a/zone/dynamiczone.cpp b/zone/dynamiczone.cpp index 157b487e3..754e7c31a 100644 --- a/zone/dynamiczone.cpp +++ b/zone/dynamiczone.cpp @@ -62,6 +62,42 @@ DynamicZone DynamicZone::LoadDzFromDatabase(uint32_t instance_id) return dynamic_zone; } +std::unordered_map DynamicZone::LoadMultipleDzFromDatabase( + const std::vector& instance_ids) +{ + LogDynamicZonesDetail("Loading dynamic zone data for [{}] instances", instance_ids.size()); + + std::string in_instance_ids_query; + for (const auto& instance_id : instance_ids) + { + fmt::format_to(std::back_inserter(in_instance_ids_query), "{},", instance_id); + } + + std::unordered_map dynamic_zones; + + if (!in_instance_ids_query.empty()) + { + in_instance_ids_query.pop_back(); // trailing comma + + std::string query = fmt::format(SQL( + {} WHERE dynamic_zones.instance_id IN ({}); + ), DynamicZoneSelectQuery(), in_instance_ids_query); + + auto results = database.QueryDatabase(query); + if (results.Success()) + { + for (auto row = results.begin(); row != results.end(); ++row) + { + DynamicZone dz; + dz.LoadDatabaseResult(row); + dynamic_zones.emplace(dz.GetInstanceID(), dz); + } + } + } + + return dynamic_zones; +} + uint32_t DynamicZone::CreateInstance() { if (m_instance_id) @@ -107,26 +143,11 @@ uint32_t DynamicZone::CreateInstance() return m_instance_id; } -void DynamicZone::LoadFromDatabase(uint32_t instance_id) +std::string DynamicZone::DynamicZoneSelectQuery() { - if (instance_id == 0) - { - return; - } - - if (m_instance_id) - { - LogDynamicZones( - "Loading instance data for [{}] failed, instance id [{}] data already loaded", - instance_id, m_instance_id - ); - return; - } - - LogDynamicZonesDetail("Loading dz instance [{}] from database", instance_id); - - std::string query = fmt::format(SQL( + return std::string(SQL( SELECT + instance_list.id, instance_list.zone, instance_list.version, instance_list.start_time, @@ -149,36 +170,53 @@ void DynamicZone::LoadFromDatabase(uint32_t instance_id) dynamic_zones.has_zone_in FROM dynamic_zones INNER JOIN instance_list ON dynamic_zones.instance_id = instance_list.id - WHERE dynamic_zones.instance_id = {}; - ), instance_id); + )); +} + +void DynamicZone::LoadDatabaseResult(MySQLRequestRow& row) +{ + m_instance_id = strtoul(row[0], nullptr, 10); + m_zone_id = strtoul(row[1], nullptr, 10); + m_version = strtoul(row[2], nullptr, 10); + m_start_time = strtoul(row[3], nullptr, 10); + m_duration = strtoul(row[4], nullptr, 10); + m_never_expires = (strtoul(row[5], nullptr, 10) != 0); + m_type = static_cast(strtoul(row[6], nullptr, 10)); + m_expire_time = std::chrono::system_clock::from_time_t(m_start_time + m_duration); + m_compass.zone_id = strtoul(row[7], nullptr, 10); + m_compass.x = strtof(row[8], nullptr); + m_compass.y = strtof(row[9], nullptr); + m_compass.z = strtof(row[10], nullptr); + m_safereturn.zone_id = strtoul(row[11], nullptr, 10); + m_safereturn.x = strtof(row[12], nullptr); + m_safereturn.y = strtof(row[13], nullptr); + m_safereturn.z = strtof(row[14], nullptr); + m_safereturn.heading = strtof(row[15], nullptr); + m_zonein.x = strtof(row[16], nullptr); + m_zonein.y = strtof(row[17], nullptr); + m_zonein.z = strtof(row[18], nullptr); + m_zonein.heading = strtof(row[19], nullptr); + m_has_zonein = (strtoul(row[20], nullptr, 10) != 0); +} + +void DynamicZone::LoadFromDatabase(uint32_t instance_id) +{ + if (instance_id == 0) + { + return; + } + + LogDynamicZonesDetail("Loading dz instance [{}] from database", instance_id); + + std::string query = fmt::format(SQL( + {} WHERE dynamic_zones.instance_id = {}; + ), DynamicZoneSelectQuery(), instance_id); auto results = database.QueryDatabase(query); if (results.Success() && results.RowCount() > 0) { auto row = results.begin(); - - m_instance_id = instance_id; - m_zone_id = strtoul(row[0], nullptr, 10); - m_version = strtoul(row[1], nullptr, 10); - m_start_time = strtoul(row[2], nullptr, 10); - m_duration = strtoul(row[3], nullptr, 10); - m_never_expires = (strtoul(row[4], nullptr, 10) != 0); - m_type = static_cast(strtoul(row[5], nullptr, 10)); - m_expire_time = std::chrono::system_clock::from_time_t(m_start_time + m_duration); - m_compass.zone_id = strtoul(row[6], nullptr, 10); - m_compass.x = strtof(row[7], nullptr); - m_compass.y = strtof(row[8], nullptr); - m_compass.z = strtof(row[9], nullptr); - m_safereturn.zone_id = strtoul(row[10], nullptr, 10); - m_safereturn.x = strtof(row[11], nullptr); - m_safereturn.y = strtof(row[12], nullptr); - m_safereturn.z = strtof(row[13], nullptr); - m_safereturn.heading = strtof(row[14], nullptr); - m_zonein.x = strtof(row[15], nullptr); - m_zonein.y = strtof(row[16], nullptr); - m_zonein.z = strtof(row[17], nullptr); - m_zonein.heading = strtof(row[18], nullptr); - m_has_zonein = (strtoul(row[19], nullptr, 10) != 0); + LoadDatabaseResult(row); } } diff --git a/zone/dynamiczone.h b/zone/dynamiczone.h index f9a64276b..54c58be9f 100644 --- a/zone/dynamiczone.h +++ b/zone/dynamiczone.h @@ -24,12 +24,14 @@ #include #include #include +#include #include #include +class MySQLRequestRow; class ServerPacket; -enum class DynamicZoneType : uint8_t // DynamicZoneActiveType +enum class DynamicZoneType : uint8_t { None = 0, Expedition, @@ -47,7 +49,7 @@ struct DynamicZoneLocation float z = 0.0f; float heading = 0.0f; - DynamicZoneLocation() {} + DynamicZoneLocation() = default; DynamicZoneLocation(uint32_t zone_id_, float x_, float y_, float z_, float heading_) : zone_id(zone_id_), x(x_), y(y_), z(z_), heading(heading_) {} }; @@ -58,9 +60,12 @@ public: DynamicZone() = default; DynamicZone(uint32_t zone_id, uint32_t version, uint32_t duration, DynamicZoneType type); DynamicZone(std::string zone_shortname, uint32_t version, uint32_t duration, DynamicZoneType type); - DynamicZone(DynamicZoneType type) : m_type(type) { } + DynamicZone(uint32_t instance_id) : m_instance_id(instance_id) {} + DynamicZone(DynamicZoneType type) : m_type(type) {} static DynamicZone LoadDzFromDatabase(uint32_t instance_id); + static std::unordered_map LoadMultipleDzFromDatabase( + const std::vector& instance_ids); static void HandleWorldMessage(ServerPacket* pack); DynamicZoneType GetType() const { return m_type; } @@ -95,6 +100,8 @@ public: uint32_t SaveToDatabase(); private: + static std::string DynamicZoneSelectQuery(); + void LoadDatabaseResult(MySQLRequestRow& row); void DeleteFromDatabase(); void SaveCompassToDatabase(); void SaveSafeReturnToDatabase(); @@ -111,7 +118,7 @@ private: DynamicZoneLocation m_compass; DynamicZoneLocation m_safereturn; DynamicZoneLocation m_zonein; - std::chrono::time_point m_expire_time; //uint64_t m_expire_time = 0; + std::chrono::time_point m_expire_time; }; #endif diff --git a/zone/expedition.cpp b/zone/expedition.cpp index d326131b3..2938882d4 100644 --- a/zone/expedition.cpp +++ b/zone/expedition.cpp @@ -153,58 +153,95 @@ void Expedition::CacheExpeditions(MySQLRequestResult& results) return; } + std::vector expedition_ids; + std::vector instance_ids; + + using col = LoadExpeditionColumns::eLoadExpeditionColumns; + + Expedition* current_expedition = nullptr; uint32_t last_expedition_id = 0; + for (auto row = results.begin(); row != results.end(); ++row) { - auto expedition_id = strtoul(row[0], nullptr, 10); + auto expedition_id = strtoul(row[col::id], nullptr, 10); + if (expedition_id != last_expedition_id) { - auto leader_id = static_cast(strtoul(row[3], nullptr, 10)); - ExpeditionMember leader{ leader_id, row[9] }; // id, name - auto instance_id = row[1] ? strtoul(row[1], nullptr, 10) : 0; // can be null from fk constraint + // finished parsing previous expedition members, send member updates + if (current_expedition) + { + current_expedition->SendWorldGetOnlineMembers(); + current_expedition->SendUpdatesToZoneMembers(); + } - DynamicZone dynamic_zone = DynamicZone::LoadDzFromDatabase(instance_id); + expedition_ids.emplace_back(expedition_id); + + uint32_t leader_id = strtoul(row[col::leader_id], nullptr, 10); + uint32_t instance_id = row[col::instance_id] ? strtoul(row[col::instance_id], nullptr, 10) : 0; + if (instance_id) // can be null from fk constraint + { + instance_ids.emplace_back(instance_id); + } std::unique_ptr expedition = std::unique_ptr(new Expedition( expedition_id, - dynamic_zone, - row[2], // expedition name - leader, // expedition leader - strtoul(row[4], nullptr, 10), // min_players - strtoul(row[5], nullptr, 10), // max_players - (strtoul(row[6], nullptr, 10) != 0) // has_replay_timer + DynamicZone{instance_id}, + row[col::expedition_name], // expedition name + ExpeditionMember{leader_id, row[col::leader_name]}, // expedition leader id, name + strtoul(row[col::min_players], nullptr, 10), // min_players + strtoul(row[col::max_players], nullptr, 10), // max_players + (strtoul(row[col::has_replay_timer], nullptr, 10) != 0) // has_replay_timer )); - bool add_replay_on_join = (strtoul(row[7], nullptr, 10) != 0); - bool is_locked = (strtoul(row[8], nullptr, 10) != 0); + bool add_replay_on_join = (strtoul(row[col::add_replay_on_join], nullptr, 10) != 0); + bool is_locked = (strtoul(row[col::is_locked], nullptr, 10) != 0); expedition->SetReplayLockoutOnMemberJoin(add_replay_on_join); expedition->SetLocked(is_locked); - expedition->LoadMembers(); - expedition->SendUpdatesToZoneMembers(); - // don't bother caching empty expeditions - if (expedition->GetMemberCount() > 0) - { - zone->expedition_cache.emplace(expedition_id, std::move(expedition)); - } + zone->expedition_cache.emplace(expedition_id, std::move(expedition)); } last_expedition_id = expedition_id; - // optional lockouts from left join - if (row[10] && row[11] && row[12] && row[13]) + // looping expedition members + current_expedition = Expedition::FindCachedExpeditionByID(last_expedition_id); + if (current_expedition) { - auto it = zone->expedition_cache.find(last_expedition_id); - if (it != zone->expedition_cache.end()) + auto member_id = strtoul(row[col::member_id], nullptr, 10); + bool is_current_member = (strtoul(row[col::is_current_member], nullptr, 10) != 0); + current_expedition->AddInternalMember( + row[col::member_name], member_id, ExpeditionMemberStatus::Offline, is_current_member + ); + } + } + + // update for the last cached expedition + if (current_expedition) + { + current_expedition->SendWorldGetOnlineMembers(); + current_expedition->SendUpdatesToZoneMembers(); + } + + // bulk load dynamic zone data and expedition lockouts for cached expeditions + auto dynamic_zones = DynamicZone::LoadMultipleDzFromDatabase(instance_ids); + auto expedition_lockouts = ExpeditionDatabase::LoadMultipleExpeditionLockouts(expedition_ids); + + for (const auto& expedition_id : expedition_ids) + { + auto expedition = Expedition::FindCachedExpeditionByID(expedition_id); + if (expedition) + { + auto dz_iter = dynamic_zones.find(expedition->GetInstanceID()); + if (dz_iter != dynamic_zones.end()) { - it->second->AddInternalLockout(ExpeditionLockoutTimer{ - row[2], // expedition_name - row[10], // event_name - strtoull(row[11], nullptr, 10), // expire_time - static_cast(strtoul(row[12], nullptr, 10)), // original duration - (strtoul(row[13], nullptr, 10) != 0) // is_inherited - }); + expedition->m_dynamiczone = dz_iter->second; + } + + auto lockout_iter = expedition_lockouts.find(expedition->GetID()); + if (lockout_iter != expedition_lockouts.end()) + { + expedition->m_lockouts = lockout_iter->second; } } } @@ -257,23 +294,6 @@ bool Expedition::CacheAllFromDatabase() return true; } -void Expedition::LoadMembers() -{ - m_members.clear(); - - auto results = ExpeditionDatabase::LoadExpeditionMembers(m_id); - if (results.Success()) - { - for (auto row = results.begin(); row != results.end(); ++row) - { - auto character_id = strtoul(row[0], nullptr, 10); - bool is_current_member = strtoul(row[1], nullptr, 10); - AddInternalMember(row[2], character_id, ExpeditionMemberStatus::Offline, is_current_member); - } - SendWorldGetOnlineMembers(); - } -} - void Expedition::SaveLockouts(ExpeditionRequest& request) { m_lockouts = std::move(request).TakeLockouts(); diff --git a/zone/expedition.h b/zone/expedition.h index 403dff438..11bc6fda4 100644 --- a/zone/expedition.h +++ b/zone/expedition.h @@ -139,7 +139,6 @@ private: void AddInternalMember(const std::string& char_name, uint32_t char_id, ExpeditionMemberStatus status, bool is_current_member = true); bool ChooseNewLeader(); bool ConfirmLeaderCommand(Client* requester); - void LoadMembers(); bool ProcessAddConflicts(Client* leader_client, Client* add_client, bool swapping); void ProcessLeaderChanged(uint32_t new_leader_id, const std::string& new_leader_name); void ProcessLockoutUpdate(const std::string& event_name, uint64_t expire_time, uint32_t duration, bool remove); diff --git a/zone/expedition_database.cpp b/zone/expedition_database.cpp index fb913a9b2..1c4bf6207 100644 --- a/zone/expedition_database.cpp +++ b/zone/expedition_database.cpp @@ -48,11 +48,9 @@ uint32_t ExpeditionDatabase::InsertExpedition( return results.LastInsertedID(); } -MySQLRequestResult ExpeditionDatabase::LoadExpedition(uint32_t expedition_id) +std::string ExpeditionDatabase::LoadExpeditionsSelectQuery() { - LogExpeditionsDetail("Loading expedition [{}]", expedition_id); - - std::string query = fmt::format(SQL( + return std::string(SQL( SELECT expedition_details.id, expedition_details.instance_id, @@ -64,53 +62,36 @@ MySQLRequestResult ExpeditionDatabase::LoadExpedition(uint32_t expedition_id) expedition_details.add_replay_on_join, expedition_details.is_locked, character_data.name leader_name, - expedition_lockouts.event_name, - UNIX_TIMESTAMP(expedition_lockouts.expire_time), - expedition_lockouts.duration, - expedition_lockouts.is_inherited + expedition_members.character_id, + expedition_members.is_current_member, + member_data.name FROM expedition_details INNER JOIN character_data ON expedition_details.leader_id = character_data.id - LEFT JOIN expedition_lockouts - ON expedition_details.id = expedition_lockouts.expedition_id - AND expedition_lockouts.expire_time > NOW() - WHERE expedition_details.id = {}; - ), expedition_id); + INNER JOIN expedition_members ON expedition_details.id = expedition_members.expedition_id + INNER JOIN character_data member_data ON expedition_members.character_id = member_data.id + )); +} - auto results = database.QueryDatabase(query); - return results; +MySQLRequestResult ExpeditionDatabase::LoadExpedition(uint32_t expedition_id) +{ + LogExpeditionsDetail("Loading expedition [{}]", expedition_id); + + std::string query = fmt::format(SQL( + {} WHERE expedition_details.id = {}; + ), LoadExpeditionsSelectQuery(), expedition_id); + + return database.QueryDatabase(query); } MySQLRequestResult ExpeditionDatabase::LoadAllExpeditions() { LogExpeditionsDetail("Loading all expeditions from database"); - // load all active expeditions and members to current zone cache - std::string query = SQL( - SELECT - expedition_details.id, - expedition_details.instance_id, - expedition_details.expedition_name, - expedition_details.leader_id, - expedition_details.min_players, - expedition_details.max_players, - expedition_details.has_replay_timer, - expedition_details.add_replay_on_join, - expedition_details.is_locked, - character_data.name leader_name, - expedition_lockouts.event_name, - UNIX_TIMESTAMP(expedition_lockouts.expire_time), - expedition_lockouts.duration, - expedition_lockouts.is_inherited - FROM expedition_details - INNER JOIN character_data ON expedition_details.leader_id = character_data.id - LEFT JOIN expedition_lockouts - ON expedition_details.id = expedition_lockouts.expedition_id - AND expedition_lockouts.expire_time > NOW() - ORDER BY expedition_details.id; - ); + std::string query = fmt::format(SQL( + {} ORDER BY expedition_details.id; + ), LoadExpeditionsSelectQuery()); - auto results = database.QueryDatabase(query); - return results; + return database.QueryDatabase(query); } MySQLRequestResult ExpeditionDatabase::LoadCharacterLockouts(uint32_t character_id) @@ -151,26 +132,58 @@ MySQLRequestResult ExpeditionDatabase::LoadCharacterLockouts( return database.QueryDatabase(query); } -MySQLRequestResult ExpeditionDatabase::LoadExpeditionMembers(uint32_t expedition_id) +std::unordered_map> +ExpeditionDatabase::LoadMultipleExpeditionLockouts( + const std::vector& expedition_ids) { - LogExpeditionsDetail("Loading all members for expedition [{}]", expedition_id); + LogExpeditionsDetail("Loading internal lockouts for [{}] expeditions", expedition_ids.size()); - std::string query = fmt::format(SQL( - SELECT - expedition_members.character_id, - expedition_members.is_current_member, - character_data.name - FROM expedition_members - INNER JOIN character_data ON expedition_members.character_id = character_data.id - WHERE expedition_id = {}; - ), expedition_id); - - auto results = database.QueryDatabase(query); - if (!results.Success()) + std::string in_expedition_ids_query; + for (const auto& expedition_id : expedition_ids) { - LogExpeditions("Failed to load expedition [{}] members from db", expedition_id); + fmt::format_to(std::back_inserter(in_expedition_ids_query), "{},", expedition_id); } - return results; + + // these are loaded into the same container type expedition's use to store lockouts + std::unordered_map> lockouts; + + if (!in_expedition_ids_query.empty()) + { + in_expedition_ids_query.pop_back(); // trailing comma + + std::string query = fmt::format(SQL( + SELECT + expedition_lockouts.expedition_id, + expedition_lockouts.event_name, + UNIX_TIMESTAMP(expedition_lockouts.expire_time), + expedition_lockouts.duration, + expedition_lockouts.is_inherited, + expedition_details.expedition_name + FROM expedition_lockouts + INNER JOIN expedition_details ON expedition_lockouts.expedition_id = expedition_details.id + WHERE expedition_id IN ({}) + ORDER BY expedition_id; + ), in_expedition_ids_query); + + auto results = database.QueryDatabase(query); + + if (results.Success()) + { + for (auto row = results.begin(); row != results.end(); ++row) + { + auto expedition_id = strtoul(row[0], nullptr, 10); + lockouts[expedition_id].emplace(row[1], ExpeditionLockoutTimer{ + row[5], // expedition_name + row[1], // event_name + strtoull(row[2], nullptr, 10), // expire_time + static_cast(strtoul(row[3], nullptr, 10)), // original duration + (strtoul(row[4], nullptr, 10) != 0) // is_inherited + }); + } + } + } + + return lockouts; } MySQLRequestResult ExpeditionDatabase::LoadValidationData( @@ -200,8 +213,7 @@ MySQLRequestResult ExpeditionDatabase::LoadValidationData( ORDER BY character_data.id; ), expedition_name, character_names); - auto results = database.QueryDatabase(query); - return results; + return database.QueryDatabase(query); } void ExpeditionDatabase::DeleteAllCharacterLockouts(uint32_t character_id) diff --git a/zone/expedition_database.h b/zone/expedition_database.h index 1773d2550..e6e35368e 100644 --- a/zone/expedition_database.h +++ b/zone/expedition_database.h @@ -38,12 +38,14 @@ namespace ExpeditionDatabase uint32_t InsertExpedition( uint32_t instance_id, const std::string& expedition_name, uint32_t leader_id, uint32_t min_players, uint32_t max_players, bool has_replay_lockout); + std::string LoadExpeditionsSelectQuery(); MySQLRequestResult LoadExpedition(uint32_t expedition_id); MySQLRequestResult LoadAllExpeditions(); MySQLRequestResult LoadCharacterLockouts(uint32_t character_id); MySQLRequestResult LoadCharacterLockouts(uint32_t character_id, const std::string& expedition_name); - MySQLRequestResult LoadExpeditionMembers(uint32_t expedition_id); MySQLRequestResult LoadValidationData(const std::string& character_names_query, const std::string& expedition_name); + std::unordered_map> + LoadMultipleExpeditionLockouts(const std::vector& expedition_ids); void DeleteAllCharacterLockouts(uint32_t character_id); void DeleteAllCharacterLockouts(uint32_t character_id, const std::string& expedition_name); void DeleteCharacterLockout(uint32_t character_id, const std::string& expedition_name, const std::string& event_name); @@ -70,4 +72,24 @@ namespace ExpeditionDatabase void UpdateReplayLockoutOnJoin(uint32_t expedition_id, bool add_on_join); }; +namespace LoadExpeditionColumns +{ + enum eLoadExpeditionColumns + { + id = 0, + instance_id, + expedition_name, + leader_id, + min_players, + max_players, + has_replay_timer, + add_replay_on_join, + is_locked, + leader_name, + member_id, + is_current_member, + member_name + }; +}; + #endif