ElunaLuaEngine / Eluna

Eluna Lua Engine © for WoW Emulators
https://elunaluaengine.github.io
GNU General Public License v3.0
378 stars 362 forks source link

Multistate thread unsafety #474

Open Shauren opened 8 months ago

Shauren commented 8 months ago

In multistate mode and worldserver map update threads > 1, all exposed functions mutating group and guild are missing synchronization

I'm talking about actions whose regular ingame packet handlers are marked as THREADUNSAFE, forced to only ever happen in main thread such as:

Solution could be one of

Foereaper commented 8 months ago

We added support for marking methods as either world only, map only or both. I went through and changed a good amount of these to map/world only, but I probably missed some. I think this would be the best way to handle it, allowing the "global" state to deal with guild/party etc in multistate.

If for whatever reason some of these actions need to be done from a map state script, then I suppose LuaVal could be used as some form of message queue which could be checked during the global state execution and handled accordingly without the need for any extra form of queueing.

Shauren commented 8 months ago

None of these are marked as world only

https://github.com/ElunaLuaEngine/Eluna/blob/6d3cdb74e397bf60804846ccedfcbf024b038e64/TrinityCore/GroupMethods.h#L451-L457 https://github.com/ElunaLuaEngine/Eluna/blob/6d3cdb74e397bf60804846ccedfcbf024b038e64/TrinityCore/GroupMethods.h#L476-L477 https://github.com/ElunaLuaEngine/Eluna/blob/6d3cdb74e397bf60804846ccedfcbf024b038e64/TrinityCore/GuildMethods.h#L262-L266 https://github.com/ElunaLuaEngine/Eluna/blob/6d3cdb74e397bf60804846ccedfcbf024b038e64/TrinityCore/GuildMethods.h#L271-L277 https://github.com/ElunaLuaEngine/Eluna/blob/6d3cdb74e397bf60804846ccedfcbf024b038e64/TrinityCore/PlayerMethods.h#L4077-L4078 https://github.com/ElunaLuaEngine/Eluna/blob/6d3cdb74e397bf60804846ccedfcbf024b038e64/TrinityCore/PlayerMethods.h#L4050-L4053

You can obtain a reference to guild/group from player methods https://github.com/ElunaLuaEngine/Eluna/blob/6d3cdb74e397bf60804846ccedfcbf024b038e64/TrinityCore/PlayerMethods.h#L3811-L3812 https://github.com/ElunaLuaEngine/Eluna/blob/6d3cdb74e397bf60804846ccedfcbf024b038e64/TrinityCore/PlayerMethods.h#L3864

Rochet2 commented 8 months ago

all exposed functions mutating group and guild

Getting members of guild/group can also be unsafe depending on use. Tricky.

queue the code for later execution on world thread (queue itself must be threadsafe)

For executing functions from map states, communicating actions between states is one option. However, that is not maybe the most efficient way to handle executing functions from map context. It is also slightly limited as passing data between the states is limited and relatively slow. Having a mechanism for delayed execution of some code would make usage much more user friendly, but adds implementation complexity.

In my mind this would indeed mean that when some kind of function is called, a function of the current lua state is queued to be run on the world update thread later. We have had some discussion before on how we could achieve this.

Modify each function to be asynchronous, for example DoForPlayersInWorld(function (player) ...code... end) where a closure would run for all players on world update later.

Alternatively create a kind of privileged mode achieved in similar manner, which is delayed to world update, but allows one to synchronously call unsafe functions. This would be more generic and user friendly I feel.

Deferred(function(unsafe_functions)
    -- This code runs on world update
    -- We could have access to unsafe functions only through a parameter like unsafe_functions here
    local players = unsafe_functions:GetPlayersInWorld()
    -- Or we could have them as global functions, that only exist/work inside deferred function
    local players = GetPlayersInWorld()
end)

I myself feel like something along the lines of Deferred(function(unsafe_functions) is the best choice so there is just one asynchonous function to make things simple and unsafe_functions would make it clear what functions exist in which scope.

Some problems to solve/consider:

  1. As seen above, these functions may allow access to references outside of current map. For example players on other maps. These references would need to be inaccessible after the asynchronous function call ends. This would warrant reintroduction of the invalidation system which was used for all events before. In it all objects were invalidated at end of an event, which in this case would be any reference pushed during the async function call when the function call ends)
  2. Something that none of the above solves is unsafe methods related to some objects. For example, if a map state has access to group through local group = player:GetGroup() then how do we handle calling/blocking local members = group:GetMembers() when in and out of privilaged mode? Global functions are a bit easier to move into unsafe_functions, but unsafe member functions are less clear on how they could be handled. I somewhat dislike the idea of having functions that sometimes work and sometimes dont. But I feel like that is the only good option I can think of here. The member functions would print an error if not in privileged mode.
Foereaper commented 8 months ago

I think step one is marking the methods outlined above as world only for now, and then we can think of a cleaner way to either defer or queue inline functions to be executed post map update.

I'm not overly worried about GetGuild and GetGroup in the map thread, I feel these are probably valid to have available, though they would be limited in scope to what methods would work on said objects with a lot of the methods marked as world only.

Shauren commented 8 months ago

Oh I only pointed those out as a way to access a group/guild objects outside of their dedicated group/guild event hooks that run in world thread only

Foereaper commented 8 months ago

Also, the unbind/bind methods in player, are these not inherently safe in the map context since they are strictly tied to the player that already exists in the map context?

Shauren commented 8 months ago

Also, the unbind/bind methods in player, are these not inherently safe in the map context since they are strictly tied to the player that already exists in the map context?

Ignore it, bind/unbind are safe (I thought at first that the InstanceSave object isnt safe to use but it has a mutex)

As for deferring execution, I did not think that far, only imagined doing that purely in c++, for example having LuaGuild::AddMember be implemented as

int AddMember(Eluna* E, Guild* guild)
{
    Player* player = E->CHECKOBJ<Player>(2);
    uint8 rankId = E->CHECKVAL<uint8>(3, GUILD_RANK_NONE);

    sWorld->IMAGINARY_ASYNC_QUEUE_QUEUEING_FUNCTION([guildId = guild->GetId(), guid = player->GET_GUID(), rankId]()
    {
        if (Guild* guild = sGuildMgr->GetGuildById(guildId))
        {
            CharacterDatabaseTransaction trans(nullptr);
            guild->AddMember(trans, guid, rankId);
        }
    });
    return 0;
}
Foereaper commented 8 months ago

Pushed an update to flag the outlined methods above as world only, let me know if you find others and I'll flag those accordingly as well.

Rochet2 commented 8 months ago

The approach in https://github.com/ElunaLuaEngine/Eluna/issues/474#issuecomment-2038338535 would work fine for most if not all cases. The only difficulty is with the GetMembers of guild, group and the get players in world.

We could just keep the player object getter functions on world state. Or they could return guids instead of objects and if the guild/group functions would operate on guids rather than objects it would allow doing actions on players that are not on the same map through the guids.

Shauren commented 8 months ago

The approach in #474 (comment) would work fine for most if not all cases. The only difficulty is with the GetMembers of guild, group and the get players in world.

We could just keep the player object getter functions on world state. Or they could return guids instead of objects and if the guild/group functions would operate on guids rather than objects it would allow doing actions on players that are not on the same map through the guids.

Latest version of #469 should actually handle that case, it prevents accessing objects whose bound map is different than current eluna state bound map