Crash using service with unreal

juaxix
juaxix ✭✭
edited February 2020 in Native
I have one timer that calls the service function each 0.001 seconds ( 1/60FPS ) and another one that async calls sendAcksOnly -that is supposed to keep the connection alive- but after creating some calls, there is a moment when the peer encounters a failure and the engine crashes.
looks like it's something related to :
ExitGames::Photon::Internal::EnetCommand::~EnetCommand

Log:
Unhandled exception

UE4Editor_PhotonCloudAPI!ExitGames::Photon::Internal::EnetCommand::~EnetCommand()
UE4Editor_PhotonCloudAPI!ExitGames::Common::JVector<ExitGames::Photon::Internal::EnetCommand>::removeElementAt()
UE4Editor_PhotonCloudAPI!ExitGames::Photon::Internal::EnetPeer::serializeToBuffer()
UE4Editor_PhotonCloudAPI!ExitGames::Photon::Internal::EnetPeer::sendOutgoingCommands()
UE4Editor_PhotonCloudAPI!ExitGames::Photon::Internal::PeerBase::service()
UE4Editor_PhotonCloudAPI!ExitGames::Photon::PhotonPeer::service()
UE4Editor_PhotonCloudAPI!ExitGames::LoadBalancing::Client::service()
UE4Editor_PhotonCloudAPI!APhotonCloud::Service()

can you point me how to solve this issue? thanks!

Comments

  • I think the problem is that there is one thread running service and in the middle of the service the other thread is executing the sendAcksOnly and break the whole thing, so, what is your suggestion for this case?
  • Hi @juaxix.

    The Photon Client is only partly thread-safe.
    It is safe to access different classes or different instances of the same class from different threads in parallel (as shared resources are accessed by them in a thread-safe manner), but it is not safe to access the same instance of the same class from different threads in parallel.
    Hence when access the same Client or Peer instance (or an instance of any other Photon class) like i.e when one thread calls a function on that instance and another thread also calls a function on it (it does not matter if it is the same function or not), then you need to assure that one thread does not start its access of that instance before the other one has finished accessing it. The simplest way to guarantee this is to apply locks around the code that accesses that instance.

    Example:
    // should have the same scope and accessibility as mLoadBalancingClient
    std::mutex mClientMutex;
    

    // Thread 1
    // [...] // some other code that does not need to be locked as it can run in parallel
    {
    	std::lock_guard<std::mutex> lock(mClientMutex); // locks the mutex until the lock_guard goes out of scope
    	mLoadBalancingClient.service(); // won't be called until the other thread releases its lock by having its lockguard going out of scope
    }
    // [...] // more other code that does not need to be locked as it can run in parallel
    

    // Thread 2
    // [...] // some other code that does not need to be locked as it can run in parallel
    {
    	std::lock_guard<std::mutex> lock(mClientMutex); // locks the mutex until the lock_guard goes out of scope
    	mLoadBalancingClient.sendAcksOnly(); // won't be called until the other thread releases its lock by having its lockguard going out of scope
    }
    // [...] // more other code that does not need to be locked as it can run in parallel
    


    You can even add a small helper template class and this way make it possible to lock the Client instance itself instead of a separate mutex:
    template<typename T>
    class Lockable : public T, public std::mutex
    {
    public:
    	Lockable(void);
    	virtual ~Lockable(void);
    
    	Lockable<T>(const T& toCopy);
    };
    
    
    
    template<typename T>
    Lockable<T>::Lockable(void) : T(), std::mutex()
    {
    }
    
    template<typename T>
    Lockable<T>::~Lockable(void)
    {
    }
    
    template<typename T>
    Lockable<T>::Lockable(const T& toCopy) : T(toCopy)
    {
    }
    


    Now you can do the locking like this:
    Lockable<ExitGames::LoadBalancing::Client> mLoadBalancingClient;
    

    {
    	std::lock_guard<std::mutex> lock(mLoadBalancingClient); // locks the client until the lock_guard goes out of scope
    	mLoadBalancingClient.service(); // won't be called until the other thread releases its lock by having its lockguard going out of scope
    }
    

    {
    	std::lock_guard<std::mutex> lock(mLoadBalancingClient); // locks the client until the lock_guard goes out of scope
    	mLoadBalancingClient.sendAcksOnly(); // won't be called until the other thread releases its lock by having its lockguard going out of scope
    }
    


    Starting with v5 the Photon Client SDK will already include such a a class Lockable as well as a class RecursivelyLockable and a class SpinLockable, which inherit from std::recursive_mutex / from ExitGames::Common::Spinlock instead of from std::mutex.
  • juaxix
    juaxix ✭✭
    edited February 2020
    This is brilliant, high quality response @Kaiserludi :smile: thank you very much.
    This is a video of the unreal actor component I made to sync a transform, color, speed with different player owners ,now perfectly running thanks to your response ;)
    https://www.youtube.com/watch?v=gF1D07ZNqUI


    Now, I think I have another problem, even if I call this send acks in a separated thread of the engine that's always being called each 100ms, eventually i get a disconnection if the service function is not called withing the timeout of the connection, so, what am i missing here? the service function needs to be called in the game thread, so i tried blocking it and only the sendAcks didnt secure my connection,...why?
  • Kaiserludi
    Kaiserludi admin
    edited February 2020
    Hi @juaxix.
    Now, I think I have another problem, even if I call this send acks in a separated thread of the engine that's always being called each 100ms, eventually i get a disconnection if the service function is not called withing the timeout of the connection, so, what am i missing here? the service function needs to be called in the game thread, so i tried blocking it and only the sendAcks didnt secure my connection,...why?
    service() under the hood is implemented like this:
    			void PeerBase::service(bool dispatch)
    			{
    				serviceBasic();
    
    				EGLOG(ALL, dispatch?L"dispatch == true":L"dispatch == false");
    
    				if(dispatch)
    					while(dispatchIncomingCommands());
    
    				while(sendOutgoingCommands());
    			}
    
    As you can see, it calls 3 functions:
    serviceBasic(), dispatchIncomingCommands() and sendOutgoingCommands().

    In some situations it can make sense to not call dispatchIncomingCommands() and/or to call sendAcksOnly() instead of sendOutgoingCommands().

    However you always need to make sure that serviceBasic() gets called. Hence in situations in which you can't call service(), you need to call serviceBasic() yourself.
  • Perfect, that worked :)