Better entity exception tolerance (#996)

* Use separate define constants for exception tolerance stuff.

* Improve handling of exceptions during entity creation.

The entity in question now gets marked as deleted immediately.

* Glorious.

* Testing components, tests, wrapper exception type.
This commit is contained in:
Pieter-Jan Briers
2020-02-24 03:56:50 +01:00
committed by GitHub
parent fc212f983e
commit 0b1f088f8c
12 changed files with 281 additions and 25 deletions

View File

@@ -20,4 +20,7 @@
<PropertyGroup Condition="'$(FullRelease)' == 'True'">
<DefineConstants>$(DefineConstants);FULL_RELEASE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DefineConstants>$(DefineConstants);EXCEPTION_TOLERANCE</DefineConstants>
</PropertyGroup>
</Project>

View File

@@ -68,6 +68,14 @@ namespace Robust.Client.GameObjects
Register<ContainerManagerComponent>();
RegisterReference<ContainerManagerComponent, IContainerManager>();
#if DEBUG
Register<DebugExceptionOnAddComponent>();
Register<DebugExceptionExposeDataComponent>();
Register<DebugExceptionInitializeComponent>();
Register<DebugExceptionStartupComponent>();
#endif
}
}
}

View File

@@ -2,14 +2,12 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Client.Interfaces.GameObjects;
using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Client.GameObjects
@@ -21,6 +19,7 @@ namespace Robust.Client.GameObjects
{
#pragma warning disable 649
[Dependency] private readonly IMapManager _mapManager;
[Dependency] private readonly IRuntimeLog _runtimeLog;
#pragma warning restore 649
private int _nextClientEntityUid = EntityUid.ClientUid + 1;
@@ -178,7 +177,7 @@ namespace Robust.Client.GameObjects
return new EntityUid(_nextClientEntityUid++);
}
private static void HandleEntityState(IComponentManager compMan, IEntity entity, EntityState curState,
private void HandleEntityState(IComponentManager compMan, IEntity entity, EntityState curState,
EntityState nextState)
{
var compStateWork = new Dictionary<uint, (ComponentState curState, ComponentState nextState)>();
@@ -244,8 +243,13 @@ namespace Robust.Client.GameObjects
}
catch (Exception e)
{
Logger.ErrorS("entity", $"Failed to apply comp state: entity={component.Owner}, comp={component.Name}\n {e}");
DebugTools.Assert(e.Message);
var wrapper = new ComponentStateApplyException(
$"Failed to apply comp state: entity={component.Owner}, comp={component.Name}", e);
#if EXCEPTION_TOLERANCE
_runtimeLog.LogException(wrapper, "Component state apply");
#else
throw wrapper;
#endif
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Runtime.Serialization;
namespace Robust.Client.GameObjects
{
[Serializable]
public class ComponentStateApplyException : Exception
{
public ComponentStateApplyException()
{
}
public ComponentStateApplyException(string message) : base(message)
{
}
public ComponentStateApplyException(string message, Exception inner) : base(message, inner)
{
}
protected ComponentStateApplyException(
SerializationInfo info,
StreamingContext context) : base(info, context)
{
}
}
}

View File

@@ -60,6 +60,13 @@ namespace Robust.Server.GameObjects
Register<IgnorePauseComponent>();
RegisterIgnore("AnimationPlayer");
#if DEBUG
Register<DebugExceptionOnAddComponent>();
Register<DebugExceptionExposeDataComponent>();
Register<DebugExceptionInitializeComponent>();
Register<DebugExceptionStartupComponent>();
#endif
}
}
}

View File

@@ -40,13 +40,13 @@ namespace Robust.Shared.Asynchronous
{
while (_pending.TryTake(out var task))
{
#if RELEASE
#if EXCEPTION_TOLERANCE
try
#endif
{
task.d(task.state);
}
#if RELEASE
#if EXCEPTION_TOLERANCE
catch (Exception e)
{
_runtimeLog.LogException(e, "Async Queued Callback");

View File

@@ -0,0 +1,53 @@
using System;
using Robust.Shared.Serialization;
namespace Robust.Shared.GameObjects.Components
{
#if DEBUG
// If you wanna use these, add it to some random prototype.
// I recommend the #1 mug:
// 1. it doesn't spawn on the map (currently).
// 2. it's at the top of the entity list (currently).
// 3. it crashed the game before, it's iconic!
/// <summary>
/// Throws an exception in <see cref="OnAdd" />.
/// </summary>
public sealed class DebugExceptionOnAddComponent : Component
{
public override string Name => "DebugExceptionOnAdd";
public override void OnAdd() => throw new NotSupportedException();
}
/// <summary>
/// Throws an exception in <see cref="ExposeData" />.
/// </summary>
public sealed class DebugExceptionExposeDataComponent : Component
{
public override string Name => "DebugExceptionExposeData";
public override void ExposeData(ObjectSerializer serializer) => throw new NotSupportedException();
}
/// <summary>
/// Throws an exception in <see cref="Initialize" />.
/// </summary>
public sealed class DebugExceptionInitializeComponent : Component
{
public override string Name => "DebugExceptionInitialize";
public override void Initialize() => throw new NotSupportedException();
}
/// <summary>
/// Throws an exception in <see cref="Startup" />.
/// </summary>
public sealed class DebugExceptionStartupComponent : Component
{
public override string Name => "DebugExceptionStartup";
protected override void Startup() => throw new NotSupportedException();
}
#endif
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Runtime.Serialization;
namespace Robust.Shared.GameObjects
{
/// <summary>
/// Thrown if an entity fails to be created due to an exception inside a component.
/// </summary>
/// <remarks>
/// See the <see cref="Exception.InnerException"/> for the actual exception.
/// </remarks>
[Serializable]
public class EntityCreationException : Exception
{
public EntityCreationException()
{
}
public EntityCreationException(string message) : base(message)
{
}
public EntityCreationException(string message, Exception inner) : base(message, inner)
{
}
protected EntityCreationException(
SerializationInfo info,
StreamingContext context) : base(info, context)
{
}
}
}

View File

@@ -255,7 +255,6 @@ namespace Robust.Shared.GameObjects
Entities[entity.Uid] = entity;
AllEntities.Add(entity);
UpdateEntityTree(entity);
return entity;
}
@@ -269,8 +268,18 @@ namespace Robust.Shared.GameObjects
return AllocEntity(uid);
var entity = AllocEntity(prototypeName, uid);
EntityPrototype.LoadEntity(entity.Prototype, entity, ComponentFactory, null);
return entity;
try
{
EntityPrototype.LoadEntity(entity.Prototype, entity, ComponentFactory, null);
return entity;
}
catch (Exception e)
{
// Exception during entity loading.
// Need to delete the entity to avoid corrupt state causing crashes later.
DeleteEntity(entity);
throw new EntityCreationException("Exception inside CreateEntity", e);
}
}
private protected void LoadEntity(Entity entity, IEntityLoadContext context)
@@ -278,10 +287,18 @@ namespace Robust.Shared.GameObjects
EntityPrototype.LoadEntity(entity.Prototype, entity, ComponentFactory, context);
}
private protected static void InitializeAndStartEntity(Entity entity)
private protected void InitializeAndStartEntity(Entity entity)
{
InitializeEntity(entity);
StartEntity(entity);
try
{
InitializeEntity(entity);
StartEntity(entity);
}
catch (Exception e)
{
DeleteEntity(entity);
throw new EntityCreationException("Exception inside InitializeAndStartEntity", e);
}
}
private protected static void InitializeEntity(Entity entity)

View File

@@ -51,13 +51,13 @@ namespace Robust.Shared.Timers
if (_timeCounter <= 0)
{
#if RELEASE
#if EXCEPTION_TOLERANCE
try
#endif
{
OnFired();
}
#if RELEASE
#if EXCEPTION_TOLERANCE
catch (Exception e)
{
runtimeLog.LogException(e, "Timer Callback");

View File

@@ -87,7 +87,7 @@ namespace Robust.Shared.Timing
// ReSharper disable once NotAccessedField.Local
private readonly IRuntimeLog _runtimeLog;
#if RELEASE
#if EXCEPTION_TOLERANCE
private int _tickExceptions;
#endif
@@ -143,14 +143,14 @@ namespace Robust.Shared.Timing
_timing.StartFrame();
realFrameEvent = new FrameEventArgs((float) _timing.RealFrameTime.TotalSeconds);
#if RELEASE
#if EXCEPTION_TOLERANCE
try
#endif
{
// process Net/KB/Mouse input
Input?.Invoke(this, realFrameEvent);
}
#if RELEASE
#if EXCEPTION_TOLERANCE
catch (Exception exp)
{
_runtimeLog.LogException(exp, "GameLoop Input");
@@ -172,13 +172,13 @@ namespace Robust.Shared.Timing
// update the simulation
simFrameEvent = new FrameEventArgs((float) _timing.FrameTime.TotalSeconds);
#if RELEASE
#if EXCEPTION_TOLERANCE
var threw = false;
try
{
#endif
Tick?.Invoke(this, simFrameEvent);
#if RELEASE
#if EXCEPTION_TOLERANCE
}
catch (Exception exp)
{
@@ -215,13 +215,13 @@ namespace Robust.Shared.Timing
// update out of the simulation
simFrameEvent = new FrameEventArgs((float) _timing.FrameTime.TotalSeconds);
#if RELEASE
#if EXCEPTION_TOLERANCE
try
#endif
{
Update?.Invoke(this, simFrameEvent);
}
#if RELEASE
#if EXCEPTION_TOLERANCE
catch (Exception exp)
{
_runtimeLog.LogException(exp, "GameLoop Update");
@@ -229,13 +229,13 @@ namespace Robust.Shared.Timing
#endif
// render the simulation
#if RELEASE
#if EXCEPTION_TOLERANCE
try
#endif
{
Render?.Invoke(this, realFrameEvent);
}
#if RELEASE
#if EXCEPTION_TOLERANCE
catch (Exception exp)
{
_runtimeLog.LogException(exp, "GameLoop Render");

View File

@@ -0,0 +1,104 @@
using System;
using System.IO;
using System.Linq;
using NUnit.Framework;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Robust.UnitTesting.Server.GameObjects
{
[TestFixture]
public class ThrowingEntityDeletion_Test : RobustUnitTest
{
private IServerEntityManager EntityManager;
private IComponentFactory _componentFactory;
private IMapManager MapManager;
const string PROTOTYPES = @"
- type: entity
id: throwInAdd
components:
- type: ThrowsInAdd
- type: entity
id: throwInExposeData
components:
- type: ThrowsInExposeData
- type: entity
id: throwsInInitialize
components:
- type: ThrowsInInitialize
- type: entity
id: throwsInStartup
components:
- type: ThrowsInStartup
";
[OneTimeSetUp]
public void Setup()
{
_componentFactory = IoCManager.Resolve<IComponentFactory>();
_componentFactory.Register<ThrowsInAddComponent>();
_componentFactory.Register<ThrowsInExposeDataComponent>();
_componentFactory.Register<ThrowsInInitializeComponent>();
_componentFactory.Register<ThrowsInStartupComponent>();
EntityManager = IoCManager.Resolve<IServerEntityManager>();
MapManager = IoCManager.Resolve<IMapManager>();
MapManager.Initialize();
MapManager.Startup();
MapManager.CreateNewMapEntity(MapId.Nullspace);
var manager = IoCManager.Resolve<IPrototypeManager>();
manager.LoadFromStream(new StringReader(PROTOTYPES));
manager.Resync();
//NOTE: The grids have not moved, so we can assert worldpos == localpos for the test
}
[Test]
public void Test([Values("throwInAdd", "throwInExposeData", "throwsInInitialize", "throwsInStartup")]
string prototypeName)
{
Assert.That(() => EntityManager.SpawnEntity(prototypeName, MapCoordinates.Nullspace),
Throws.TypeOf<EntityCreationException>());
Assert.That(EntityManager.GetEntities().Where(p => p.Prototype?.ID == prototypeName), Is.Empty);
}
private sealed class ThrowsInAddComponent : Component
{
public override string Name => "ThrowsInAdd";
public override void OnAdd() => throw new NotSupportedException();
}
private sealed class ThrowsInExposeDataComponent : Component
{
public override string Name => "ThrowsInExposeData";
public override void ExposeData(ObjectSerializer serializer) => throw new NotSupportedException();
}
private sealed class ThrowsInInitializeComponent : Component
{
public override string Name => "ThrowsInInitialize";
public override void Initialize() => throw new NotSupportedException();
}
private sealed class ThrowsInStartupComponent : Component
{
public override string Name => "ThrowsInStartup";
protected override void Startup() => throw new NotSupportedException();
}
}
}