Skip to content

Commit 3979740

Browse files
authored
Port chaos mode from OPC Publisher test server (#416)
* Port chaos mode from OPC Publisher test server * Fix log * Add line in readme.md
1 parent c9e42dc commit 3979740

File tree

4 files changed

+334
-0
lines changed

4 files changed

+334
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ The option `--dalm=<file>` enables deterministic testing of Alarms and Condition
218218

219219
More information about this feature can be found [here](deterministic-alarms.md).
220220

221+
## Chaos mode
222+
The server can also be started in `chaos mode` which randomly injects errors, closes subscriptions or sessions, expires subscriptions and more. You can use it to test the resiliency of OPC UA clients. To enable chaos mode, start the server with the `--chaos=True` option.
223+
221224
## Other features
222225
- Node with special characters in name and NodeId
223226
- Node with long ID (3950 bytes)

src/Configuration/CliOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ public static (PlcSimulation PlcSimulationInstance, List<string> ExtraArgs) Init
215215
{ "spf|showpnfname=", $"filename of the OPC Publisher configuration file to write when using options sp/sph.\nDefault: {config.PnJson}", (s) => config.PnJson = s },
216216
{ "wp|webport=", $"web server port for hosting OPC Publisher configuration file.\nDefault: {config.WebServerPort}", (uint i) => config.WebServerPort = i },
217217
{ "cdn|certdnsnames=", "add additional DNS names or IP addresses to this application's certificate (comma separated values; no spaces allowed).\nDefault: DNS hostname", (s) => config.OpcUa.DnsNames = ParseListOfStrings(s) },
218+
219+
{ "chaos", $"flag to run the server in Chaos mode. Injects errors, closes sessions and subscriptions, etc. randomly.\nDefault: not set", (s) => config.RunInChaosMode = s != null },
218220

219221
{ "h|help", "show this message and exit", (s) => config.ShowHelp = s != null },
220222
};

src/Configuration/Configuration.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,9 @@ public class OpcPlcConfiguration
9292
public TimeSpan LogFileFlushTimeSpanSec { get; set; } = TimeSpan.FromSeconds(30);
9393

9494
public OpcApplicationConfiguration OpcUa { get; set; } = new OpcApplicationConfiguration();
95+
96+
/// <summary>
97+
/// Configure chaos mode
98+
/// </summary>
99+
public bool RunInChaosMode { get; set; }
95100
}

src/PlcServer.cs

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ namespace OpcPlc;
1919
using System.Linq;
2020
using System.Reflection;
2121
using System.Threading;
22+
using System.Threading.Tasks;
2223

2324
public partial class PlcServer : StandardServer
2425
{
@@ -41,6 +42,8 @@ public partial class PlcServer : StandardServer
4142
private readonly ImmutableList<IPluginNodes> _pluginNodes;
4243
private readonly ILogger _logger;
4344
private readonly Timer _periodicLoggingTimer;
45+
private CancellationTokenSource _chaosCts;
46+
private Task _chaosMode;
4447

4548
private bool _autoDisablePublishMetrics;
4649
private uint _countCreateSession;
@@ -529,6 +532,12 @@ protected override void OnServerStarted(IServerInternal server)
529532

530533
// request notifications when the user identity is changed, all valid users are accepted by default.
531534
server.SessionManager.ImpersonateUser += new ImpersonateEventHandler(SessionManager_ImpersonateUser);
535+
536+
if (Config.RunInChaosMode)
537+
{
538+
LogStartChaos();
539+
Chaos = true;
540+
}
532541
}
533542

534543
/// <inheritdoc/>
@@ -585,8 +594,263 @@ protected override void OnServerStopping()
585594
}
586595

587596
base.OnServerStopping();
597+
598+
if (Config.RunInChaosMode)
599+
{
600+
Chaos = false;
601+
LogChaosModeStopped();
602+
}
603+
}
604+
605+
/// <summary>
606+
/// Run in choas mode and randomly delete sessions, subscriptions
607+
/// inject errors and so on.
608+
/// </summary>
609+
public bool Chaos
610+
{
611+
get
612+
{
613+
return _chaosMode != null;
614+
}
615+
set
616+
{
617+
if (value)
618+
{
619+
if (_chaosMode == null)
620+
{
621+
_chaosCts = new CancellationTokenSource();
622+
_chaosMode = ChaosAsync(_chaosCts.Token);
623+
}
624+
}
625+
else if (_chaosMode != null)
626+
{
627+
_chaosCts.Cancel();
628+
_chaosMode.GetAwaiter().GetResult();
629+
_chaosCts.Dispose();
630+
_chaosMode = null;
631+
_chaosCts = null;
632+
}
633+
}
634+
}
635+
636+
/// <summary>
637+
/// Inject errors responding to incoming requests. The error
638+
/// rate is the probability of injection, e.g. 3 means 1 out
639+
/// of 3 requests will be injected with a random error.
640+
/// </summary>
641+
public int InjectErrorResponseRate { get; set; }
642+
643+
private NodeId[] Sessions => CurrentInstance.SessionManager
644+
.GetSessions()
645+
.Select(s => s.Id)
646+
.ToArray();
647+
648+
/// <summary>
649+
/// Close all sessions
650+
/// </summary>
651+
/// <param name="deleteSubscriptions"></param>
652+
public void CloseSessions(bool deleteSubscriptions = false)
653+
{
654+
if (deleteSubscriptions)
655+
{
656+
LogClosingAllSessionsAndSubscriptions();
657+
}
658+
else
659+
{
660+
LogClosingAllSessions();
661+
}
662+
foreach (var session in Sessions)
663+
{
664+
CurrentInstance.CloseSession(null, session, deleteSubscriptions);
665+
}
666+
}
667+
668+
private uint[] Subscriptions => CurrentInstance.SubscriptionManager
669+
.GetSubscriptions()
670+
.Select(s => s.Id)
671+
.ToArray();
672+
673+
/// <summary>
674+
/// Close all subscriptions. Notify expiration (timeout) of the
675+
/// subscription before closing (status message) if notifyExpiration
676+
/// is set to true.
677+
/// </summary>
678+
/// <param name="notifyExpiration"></param>
679+
public void CloseSubscriptions(bool notifyExpiration = false)
680+
{
681+
if (notifyExpiration)
682+
{
683+
LogNotifyingExpirationAndClosingAllSubscriptions();
684+
}
685+
else
686+
{
687+
LogClosingAllSubscriptions();
688+
}
689+
foreach (var subscription in Subscriptions)
690+
{
691+
CloseSubscription(subscription, notifyExpiration);
692+
}
693+
}
694+
695+
/// <summary>
696+
/// Close subscription. Notify expiration (timeout) of the
697+
/// subscription before closing (status message) if notifyExpiration
698+
/// is set to true.
699+
/// </summary>
700+
/// <param name="subscriptionId"></param>
701+
/// <param name="notifyExpiration"></param>
702+
public void CloseSubscription(uint subscriptionId, bool notifyExpiration)
703+
{
704+
if (notifyExpiration)
705+
{
706+
NotifySubscriptionExpiration(subscriptionId);
707+
}
708+
CurrentInstance.DeleteSubscription(subscriptionId);
588709
}
589710

711+
private void NotifySubscriptionExpiration(uint subscriptionId)
712+
{
713+
try
714+
{
715+
var subscription = CurrentInstance.SubscriptionManager
716+
.GetSubscriptions()
717+
.FirstOrDefault(s => s.Id == subscriptionId);
718+
if (subscription != null)
719+
{
720+
var expireMethod = typeof(SubscriptionManager).GetMethod("SubscriptionExpired",
721+
BindingFlags.NonPublic | BindingFlags.Instance);
722+
expireMethod?.Invoke(
723+
CurrentInstance.SubscriptionManager, new object[] { subscription });
724+
}
725+
}
726+
catch
727+
{
728+
// Nothing to do
729+
}
730+
}
731+
732+
/// <summary>
733+
/// Chaos monkey mode
734+
/// </summary>
735+
/// <param name="ct"></param>
736+
/// <returns></returns>
737+
#pragma warning disable CA5394 // Do not use insecure randomness
738+
private async Task ChaosAsync(CancellationToken ct)
739+
{
740+
try
741+
{
742+
while (!ct.IsCancellationRequested)
743+
{
744+
await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(10, 60)), ct).ConfigureAwait(false);
745+
LogChaosMonkeyTime();
746+
LogSubscriptionsAndSessions(Subscriptions.Length, Sessions.Length);
747+
switch (Random.Shared.Next(0, 16))
748+
{
749+
case 0:
750+
CloseSessions(true);
751+
break;
752+
case 1:
753+
CloseSessions(false);
754+
break;
755+
case 2:
756+
CloseSubscriptions(true);
757+
break;
758+
case 3:
759+
CloseSubscriptions(false);
760+
break;
761+
case > 3 and < 8:
762+
var sessions = Sessions;
763+
if (sessions.Length == 0)
764+
{
765+
break;
766+
}
767+
768+
var session = sessions[Random.Shared.Next(0, sessions.Length)];
769+
var delete = Random.Shared.Next() % 2 == 0;
770+
LogClosingSession(session, delete);
771+
CurrentInstance.CloseSession(null, session, delete);
772+
break;
773+
case > 10 and < 13:
774+
if (InjectErrorResponseRate != 0)
775+
{
776+
break;
777+
}
778+
InjectErrorResponseRate = Random.Shared.Next(1, 20);
779+
var duration = TimeSpan.FromSeconds(Random.Shared.Next(10, 150));
780+
LogInjectingRandomErrors(InjectErrorResponseRate, duration.TotalMilliseconds);
781+
_ = Task.Run(async () =>
782+
{
783+
try
784+
{
785+
await Task.Delay(duration, ct).ConfigureAwait(false);
786+
}
787+
catch (OperationCanceledException) { }
788+
InjectErrorResponseRate = 0;
789+
}, ct);
790+
break;
791+
default:
792+
var subscriptions = Subscriptions;
793+
if (subscriptions.Length == 0)
794+
{
795+
break;
796+
}
797+
798+
var subscription = subscriptions[Random.Shared.Next(0, subscriptions.Length)];
799+
var notify = Random.Shared.Next() % 2 == 0;
800+
CloseSubscription(subscription, notify);
801+
break;
802+
}
803+
}
804+
}
805+
catch (OperationCanceledException)
806+
{
807+
// Nothing to do
808+
}
809+
}
810+
811+
/// <summary>
812+
/// Errors to inject, tilt the scale towards the most common errors.
813+
/// </summary>
814+
private static readonly StatusCode[] kStatusCodes =
815+
{
816+
StatusCodes.BadCertificateInvalid,
817+
StatusCodes.BadAlreadyExists,
818+
StatusCodes.BadNoSubscription,
819+
StatusCodes.BadSecureChannelClosed,
820+
StatusCodes.BadSessionClosed,
821+
StatusCodes.BadSessionIdInvalid,
822+
StatusCodes.BadSessionIdInvalid,
823+
StatusCodes.BadSessionIdInvalid,
824+
StatusCodes.BadSessionIdInvalid,
825+
StatusCodes.BadSessionIdInvalid,
826+
StatusCodes.BadSessionIdInvalid,
827+
StatusCodes.BadSessionIdInvalid,
828+
StatusCodes.BadSessionIdInvalid,
829+
StatusCodes.BadConnectionClosed,
830+
StatusCodes.BadServerHalted,
831+
StatusCodes.BadNotConnected,
832+
StatusCodes.BadNoCommunication,
833+
StatusCodes.BadRequestInterrupted,
834+
StatusCodes.BadRequestInterrupted,
835+
StatusCodes.BadRequestInterrupted
836+
};
837+
838+
protected override OperationContext ValidateRequest(RequestHeader requestHeader, RequestType requestType)
839+
{
840+
if (InjectErrorResponseRate != 0)
841+
{
842+
var dice = Random.Shared.Next(0, kStatusCodes.Length * InjectErrorResponseRate);
843+
if (dice < kStatusCodes.Length)
844+
{
845+
var error = kStatusCodes[dice];
846+
LogInjectingError(error);
847+
throw new ServiceResultException(error);
848+
}
849+
}
850+
return base.ValidateRequest(requestHeader, requestType);
851+
}
852+
#pragma warning restore CA5394 // Do not use insecure randomness
853+
590854
private void AddPublishMetrics(NotificationMessage notificationMessage)
591855
{
592856
int events = 0;
@@ -686,4 +950,64 @@ partial void LogPeriodicInfo(
686950
Level = LogLevel.Debug,
687951
Message = "Unknown notification type: {NotificationType}")]
688952
partial void LogUnknownNotification(string notificationType);
953+
954+
[LoggerMessage(
955+
Level = LogLevel.Warning,
956+
Message = "Starting chaos mode...")]
957+
partial void LogStartChaos();
958+
959+
[LoggerMessage(
960+
Level = LogLevel.Information,
961+
Message = "=================== CHAOS MONKEY TIME ===================")]
962+
partial void LogChaosMonkeyTime();
963+
964+
[LoggerMessage(
965+
Level = LogLevel.Information,
966+
Message = "--------> Injecting error: {StatusCode}")]
967+
partial void LogInjectingError(StatusCode statusCode);
968+
969+
[LoggerMessage(
970+
Level = LogLevel.Information,
971+
Message = "{Subscriptions} subscriptions in {Sessions} sessions!")]
972+
partial void LogSubscriptionsAndSessions(int subscriptions, int sessions);
973+
974+
[LoggerMessage(
975+
Level = LogLevel.Information,
976+
Message = "!!!!! Closing all sessions and associated subscriptions. !!!!!!")]
977+
partial void LogClosingAllSessionsAndSubscriptions();
978+
979+
[LoggerMessage(
980+
Level = LogLevel.Information,
981+
Message = "!!!!! Closing all sessions. !!!!!")]
982+
partial void LogClosingAllSessions();
983+
984+
[LoggerMessage(
985+
Level = LogLevel.Information,
986+
Message = "!!!!! Notifying expiration and closing all subscriptions. !!!!!")]
987+
partial void LogNotifyingExpirationAndClosingAllSubscriptions();
988+
989+
[LoggerMessage(
990+
Level = LogLevel.Information,
991+
Message = "!!!!! Closing all subscriptions. !!!!!")]
992+
partial void LogClosingAllSubscriptions();
993+
994+
[LoggerMessage(
995+
Level = LogLevel.Information,
996+
Message = "!!!!! Closing session {Session} (delete subscriptions: {Delete}). !!!!!")]
997+
partial void LogClosingSession(NodeId session, bool delete);
998+
999+
[LoggerMessage(
1000+
Level = LogLevel.Information,
1001+
Message = "!!!!! Injecting random errors every {Rate} responses for {Duration} ms. !!!!!")]
1002+
partial void LogInjectingRandomErrors(int rate, double duration);
1003+
1004+
[LoggerMessage(
1005+
Level = LogLevel.Information,
1006+
Message = "!!!!! Closing subscription {Subscription} (notify: {Notify}). !!!!!")]
1007+
partial void LogClosingSubscription(uint subscription, bool notify);
1008+
1009+
[LoggerMessage(
1010+
Level = LogLevel.Information,
1011+
Message = "Chaos mode stopped!")]
1012+
partial void LogChaosModeStopped();
6891013
}

0 commit comments

Comments
 (0)