إحداث فوضى يتم التحكم فيها في نُظم مجموعات Service Fabric

الأنظمة المُوزَّعة على نطاقٍ واسعٍ مثل البنى الأساسية السحابية غير موثوقة بطبيعتها. تمكِّن خدمة Azure Service Fabric المطورين من كتابة خدمات موزعة موثوقة فوق بنية أساسية غير موثوقة. لكتابة خدمات موزعة قوية فوق بنية أساسية غير موثوقة، يحتاج المطورون إلى أن يكونوا قادرين على اختبار استقرار خدماتهم بينما تمر البنية التحتية الأساسية غير الموثوقة بتحولات معقدة في الحالة بسبب الأخطاء.

توفر خدمة Fault Injection وCluster Analysis (المعروفة أيضاً باسم خدمة Fault Analysis) للمُطورين القدرة على تحفيز الأخطاء لاختبار خدماتهم. يمكن أن تساعد هذه الأخطاء المُحاكاة المستهدفة، مثل إعادة تشغيل القسم، في ممارسة انتقالات الحالة الأكثر شيوعاً. ومع ذلك، فإن الأخطاء المُحاكاة المستهدفة متحيزة بحكم تعريفها، وبالتالي قد تفوت الأخطاء التي تظهر فقط في تسلسل طويل ومُعقد يصعب التنبؤ به من انتقالات الحالة. لإجراء اختبار غير متحيز، يمكنك استخدام "Chaos".

تحاكي "Chaos" الأخطاء الدورية المتداخلة (سواء كانت آمنة أو غير آمنة) في جميع أنحاء نظام المجموعة على مدى فترات طويلة من الزمن. يتكون الخطأ الآمن من مجموعة من استدعاءات واجهة برمجة تطبيقات Service Fabric، على سبيل المثال، يُعد خطأ إعادة تشغيل النسخة المتماثلة خطأ آمن لأن هذا إغلاق متبوع بفتح على نسخة متماثلة. إزالة النسخة المتماثلة ونقل النسخة المتماثلة الأساسية ونقل النسخة المتماثلة الثانوية ونقل المثيل هي الأخطاء الآمنة الأخرى التي تمارسها "Chaos". الأخطاء غير الآمنة هي عمليات الخروج مثل عُقدة إعادة التشغيل وإعادة تشغيل حزمة التعليمات البرمجية.

بمجرد تكوين "Chaos" مع معدل ونوع الأخطاء، يمكنك بدء "Chaos" من خلال C# أو PowerShell أو واجهة برمجة تطبيقات REST لبدء إنشاء أخطاء في نظام المجموعة وفي خدماتك. يمكنك تكوين "Chaos" ليتم تشغيلها لفترةٍ زمنيةٍ محددةٍ (على سبيل المثال، لمدة ساعة واحدة)، وبعد ذلك تتوقف "Chaos" تلقائياً، أو يمكنك الاتصال بواجهة برمجة تطبيقات StopChaos (C# أو PowerShell أو REST) لإيقافها في أي وقت.

ملاحظة

تحفز "Chaos"، في شكلها الحالي، الأخطاء الآمنة فقط، مما يُعني أنه في حالة غياب أخطاء خارجية، لا يحدث فقدان للحصة أو فقدان للبيانات أبداً.

أثناء تشغيل "Chaos"، فإنه ينتج أحداثاً مختلفةً تلتقط حالة التشغيل في الوقت الحالي. على سبيل المثال، تحتوي ExecutingFaultsEvent على جميع الأخطاء التي قررت "Chaos" تنفيذها في ذلك التكرار. تحتوي ValidationFailedEvent على تفاصيل فشل التحقق من الصحة (مشكلات تتعلق بالصحة أو الاستقرار) تم العثور عليها أثناء التحقق من صحة نظام المجموعة. يمكنك استدعاء واجهة برمجة تطبيقات GetChaosReport (C# أو PowerShell أو REST) للحصول على تقرير تشغيل "Chaos". تستمر هذه الأحداث في قاموس موثوق، والذي يحتوي على نهج اقتطاع يمليه تكوينان: MaxStoredChaosEventCount (حيث القيمة الافتراضية هي 25000) و StoredActionCleanupIntervalInSeconds (حيث القيمة الافتراضية هي 3600). يتم مسح كل عمليات تحقق "Chaos" StoredActionCleanupIntervalInSeconds وجميع أحداث MaxStoredChaosEventCount باستثناء أحدثها من القاموس الموثوق.

الأخطاء المُحدثة عن "Chaos"

تولد الفوضى أخطاء عبر مجموعة Service Fabric بأكملها وتضغط الأخطاء التي يتم رؤيتها في غضون أشهر أو سنوات في غضون ساعات قليلة. يؤدي الجمع بين العيوب المتداخلة مع معدل الخطأ المرتفع إلى العثور على حالات الزاوية التي قد يتم تفويتها بطريقةٍ أخرى. سينشأ عن تمرين "Chaos" تحسناً كبيراً في جودة التعليمة البرمجية الخاصة بالخدمة.

"Chaos" تحدِث الأخطاء من الفئات التالية:

  • عملية إعادة تشغيل عقدة
  • إعادة تشغيل حزمة التعليمات البرمجية الموزعة
  • إزالة نسخة متماثلة
  • عملية إعادة تشغيل نسخة متماثلة
  • نقل نسخة متماثلة أساسية (قابلة للتكوين)
  • نقل نسخة متماثلة ثانوية (قابلة للتكوين)
  • نقل مثيل

"Chaos" قيد التشغيل في تكرارات متعددة. يتكون كل تكرار من أخطاء والتحقق من صحة نظام المجموعة للفترة المحددة. يمكنك تكوين الوقت المُستغرق لاستقرار نظام المجموعة ولكي تنجح عملية التحقق من الصحة. إذا تم العثور على فشل عند التحقق من صحة نظام المجموعة، تنشئ "Chaos" وتحافظ على ValidationFailedEvent مع الطابع الزمني للتوقيت العالمي المُتفق عليه وتفاصيل الفشل. على سبيل المثال، ضع في اعتبارك مثيل "Chaos" الذي تم تعيينه للتشغيل لمدة ساعة بحد أقصى ثلاثة أخطاء متزامنة. تؤدي الفوضى إلى حدوث ثلاثة أخطاء ثم تتحقق من صحة نظام المجموعة. تتكرر خلال الخطوة السابقة حتى يتم إيقافها صراحةً من خلال واجهة برمجة تطبيقات StopChaosAsync أو التمريرات لمدة ساعة واحدة. إذا أصبح نظام المجموعة غير صحي في أي تكرار (أي أنه لم يستقر أو لم يصبح صحياً داخل MaxClusterStabilizationTimeout التي تم تمريرها)، فتنشئ "Chaos" ValidationFailedEvent. يشير هذا الحدث إلى وقوع خطأ ما وقد يحتاج إلى المزيد من التحقيق.

للحصول على الأخطاء التي أحدثتها "Chaos"، يمكنك استخدام واجهة برمجة تطبيقات GetChaosReport (PowerShell أو C# أو REST). تحصل واجهة برمجة التطبيقات على الجزء التالي من تقرير "Chaos" استناداً إلى الرمز المميز للاستمرار الذي تم تمريره أو النطاق الزمني الذي تم تمريره. يمكنك إما تحديد ContinuationToken للحصول على الشريحة التالية من تقرير "Chaos" أو يمكنك تحديد النطاق الزمني من خلال StartTimeUtc وEndTimeUtc، ولكن لا يمكنك تحديد كل من ContinuationToken والنطاق الزمني في نفس الاستدعاء. عندما يكون هناك أكثر من 100 حدث من أحداث "Chaos"، يتم إرجاع تقرير "Chaos" في شرائح، حيث لا تحتوي الشريحة على أكثر من 100 حدث "Chaos".

خيارات مهمة للتكوين

  • TimeToRun: إجمالي الوقت الذي تستغرقه "Chaos" قبل أن تنتهي بنجاح. يمكنك إيقاف "Chaos" قبل تشغيلها لفترة TimeToRun من خلال واجهة برمجة تطبيقات StopChaos.

  • MaxClusterStabilizationTimeout: أقصى مدة انتظار حتى يصبح نظام المجموعة سليماً قبل إنتاج ValidationFailedEvent. هذا الانتظار لتقليل الحمل على نظام المجموعة أثناء الاسترداد. الفحوصات التي أجريت هي:

    • إذا كانت صحة نظام المجموعة على ما يرام
    • إذا كانت صحة الخدمة على ما يرام
    • إذا تم تحقيق حجم مجموعة النسخ المتماثلة المستهدفة لقسم الخدمة
    • عدم وجود نُسخ متماثلة من InBuild
  • MaxConcurrentFaults : الحد الأقصى لعدد الأخطاء المتزامنة الناتجة في كل تكرار. كلما زاد العدد، كانت "Chaos" أكثر حدةً، كما أن عمليات تجاوز الفشل ومجموعات انتقال الحالة التي يمر بها نظام المجموعة أكثر تعقيداً.

ملاحظة

بغض النظر عن مدى ارتفاع قيمة MaxConcurrentFaults، تضمن "Chaos"، في حالة عدم وجود أخطاء خارجية، عدم فقدان الحصة أو فقدان البيانات.

  • EnableMoveReplicaFaults: تمكين أو تعطيل الأخطاء التي تتسبب في نقل النُسخ المتماثلة الأساسية أو الثانوية أو المثيلات. يتم تعطيل هذه الأخطاء بشكل افتراضي.
  • WaitTimeBetweenIterations: مقدار الوقت اللازم للانتظار بين التكرارات. أي أن مقدار الوقت الذي ستتوقف فيه "Chaos" بعد تنفيذ جولة من الأخطاء والانتهاء من التحقق المقابل من صحة نظام المجموعة. كلما ارتفعت القيمة، انخفض متوسط معدل إدخال الخطأ.
  • WaitTimeBetweenFaults: مقدار الوقت اللازم للانتظار بين خطأين متتاليين في تكرارٍ واحدٍ. كلما ارتفعت القيمة، انخفض تزامن (أو التداخل بين) الأخطاء.
  • ClusterHealthPolicy: يستخدم نهج صحة نظام المجموعة للتحقق من صحة نظام المجموعة بين تكرارات "Chaos". إذا كانت صحة نظام المجموعة "خطأ" أو إذا حدث استثناء غير متوقع أثناء تنفيذ الخطأ، فستنتظر "Chaos" لمدة 30 دقيقة قبل الفحص الصحي التالي، لتزويد نظام المجموعة ببعض الوقت للتعافي.
  • السياق: مجموعة من أزواج القيم الرئيسية (سلسلة، سلسلة). يمكن استخدام الخريطة لتسجيل المعلومات حول تشغيل "Chaos". لا يمكن أن يكون هناك أكثر من 100 زوج من هذه الأزواج ويمكن أن يكون طول كل سلسلة (مفتاح أو قيمة) 4095 حرفاً على الأكثر. تُعين هذه الخريطة بواسطة بادئ تشغيل "Chaos" لتخزين السياق بشكل اختياري حول التشغيل المحدد.
  • ChaosTargetFilter: يمكن استخدام عامل التصفية هذا لاستهداف أخطاء "Chaos" فقط لأنواع معينة من العُقد أو فقط لمثيلات تطبيقات معينة. إذا لم يتم استخدام ChaosTargetFilter، فإن "Chaos" تخطئ في جميع كيانات نُظم المجموعات. عند استخدام ChaosTargetFilter، فإن "Chaos" لا تخطئ سوى الكيانات التي تستوفي مواصفات ChaosTargetFilter. NodeTypeInclusionList و ApplicationInclusionList تسمحا بدلالات مُوحدة فقط. بمعنى آخر، لا يمكن تحديد تقاطع NodeTypeInclusionList وApplicationInclusionList. على سبيل المثال، لا يمكن تحديد "خطأ هذا التطبيق فقط عندما يكون على نوع العقدة هذه." بمجرد تضمين كيان في إما NodeTypeInclusionList أو ApplicationInclusionList، لا يمكن استبعاد هذا الكيان باستخدام ChaosTargetFilter. حتى إذا كان التطبيق X لا يظهر في ApplicationInclusionList، في بعض تطبيقات تكرار الفوضى X يمكن أن يكون خطأ لأنه يحدث أن يكون على عقدة من nodeTypeY التي يتم تضمينها في NodeTypeInclusionList. إذا كان كل من NodeTypeInclusionList و ApplicationInclusionList قيمتين فارغتين، فسيتم طرح ArgumentException.
    • NodeTypeInclusionList: قائمة بأنواع العُقد لتضمينها في أخطاء "Chaos". تُمَكن جميع أنواع الأخطاء (إعادة تشغيل العقدة وإعادة تشغيل حزمة التعليمات البرمجية وإزالة النُسخ المتماثلة وإعادة تشغيل النُسخ المتماثلة والنقل الأساسي والنقل الثانوي ونقل المثيل) للعُقد الخاصة بأنواع العُقدة هذه. إذا لم يظهر نوع العُقدة (على سبيل المثال NodeTypeX) في NodeTypeInclusionList، فلن يتم تمكين أخطاء مستوى العقدة (مثل NodeRestart) أبداً لعقد NodeTypeX، ولكن يظل من الممكن تمكين حزمة الشفرة وأخطاء النسخ المتماثل لـ NodeTypeX في حالة وجود تطبيق في ApplicationInclusionList على عقدة NodeTypeX. يُمْكن تضمين 100 اسم نوع عقدة كحد أقصى في هذه القائمة، لزيادة هذا الرقم، يلزم ترقية التكوين لتكوين MaxNumberOfNodeTypesInChaosTargetFilter.
    • ApplicationInclusionList: قائمة بمعرفات URI للتطبيق لتضمينها في أخطاء "Chaos". جميع النسخ المتماثلة التي تنتمي إلى خدمات هذه التطبيقات قابلة للتطبيق على أخطاء النسخ المتماثل (إعادة تشغيل النُسخ المتماثلة وإزالة النُسخ المتماثلة والنقل الأساسي والنقل الثانوي ونقل المثيل) بواسطة "Chaos". لا يمكن إعادة تشغيل حزمة التعليمات البرمجية من قِبل "Chaos" إلا إذا كانت حزمة التعليمات البرمجية تستضيف نسخاً طبق الأصل من هذه التطبيقات فقط. إذا لم يظهر التطبيق في هذه القائمة، فلا يزال هناك عطل في بعض تكرارات "Chaos" إذا انتهى الأمر بالتطبيق على عقدة من نوع العقدة الموجودة في NodeTypeInclusionList. ومع ذلك، إذا كان التطبيق X مرتبطاً nodeTypeY من خلال قيود المواضع وكان التطبيق X غير موجود من ApplicationInclusionList وnodeTypeY غير موجود من NodeTypeInclusionList، فلن يكون التطبيق X خطأ أبداً. يمكن تضمين 1000 اسم تطبيق على الأكثر في هذه القائمة، لزيادة هذا الرقم، يلزم ترقية التكوين لتكوين MaxNumberOfApplicationsInChaosTargetFilter.

كيفية تشغيل "Chaos"

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Fabric;

using System.Diagnostics;
using System.Fabric.Chaos.DataStructures;

static class Program
{
    private class ChaosEventComparer : IEqualityComparer<ChaosEvent>
    {
        public bool Equals(ChaosEvent x, ChaosEvent y)
        {
            return x.TimeStampUtc.Equals(y.TimeStampUtc);
        }
        public int GetHashCode(ChaosEvent obj)
        {
            return obj.TimeStampUtc.GetHashCode();
        }
    }

    static async Task Main(string[] args)
    {
        var clusterConnectionString = "localhost:19000";
        using (var client = new FabricClient(clusterConnectionString))
        {
            var startTimeUtc = DateTime.UtcNow;

            // The maximum amount of time to wait for all cluster entities to become stable and healthy. 
            // Chaos executes in iterations and at the start of each iteration it validates the health of cluster
            // entities. 
            // During validation if a cluster entity is not stable and healthy within
            // MaxClusterStabilizationTimeoutInSeconds, Chaos generates a validation failed event.
            var maxClusterStabilizationTimeout = TimeSpan.FromSeconds(30.0);

            var timeToRun = TimeSpan.FromMinutes(60.0);

            // MaxConcurrentFaults is the maximum number of concurrent faults induced per iteration. 
            // Chaos executes in iterations and two consecutive iterations are separated by a validation phase.
            // The higher the concurrency, the more aggressive the injection of faults -- inducing more complex
            // series of states to uncover bugs.
            // The recommendation is to start with a value of 2 or 3 and to exercise caution while moving up.
            var maxConcurrentFaults = 3;

            // Describes a map, which is a collection of (string, string) type key-value pairs. The map can be
            // used to record information about the Chaos run. There cannot be more than 100 such pairs and
            // each string (key or value) can be at most 4095 characters long.
            // This map is set by the starter of the Chaos run to optionally store the context about the specific run.
            var startContext = new Dictionary<string, string>{{"ReasonForStart", "Testing"}};

            // Time-separation (in seconds) between two consecutive iterations of Chaos. The larger the value, the
            // lower the fault injection rate.
            var waitTimeBetweenIterations = TimeSpan.FromSeconds(10);

            // Wait time (in seconds) between consecutive faults within a single iteration.
            // The larger the value, the lower the overlapping between faults and the simpler the sequence of
            // state transitions that the cluster goes through. 
            // The recommendation is to start with a value between 1 and 5 and exercise caution while moving up.
            var waitTimeBetweenFaults = TimeSpan.Zero;

            // Passed-in cluster health policy is used to validate health of the cluster in between Chaos iterations. 
            var clusterHealthPolicy = new ClusterHealthPolicy
            {
                ConsiderWarningAsError = false,
                MaxPercentUnhealthyApplications = 100,
                MaxPercentUnhealthyNodes = 100
            };

            // All types of faults, restart node, restart code package, restart replica, move primary
            // replica, move secondary replica, and move instance will happen for nodes of type 'FrontEndType'
            var nodetypeInclusionList = new List<string> { "FrontEndType"};

            // In addition to the faults included by nodetypeInclusionList,
            // restart code package, restart replica, move primary replica, move secondary replica,
            //  and move instance faults will happen for 'fabric:/TestApp2' even if a replica or code
            // package from 'fabric:/TestApp2' is residing on a node which is not of type included
            // in nodeypeInclusionList.
            var applicationInclusionList = new List<string> { "fabric:/TestApp2" };

            // List of cluster entities to target for Chaos faults.
            var chaosTargetFilter = new ChaosTargetFilter
            {
                NodeTypeInclusionList = nodetypeInclusionList,
                ApplicationInclusionList = applicationInclusionList
            };

            var parameters = new ChaosParameters(
                maxClusterStabilizationTimeout,
                maxConcurrentFaults,
                true, /* EnableMoveReplicaFault */
                timeToRun,
                startContext,
                waitTimeBetweenIterations,
                waitTimeBetweenFaults,
                clusterHealthPolicy) {ChaosTargetFilter = chaosTargetFilter};

            try
            {
                await client.TestManager.StartChaosAsync(parameters);
            }
            catch (FabricChaosAlreadyRunningException)
            {
                Console.WriteLine("An instance of Chaos is already running in the cluster.");
            }

            var filter = new ChaosReportFilter(startTimeUtc, DateTime.MaxValue);

            var eventSet = new HashSet<ChaosEvent>(new ChaosEventComparer());

            string continuationToken = null;

            while (true)
            {
                ChaosReport report;
                try
                {
                    report = string.IsNullOrEmpty(continuationToken)
                        ? await client.TestManager.GetChaosReportAsync(filter)
                        : await client.TestManager.GetChaosReportAsync(continuationToken);
                }
                catch (Exception e)
                {
                    if (e is FabricTransientException)
                    {
                        Console.WriteLine("A transient exception happened: '{0}'", e);
                    }
                    else if(e is TimeoutException)
                    {
                        Console.WriteLine("A timeout exception happened: '{0}'", e);
                    }
                    else
                    {
                        throw;
                    }

                    await Task.Delay(TimeSpan.FromSeconds(1.0));
                    continue;
                }

                continuationToken = report.ContinuationToken;

                foreach (var chaosEvent in report.History)
                {
                    if (eventSet.Add(chaosEvent))
                    {
                        Console.WriteLine(chaosEvent);
                    }
                }

                // When Chaos stops, a StoppedEvent is created.
                // If a StoppedEvent is found, exit the loop.
                var lastEvent = report.History.LastOrDefault();

                if (lastEvent is StoppedEvent)
                {
                    break;
                }

                await Task.Delay(TimeSpan.FromSeconds(1.0));
            }
        }
    }
}
$clusterConnectionString = "localhost:19000"
$timeToRunMinute = 60

# The maximum amount of time to wait for all cluster entities to become stable and healthy.
# Chaos executes in iterations and at the start of each iteration it validates the health of cluster entities.
# During validation if a cluster entity is not stable and healthy within MaxClusterStabilizationTimeoutInSeconds,
# Chaos generates a validation failed event.
$maxClusterStabilizationTimeSecs = 30

# MaxConcurrentFaults is the maximum number of concurrent faults induced per iteration.
# Chaos executes in iterations and two consecutive iterations are separated by a validation phase.
# The higher the concurrency, the more aggressive the injection of faults -- inducing more complex series of
# states to uncover bugs.
# The recommendation is to start with a value of 2 or 3 and to exercise caution while moving up.
$maxConcurrentFaults = 3

# Time-separation (in seconds) between two consecutive iterations of Chaos. The larger the value, the lower the
# fault injection rate.
$waitTimeBetweenIterationsSec = 10

# Wait time (in seconds) between consecutive faults within a single iteration.
# The larger the value, the lower the overlapping between faults and the simpler the sequence of state
# transitions that the cluster goes through.
# The recommendation is to start with a value between 1 and 5 and exercise caution while moving up.
$waitTimeBetweenFaultsSec = 0

# Passed-in cluster health policy is used to validate health of the cluster in between Chaos iterations. 
$clusterHealthPolicy = new-object -TypeName System.Fabric.Health.ClusterHealthPolicy
$clusterHealthPolicy.MaxPercentUnhealthyNodes = 100
$clusterHealthPolicy.MaxPercentUnhealthyApplications = 100
$clusterHealthPolicy.ConsiderWarningAsError = $False

# Describes a map, which is a collection of (string, string) type key-value pairs. The map can be used to record
# information about the Chaos run.
# There cannot be more than 100 such pairs and each string (key or value) can be at most 4095 characters long.
# This map is set by the starter of the Chaos run to optionally store the context about the specific run.
$context = @{"ReasonForStart" = "Testing"}

#List of cluster entities to target for Chaos faults.
$chaosTargetFilter = new-object -TypeName System.Fabric.Chaos.DataStructures.ChaosTargetFilter
$chaosTargetFilter.NodeTypeInclusionList = new-object -TypeName "System.Collections.Generic.List[String]"

# All types of faults, restart node, restart code package, restart replica, move primary replica, and move
# secondary replica will happen for nodes of type 'FrontEndType'
$chaosTargetFilter.NodeTypeInclusionList.AddRange( [string[]]@("FrontEndType") )
$chaosTargetFilter.ApplicationInclusionList = new-object -TypeName "System.Collections.Generic.List[String]"

# In addition to the faults included by nodetypeInclusionList, 
# restart code package, restart replica, move primary replica, move secondary replica faults will happen for
# 'fabric:/TestApp2' even if a replica or code package from 'fabric:/TestApp2' is residing on a node which is
# not of type included in nodeypeInclusionList.
$chaosTargetFilter.ApplicationInclusionList.Add("fabric:/TestApp2")

Connect-ServiceFabricCluster $clusterConnectionString

$events = @{}
$now = [System.DateTime]::UtcNow

Start-ServiceFabricChaos -TimeToRunMinute $timeToRunMinute -MaxConcurrentFaults $maxConcurrentFaults -MaxClusterStabilizationTimeoutSec $maxClusterStabilizationTimeSecs -EnableMoveReplicaFaults -WaitTimeBetweenIterationsSec $waitTimeBetweenIterationsSec -WaitTimeBetweenFaultsSec $waitTimeBetweenFaultsSec -ClusterHealthPolicy $clusterHealthPolicy -ChaosTargetFilter $chaosTargetFilter -Context $context

while($true)
{
    $stopped = $false
    $report = Get-ServiceFabricChaosReport -StartTimeUtc $now -EndTimeUtc ([System.DateTime]::MaxValue)

    foreach ($e in $report.History) {

        if(-Not ($events.Contains($e.TimeStampUtc.Ticks)))
        {
            $events.Add($e.TimeStampUtc.Ticks, $e)
            if($e -is [System.Fabric.Chaos.DataStructures.ValidationFailedEvent])
            {
                Write-Host -BackgroundColor White -ForegroundColor Red $e
            }
            else
            {
                Write-Host $e
                # When Chaos stops, a StoppedEvent is created.
                # If a StoppedEvent is found, exit the loop.
                if($e -is [System.Fabric.Chaos.DataStructures.StoppedEvent])
                {
                    return
                }
            }
        }
    }

    Start-Sleep -Seconds 1
}