@@ -19,6 +19,7 @@ namespace OpcPlc;
19
19
using System . Linq ;
20
20
using System . Reflection ;
21
21
using System . Threading ;
22
+ using System . Threading . Tasks ;
22
23
23
24
public partial class PlcServer : StandardServer
24
25
{
@@ -41,6 +42,8 @@ public partial class PlcServer : StandardServer
41
42
private readonly ImmutableList < IPluginNodes > _pluginNodes ;
42
43
private readonly ILogger _logger ;
43
44
private readonly Timer _periodicLoggingTimer ;
45
+ private CancellationTokenSource _chaosCts ;
46
+ private Task _chaosMode ;
44
47
45
48
private bool _autoDisablePublishMetrics ;
46
49
private uint _countCreateSession ;
@@ -529,6 +532,12 @@ protected override void OnServerStarted(IServerInternal server)
529
532
530
533
// request notifications when the user identity is changed, all valid users are accepted by default.
531
534
server . SessionManager . ImpersonateUser += new ImpersonateEventHandler ( SessionManager_ImpersonateUser ) ;
535
+
536
+ if ( Config . RunInChaosMode )
537
+ {
538
+ LogStartChaos ( ) ;
539
+ Chaos = true ;
540
+ }
532
541
}
533
542
534
543
/// <inheritdoc/>
@@ -585,8 +594,263 @@ protected override void OnServerStopping()
585
594
}
586
595
587
596
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 ) ;
588
709
}
589
710
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
+
590
854
private void AddPublishMetrics ( NotificationMessage notificationMessage )
591
855
{
592
856
int events = 0 ;
@@ -686,4 +950,64 @@ partial void LogPeriodicInfo(
686
950
Level = LogLevel . Debug ,
687
951
Message = "Unknown notification type: {NotificationType}" ) ]
688
952
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 ( ) ;
689
1013
}
0 commit comments