Stop it being possible for an agent to teleport back to its source region before the source region has finished cleaning up old agent data and structures.

If this is allowed, then the client usually gets forcibly logged out and data structures might be put into bad states.
To prevent this, the binary state machine of EMT.m_agentsInTransit is replaced with a 4 state machine (Preparing, Transferring, ReceivedAtDestination, CleaningUp).
This is necessary because the source region needs to know when the destination region has received the user but a teleport back cannot happen until the source region has cleaned up.
Tested on standalone, grid and with v1 and v3 clients.
0.7.3-extended
Justin Clark-Casey (justincc) 2012-05-25 02:02:53 +01:00
parent 1a988ba835
commit 8d30a1f74b
3 changed files with 175 additions and 34 deletions

View File

@ -46,6 +46,29 @@ using Nini.Config;
namespace OpenSim.Region.CoreModules.Framework.EntityTransfer namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
{ {
/// <summary>
/// The possible states that an agent can be in when its being transferred between regions.
/// </summary>
/// <remarks>
/// This is a state machine.
///
/// [Entry] => Preparing
/// Preparing => { Transferring || CleaningUp || [Exit] }
/// Transferring => { ReceivedAtDestination || CleaningUp }
/// ReceivedAtDestination => CleaningUp
/// CleaningUp => [Exit]
///
/// In other words, agents normally travel throwing Preparing => Transferring => ReceivedAtDestination => CleaningUp
/// However, any state can transition to CleaningUp if the teleport has failed.
/// </remarks>
enum AgentTransferState
{
Preparing, // The agent is being prepared for transfer
Transferring, // The agent is in the process of being transferred to a destination
ReceivedAtDestination, // The destination has notified us that the agent has been successfully received
CleaningUp // The agent is being changed to child/removed after a transfer
}
public class EntityTransferModule : INonSharedRegionModule, IEntityTransferModule public class EntityTransferModule : INonSharedRegionModule, IEntityTransferModule
{ {
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
@ -68,7 +91,7 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
protected Scene m_scene; protected Scene m_scene;
protected List<UUID> m_agentsInTransit; private Dictionary<UUID, AgentTransferState> m_agentsInTransit;
private ExpiringCache<UUID, ExpiringCache<ulong, DateTime>> m_bannedRegions = private ExpiringCache<UUID, ExpiringCache<ulong, DateTime>> m_bannedRegions =
new ExpiringCache<UUID, ExpiringCache<ulong, DateTime>>(); new ExpiringCache<UUID, ExpiringCache<ulong, DateTime>>();
@ -120,7 +143,7 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
MaxTransferDistance = DefaultMaxTransferDistance; MaxTransferDistance = DefaultMaxTransferDistance;
} }
m_agentsInTransit = new List<UUID>(); m_agentsInTransit = new Dictionary<UUID, AgentTransferState>();
m_Enabled = true; m_Enabled = true;
} }
@ -253,17 +276,22 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
position.Z = newPosZ; position.Z = newPosZ;
} }
UpdateInTransit(sp.UUID, AgentTransferState.Transferring);
sp.ControllingClient.SendTeleportStart(teleportFlags); sp.ControllingClient.SendTeleportStart(teleportFlags);
sp.ControllingClient.SendLocalTeleport(position, lookAt, teleportFlags); sp.ControllingClient.SendLocalTeleport(position, lookAt, teleportFlags);
sp.Velocity = Vector3.Zero; sp.Velocity = Vector3.Zero;
sp.Teleport(position); sp.Teleport(position);
UpdateInTransit(sp.UUID, AgentTransferState.ReceivedAtDestination);
foreach (SceneObjectGroup grp in sp.GetAttachments()) foreach (SceneObjectGroup grp in sp.GetAttachments())
{ {
sp.Scene.EventManager.TriggerOnScriptChangedEvent(grp.LocalId, (uint)Changed.TELEPORT); sp.Scene.EventManager.TriggerOnScriptChangedEvent(grp.LocalId, (uint)Changed.TELEPORT);
} }
UpdateInTransit(sp.UUID, AgentTransferState.CleaningUp);
ResetFromTransit(sp.UUID); ResetFromTransit(sp.UUID);
} }
@ -502,6 +530,9 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
return; return;
} }
// Past this point we have to attempt clean up if the teleport fails, so update transfer state.
UpdateInTransit(sp.UUID, AgentTransferState.Transferring);
// OK, it got this agent. Let's close some child agents // OK, it got this agent. Let's close some child agents
sp.CloseChildAgents(newRegionX, newRegionY); sp.CloseChildAgents(newRegionX, newRegionY);
@ -592,6 +623,8 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
return; return;
} }
UpdateInTransit(sp.UUID, AgentTransferState.CleaningUp);
// For backwards compatibility // For backwards compatibility
if (version == "Unknown" || version == string.Empty) if (version == "Unknown" || version == string.Empty)
{ {
@ -618,8 +651,9 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
// We need to delay here because Imprudence viewers, unlike v1 or v3, have a short (<200ms, <500ms) delay before // We need to delay here because Imprudence viewers, unlike v1 or v3, have a short (<200ms, <500ms) delay before
// they regard the new region as the current region after receiving the AgentMovementComplete // they regard the new region as the current region after receiving the AgentMovementComplete
// response. If close is sent before then, it will cause the viewer to quit instead. // response. If close is sent before then, it will cause the viewer to quit instead.
// However, if this delay is longer, then a viewer can teleport back to this region and experience //
// a failure because the old ScenePresence has not yet been cleaned up. // This sleep can be increased if necessary. However, whilst it's active,
// an agent cannot teleport back to this region if it has teleported away.
Thread.Sleep(2000); Thread.Sleep(2000);
sp.Close(); sp.Close();
@ -638,6 +672,8 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
"[ENTITY TRANSFER MODULE]: User {0} is going to another region, profile cache removed", "[ENTITY TRANSFER MODULE]: User {0} is going to another region, profile cache removed",
sp.UUID); sp.UUID);
} }
ResetFromTransit(sp.UUID);
} }
protected virtual void Fail(ScenePresence sp, GridRegion finalDestination, bool logout) protected virtual void Fail(ScenePresence sp, GridRegion finalDestination, bool logout)
@ -1083,6 +1119,7 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
Vector3 vel2 = new Vector3(agent.Velocity.X, agent.Velocity.Y, 0); Vector3 vel2 = new Vector3(agent.Velocity.X, agent.Velocity.Y, 0);
agent.RemoveFromPhysicalScene(); agent.RemoveFromPhysicalScene();
SetInTransit(agent.UUID); SetInTransit(agent.UUID);
AgentData cAgent = new AgentData(); AgentData cAgent = new AgentData();
@ -1094,6 +1131,9 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
// We don't need the callback anymnore // We don't need the callback anymnore
cAgent.CallbackURI = String.Empty; cAgent.CallbackURI = String.Empty;
// Beyond this point, extra cleanup is needed beyond removing transit state
UpdateInTransit(agent.UUID, AgentTransferState.Transferring);
if (!m_scene.SimulationService.UpdateAgent(neighbourRegion, cAgent)) if (!m_scene.SimulationService.UpdateAgent(neighbourRegion, cAgent))
{ {
// region doesn't take it // region doesn't take it
@ -1136,7 +1176,15 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
} }
// SUCCESS! // SUCCESS!
UpdateInTransit(agent.UUID, AgentTransferState.ReceivedAtDestination);
// Unlike a teleport, here we do not wait for the destination region to confirm the receipt.
UpdateInTransit(agent.UUID, AgentTransferState.CleaningUp);
agent.MakeChildAgent(); agent.MakeChildAgent();
// FIXME: Possibly this should occur lower down after other commands to close other agents,
// but not sure yet what the side effects would be.
ResetFromTransit(agent.UUID); ResetFromTransit(agent.UUID);
// now we have a child agent in this region. Request all interesting data about other (root) agents // now we have a child agent in this region. Request all interesting data about other (root) agents
@ -1616,7 +1664,37 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
#region Agent Arrived #region Agent Arrived
public void AgentArrivedAtDestination(UUID id) public void AgentArrivedAtDestination(UUID id)
{ {
ResetFromTransit(id); lock (m_agentsInTransit)
{
if (!m_agentsInTransit.ContainsKey(id))
{
m_log.WarnFormat(
"[ENTITY TRANSFER MODULE]: Region {0} received notification of arrival in destination scene of agent {1} but no teleport request is active",
m_scene.RegionInfo.RegionName, id);
return;
}
AgentTransferState currentState = m_agentsInTransit[id];
if (currentState == AgentTransferState.ReceivedAtDestination)
{
// An anomoly but don't make this an outright failure - destination region could be overzealous in sending notification.
m_log.WarnFormat(
"[ENTITY TRANSFER MODULE]: Region {0} received notification of arrival in destination scene of agent {1} but notification has already previously been received",
m_scene.RegionInfo.RegionName, id);
}
else if (currentState != AgentTransferState.Transferring)
{
m_log.ErrorFormat(
"[ENTITY TRANSFER MODULE]: Region {0} received notification of arrival in destination scene of agent {1} but agent is in transfer state {2}",
m_scene.RegionInfo.RegionName, id, currentState);
return;
}
m_agentsInTransit[id] = AgentTransferState.ReceivedAtDestination;
}
} }
#endregion #endregion
@ -1958,10 +2036,29 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
#region Misc #region Misc
protected bool WaitForCallback(UUID id) private bool WaitForCallback(UUID id)
{ {
lock (m_agentsInTransit)
{
if (!IsInTransit(id))
throw new Exception(
string.Format(
"Asked to wait for destination callback for agent with ID {0} but it is not in transit"));
AgentTransferState currentState = m_agentsInTransit[id];
if (currentState != AgentTransferState.Transferring && currentState != AgentTransferState.ReceivedAtDestination)
throw new Exception(
string.Format(
"Asked to wait for destination callback for agent with ID {0} but it is in state {1}",
currentState));
}
int count = 200; int count = 200;
while (m_agentsInTransit.Contains(id) && count-- > 0)
// There should be no race condition here since no other code should be removing the agent transfer or
// changing the state to another other than Transferring => ReceivedAtDestination.
while (m_agentsInTransit[id] != AgentTransferState.ReceivedAtDestination && count-- > 0)
{ {
// m_log.Debug(" >>> Waiting... " + count); // m_log.Debug(" >>> Waiting... " + count);
Thread.Sleep(100); Thread.Sleep(100);
@ -1971,17 +2068,17 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
} }
/// <summary> /// <summary>
/// Set that an agent is in the process of being teleported. /// Set that an agent is in transit.
/// </summary> /// </summary>
/// <param name='id'>The ID of the agent being teleported</param> /// <param name='id'>The ID of the agent being teleported</param>
/// <returns>true if the agent was not already in transit, false if it was</returns> /// <returns>true if the agent was not already in transit, false if it was</returns>
protected bool SetInTransit(UUID id) private bool SetInTransit(UUID id)
{ {
lock (m_agentsInTransit) lock (m_agentsInTransit)
{ {
if (!m_agentsInTransit.Contains(id)) if (!m_agentsInTransit.ContainsKey(id))
{ {
m_agentsInTransit.Add(id); m_agentsInTransit[id] = AgentTransferState.Preparing;
return true; return true;
} }
} }
@ -1990,29 +2087,73 @@ namespace OpenSim.Region.CoreModules.Framework.EntityTransfer
} }
/// <summary> /// <summary>
/// Show whether the given agent is being teleported. /// Updates the state of an agent that is already in transit.
/// </summary> /// </summary>
/// <returns>true if the agent is in the process of being teleported, false otherwise.</returns> /// <param name='id'></param>
/// <param name='id'>The agent ID</para></param> /// <param name='newState'></param>
/// <returns></returns>
/// <exception cref='Exception'>Illegal transitions will throw an Exception</exception>
private void UpdateInTransit(UUID id, AgentTransferState newState)
{
lock (m_agentsInTransit)
{
// Illegal to try and update an agent that's not actually in transit.
if (!m_agentsInTransit.ContainsKey(id))
throw new Exception(string.Format("Agent with ID {0} is not registered as in transit", id));
AgentTransferState oldState = m_agentsInTransit[id];
bool transitionOkay = false;
if (newState == AgentTransferState.CleaningUp && oldState != AgentTransferState.CleaningUp)
transitionOkay = true;
else if (newState == AgentTransferState.Transferring && oldState == AgentTransferState.Preparing)
transitionOkay = true;
else if (newState == AgentTransferState.ReceivedAtDestination && oldState == AgentTransferState.Transferring)
transitionOkay = true;
if (transitionOkay)
m_agentsInTransit[id] = newState;
else
throw new Exception(
string.Format(
"Agent with ID {0} is not allowed to move from old transit state {1} to new state {2}",
id, oldState, newState));
}
}
public bool IsInTransit(UUID id) public bool IsInTransit(UUID id)
{ {
lock (m_agentsInTransit) lock (m_agentsInTransit)
return m_agentsInTransit.Contains(id); return m_agentsInTransit.ContainsKey(id);
} }
/// <summary> /// <summary>
/// Set that an agent is no longer being teleported. /// Removes an agent from the transit state machine.
/// </summary> /// </summary>
/// <returns></returns> /// <param name='id'></param>
/// <param name='id'> /// <returns>true if the agent was flagged as being teleported when this method was called, false otherwise</returns>
/// true if the agent was flagged as being teleported when this method was called, false otherwise private bool ResetFromTransit(UUID id)
/// </param>
protected bool ResetFromTransit(UUID id)
{ {
lock (m_agentsInTransit) lock (m_agentsInTransit)
{ {
if (m_agentsInTransit.Contains(id)) if (m_agentsInTransit.ContainsKey(id))
{ {
AgentTransferState state = m_agentsInTransit[id];
if (state == AgentTransferState.Transferring || state == AgentTransferState.ReceivedAtDestination)
{
// FIXME: For now, we allow exit from any state since a thrown exception in teleport is now guranteed
// to be handled properly - ResetFromTransit() could be invoked at any step along the process
m_log.WarnFormat(
"[ENTITY TRANSFER MODULE]: Agent with ID should not exit directly from state {1}, should go to {2} state first",
state, AgentTransferState.CleaningUp);
// throw new Exception(
// "Agent with ID {0} cannot exit directly from state {1}, it must go to {2} state first",
// state, AgentTransferState.CleaningUp);
}
m_agentsInTransit.Remove(id); m_agentsInTransit.Remove(id);
m_log.DebugFormat( m_log.DebugFormat(

View File

@ -77,8 +77,8 @@ namespace OpenSim.Region.Framework.Interfaces
/// <summary> /// <summary>
/// Show whether the given agent is being teleported. /// Show whether the given agent is being teleported.
/// </summary> /// </summary>
/// <returns>true if the agent is in the process of being teleported, false otherwise.</returns>
/// <param name='id'>The agent ID</para></param> /// <param name='id'>The agent ID</para></param>
/// <returns>true if the agent is in the process of being teleported, false otherwise.</returns>
bool IsInTransit(UUID id); bool IsInTransit(UUID id);
bool Cross(ScenePresence agent, bool isFlying); bool Cross(ScenePresence agent, bool isFlying);

View File

@ -5181,10 +5181,10 @@ namespace OpenSim.Region.Framework.Scenes
{ {
if (EntityTransferModule.IsInTransit(agentID)) if (EntityTransferModule.IsInTransit(agentID))
{ {
reason = "Agent is already in transit on this region"; reason = "Agent is still in transit from this region";
m_log.DebugFormat( m_log.WarnFormat(
"[SCENE]: Denying agent {0} entry into {1} since region already has them registered as in transit", "[SCENE]: Denying agent {0} entry into {1} since region still has them registered as in transit",
agentID, RegionInfo.RegionName); agentID, RegionInfo.RegionName);
return false; return false;