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.
This commit is contained in:
hg 2020-05-30 23:03:01 -04:00
parent f9eafa52f9
commit 2c4f505309
6 changed files with 254 additions and 156 deletions

View File

@ -62,6 +62,42 @@ DynamicZone DynamicZone::LoadDzFromDatabase(uint32_t instance_id)
return dynamic_zone;
}
std::unordered_map<uint32_t, DynamicZone> DynamicZone::LoadMultipleDzFromDatabase(
const std::vector<uint32_t>& 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<uint32_t, DynamicZone> 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<DynamicZoneType>(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<DynamicZoneType>(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);
}
}

View File

@ -24,12 +24,14 @@
#include <chrono>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
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<uint32_t, DynamicZone> LoadMultipleDzFromDatabase(
const std::vector<uint32_t>& 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<std::chrono::system_clock> m_expire_time; //uint64_t m_expire_time = 0;
std::chrono::time_point<std::chrono::system_clock> m_expire_time;
};
#endif

View File

@ -153,58 +153,95 @@ void Expedition::CacheExpeditions(MySQLRequestResult& results)
return;
}
std::vector<uint32_t> expedition_ids;
std::vector<uint32_t> 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<uint32_t>(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> expedition = std::unique_ptr<Expedition>(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<uint32_t>(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();

View File

@ -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);

View File

@ -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<uint32_t, std::unordered_map<std::string, ExpeditionLockoutTimer>>
ExpeditionDatabase::LoadMultipleExpeditionLockouts(
const std::vector<uint32_t>& 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<uint32_t, std::unordered_map<std::string, ExpeditionLockoutTimer>> 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<uint32_t>(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)

View File

@ -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<uint32_t, std::unordered_map<std::string, ExpeditionLockoutTimer>>
LoadMultipleExpeditionLockouts(const std::vector<uint32_t>& 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