[C#] Akka 중급 2-5 | 'Stash' 를 사용하여 메시지 처리 지연

참조

소개

  • 레슨 4의 마지막에 아래에서 볼 수 있듯이 라이브 차트에서 Pause/Resume 기능을 구현하는 방법에서 중대한 버그를 발견했습니다.
  • 버그는 ChartingActor가 동작을 Paused로 변경하면 더이상 특정 성능 카운터에 대해 토글 버튼을 누를 때마다 생성되는 AddSeries 및 RemoveSeries 메시지를 처리하지 않는다는 것입니다.
  • 현재 Form에서는, 버튼의 시각적 상태가 라이브 차트와 완전히 동기화되지 않는데 많은 시간이 걸리지 않습니다.
  • 그래프가 일시 중지되고 즉시 동기화되지 않을 때 토글 버튼을 누르기만 하면 됩니다.
  • 어떻게 이 문제를 고칠까요?
  • 대답은 ChartingActor가 Charting 동작으로 돌아올 때까지 AddSeries 및 RemoveSeries 메시지의 처리를 연기하는 것입니다.
  • 이때 실제로 해당 메시지로 무언가를 할 수 있습니다.
  • 이를 위한 메커니즘이 Stash 입니다.

Key Concepts / Background

  • 액터에 대한 전환 가능한 동작의 부작용 중 하나는 일부 동작이 특정 유형의 메시지를 처리할 수 없다는 것입니다.
  • 예를 들어 레슨 4에서 동작 전환에 사용한 인증 예제를 살펴 보겠습니다.

Stash 가 뭔가요?

  • Stash는 나중에 처리하기 위해 메시지를 연기하는 액터에 구현된 스택형(stack-like) 데이터 구조입니다.

액터에 Stash를 추가하는 방법

  • 액터에 Stash를 추가하려면 다음과 같이 IWithBoundedStash 또는 IWithUnboundedStash를 인터페이스로 장식하기 만하면 됩니다.
public class UserActor : ReceiveActor, IWithUnboundedStash 
{
    private readonly string _userId;
    private readonly string _chatRoomId;

    //added along with the IWithUnboundedStash interface
    public IStash Stash { get; set; }

    //constructors, behaviors, etc...
}

언제 BoundedStash와 UnboundedStash가 필요한가요?

  • 99%의 시간동안 UnboundedStash를 사용하기를 원할 것입니다.
  • Stash가 무제한의 메시지를 수락 할 수 있도록 합니다.
  • BoundedStash는 주어진 시간에 보관할 수 있는 최대 메시지 수를 설정하려는 경우에만 사용해야 합니다.
  • Stash가 BoundedStash 제한을 초과할 때마다 액터가 충돌합니다.

Stash를 초기화 해야 하나요?

  • UserActor에 공개 getter 와 setter를 포함하는 새로운 Stash 속성이 있습니다.
  • 이것이 바로 Stash를 초기화해야 한다는 의미인가요? 아닙니다.
  • Stash 속성은 액터가 로컬에서 생성 될 때마다 사용되는 Actor Construction Pipeline 이라는 Akka.NET 기능에 의해 자동으로 채워집니다.
  • ActorSystem이 액터에서 IWithBoundedStash 인터페이스를 볼 때 Stash 속성 안에 BoundedStash를 자동으로 채우는 것을 알고 있습니다.
  • 마찬가지로, IWithUnBoundedStash 인터페이스가 표시되면 대신 해당 속성에 UnboundedStash를 채우는 것을 알고 있습니다.

메시지 보관

  • 액터의 OnReceive 또는 Receive<T> 핸들러 내에서 Stash.Stash()를 호출하여 현재 메시지를 Stash 상단에 배치할 수 있습니다.
  • 지금 처리하고 싶지 않는 메시지만 보관할 필요가 있습니다.
  • 아래 시각화에서, 우리 액터는 메시지 1을 기꺼이 처리하지만 메시지 2와 0은 보관합니다.

단일 메시지 보관 해제

  • Stash 상단에서 메시지를 빼내기 위해 Stash.Unstash()를 호출합니다.
  • Stash.Unstash()를 호출하면 Stash가 이 메시지를 다른 대기열에 있는 사용자 메시지보다 앞서 액터의 메일박스 앞에 배치합니다.

VIP 라인

  • 메일 박스 안에는 액터가 처리할 사용자 메시지를 위한 별도의 두 대기열이 있는 것과 같습니다.
  • 일반 메시지 대기열이 있고 VIP 라인이 있습니다.
  • 이 VIP 라인은 Stash에서 오는 메시지용으로 예약되어 있으며 VIP 라인의 모든 메시지는 일반 대기열의 메시지보다 먼저 점프하여 액터가 처리됩니다.
  • 참고로, 모든 사용자 메시지보다 먼저 잘라내는 시스템 메시지에 대한 슈퍼 VIP 라인도 있습니다.

한 번에 전체 Stash 해제

  • 액터의 Stash에 있는 모든 것을 한꺼번에 보관 해제 해야 한다면 Stash.UnstashAll() 메서드를 사용하여 Stash의 전체 내용을 메일박스 전면으로 푸시할 수 있습니다.

메시지가 Stash에서 나올 때 원래 순서대로 유지 되나요?

  • Stash에서 꺼내는 방법에 따라 다릅니다.

Stash.UnstashAll()은 FIFO 메시지 순서를 유지

  • Stash.UnstashAll()을 호출할 때 Stash는 Stash에 있는 메시지의 원래 FIFO 순서가 액터의 메일박스 앞에 추가 될 때 보존되도록 합니다.

Stash.UnStash()는 메시지 순서를 변경 가능

  • Stash.UnStash()를 반복해서 호출하면 메시지의 원래 FIFO 순서를 변경할 수 있습니다.
  • 메일 함 내부에있는 VIP 라이늘 기억하시나요? Stash 는 메시지가 풀릴때 메시지를 넣는 곳입니다.
  • 단일 메시지를 UnStash() 하면 해당 VIP 라인의 뒷면으로 이동합니다.
  • 여전히 일반 사용자 메시지보다 앞서 있지만 이전에 보관되지 않았고 VIP 라인에서 앞서 있는 다른 메시지 뒤에 있습니다.

Stash에 보관한 메시지는 데이터를 잃어 버리나요?

  • 아닙니다.
  • 메시지를 Stash에 보관할 때 메시지와 메시지에 대한 모든 메타데이터가 포함된 Envelope 메시지를 기술적으로 저장해야 합니다.

다시 시작하는 동안 액터의 Stash에 있는 메시지는 어떻게 되나요?

  • Stash는 액터의 짧은 상태 중 일부입니다.
  • 다시 시작하는 경우 저장소가 파괴되고 가비지가 수집됩니다.
  • 이것은 재시작 중에도 메시지가 지속되는 액터의 메일박스와 반대입니다.
  • 그러나 액터의 PreRestart 수명 주기 메서드 내에서 Stash.UnstashAll()을 호출하여 재시작하는 동안 Stash의 내용을 보존할 수 있습니다.
  • 이렇게 하면 모든 숨김 메시지가 다시 시작될 때까지 유지되는 액터 메일박스로 이동합니다.
//move stashed messages to the mailbox so they persist through restart
protected override void PreRestart(Exception reason, object message)
{
    Stash.UnstashAll();
}

실제 시나리오 : 메시지 버퍼링을 사용한 인증

  • 이제 Stash가 무엇이며 어떻게 작동하는지 알았으니 채팅방 예제에서 UserActor를 다시 찾아가 사용자가 Authenticated 되기 전에 메시지를 버리는 문제를 해결해 보겠습니다.
  • 이것이 레슨 4의 개념 영역에서 디자인한 UserActor이며, 다양한 인증 상태에 대한 동작 전환이 있습니다.
public class UserActor : ReceiveActor
{
    private readonly string _userId;
    private readonly string _chatRoomId;

    public UserActor(string userId, string chatRoomId)
    {
        _userId = userId;
        _chatRoomId = chatRoomId;

        //start with the Authenticating behavior
        Authenticating();
    }

    protected override void PreStart()
    {
        //start the authentication process for this user
        Context.ActorSelection("/user/authenticator/")
            .Tell(new AuthenticatePlease(_userId));
    }

    private void Authenticating()
    {
        Receive<AuthenticationSuccess>(auth => {
            Become(Authenticated); //switch behavior to Authenticated
        });

        Receive<AuthenticationFailure>(auth => {
            Become(Unauthenticated); //switch behavior to Unauthenticated
        });

        Receive<IncomingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                //can't accept message yet - not auth'd
            });

        Receive<OutgoingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // can't send message yet - not auth'd
            });
    }

    private void Unauthenticated()
    {
        //switch to Authenticating
        Receive<RetryAuthentication>(retry => Become(Authenticating));
        Receive<IncomingMessage>(int => inc.ChatRoomId == _chatRoomId,
            inc => {
                //have to reject message - auth failed
            });

        Receive<OutgoingMessage>(int => inc.ChatRoomId == _chatRoomId,
            inc => {
                //have to reject message - auth failed
            })
    }

    private void Authenticated()
    {
        Receive<IncomingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // print message for user
            });

        Receive<OutgoingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // send message to chatroom
            });
    }
}
  • 레슨 4에서 채팅방 UserActor 예제를 처음 보았을 때, 처음에 인증을 활성화하기 위해 동작을 전환하는데 중점을 두었습니다.
  • 그러나 우리는 UserActor의 주요 문제를 무시했습니다.
  • Authenticating 단계에서 시도한 OutgoingMessage와 IncomingMessage 인스턴스를 모두 버리기만 하면 됩니다.
  • 메시지 처리를 지연시키는 방법을 알지 못했기 때문에 아무런 이유 없이 사용자에게 메시지가 손실되고 있습니다.
  • 이러한 메시지를 처리하는 올바른 방법은 UserActor가 Authenticated 또는 Unauthenticated 상태가 될 때까지 임시로 저장하는 것입니다.
  • 이때 UserActor는 사용자와 주고받는 메시지로 무엇을 할 것인지에 대해 현명한 결정을 내릴 수 있습니다.
  • 사용자가 인증되었는지 여부를 알 때까지 메시지 처리를 지연하도록 UserActor의 Authenticating 동작을 업데이트하면 다음과 같습니다.
public class UserActor : ReceiveActor, IWithUnboundedStash
{
    //constructors, fields, etc...

    private void Authenticating()
    {
        Receive<AuthenticationSuccess>(auth => 
        {
            Become(Authenticated); //switch behavior to Authenticated
            //move all stashed messages to the mailbox for processing in new behavior
            Stash.UnstashAll();
        });

        Receive<AuthenticationFailure>(auth => {
            Become(Unauthenticated); //switch behavior to Unauthenticated
            //move all stashed messages to the mailbox for processing in new behavior
            Stash.Unstashall();
        });

        Receive<IncomingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                //save this message for later
                Stash.Stash();
            });

        Receive<OutgoingMessage>(int => inc.ChatRoomId == _chatRoomId,
            inc => {
                //save this message for later
                Stash.Stash();
            });
    }
}

실습

  • ButtonToggleActor.cs
using Akka.Actor;
using System.Windows.Forms;

namespace Akka_Test.Actors
{
    public class ButtonToggleActor : UntypedActor
    {
        public class Toggle { }

        private readonly CounterType _myCounterType;
        private bool _isToggledOn;
        private readonly Button _myButton;
        private readonly IActorRef _coordinatorActor;

        public ButtonToggleActor(IActorRef coordinatorActor, Button myButton,
                CounterType myCounterType, bool isToggledOn = false)
        {
            _coordinatorActor = coordinatorActor;
            _myButton = myButton;
            _isToggledOn = isToggledOn;
            _myCounterType = myCounterType;
        }

        protected override void OnReceive(object message)
        {
            if (message is Toggle && _isToggledOn)
            {
                // toggle is currently on

                // stop watching this counter
                _coordinatorActor.Tell(
                    new PerformanceCounterCoordinatorActor.Unwatch(_myCounterType));

                FlipToggle();
            }
            else if (message is Toggle && !_isToggledOn)
            {
                // toggle is currently off

                // start watching this counter
                _coordinatorActor.Tell(
                    new PerformanceCounterCoordinatorActor.Watch(_myCounterType));

                FlipToggle();
            }
            else
            {
                Unhandled(message);
            }
        }

        private void FlipToggle()
        {
            // flip the toggle
            _isToggledOn = !_isToggledOn;

            // change the text of the button
            _myButton.Text = $"{_myCounterType.ToString().ToUpperInvariant()} {(_isToggledOn ? "ON" : "OFF")}";
        }
    }
}
  • ChartingActor.cs
using Akka.Actor;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms.DataVisualization.Charting;
using System.Windows.Forms;

namespace Akka_Test.Actors
{
    public class ChartingActor : ReceiveActor, IWithUnboundedStash
    {
        public const int MaxPoints = 250;
        public int xPosCounter = 0;

        public IStash Stash { get; set; }

        public class InitializeChart
        {
            public Dictionary<string, Series> InitialSeries { get; private set; }
            public InitializeChart(Dictionary<string, Series> initialSeries)
            {
                InitialSeries = initialSeries;
            }
        }

        public class TogglePause { }

        public class AddSeries
        {
            public Series Series { get; private set; }
            public AddSeries(Series series)
            {
                Series = series;
            }
        }

        public class RemoveSeries
        {
            public string SeriesName { get; private set; }
            public RemoveSeries(string seriesName)
            {
                SeriesName = seriesName;
            }
        }

        private readonly Chart _chart;
        private readonly Button _pauseButton;

        private Dictionary<string, Series> _seriesIndex;
        public ChartingActor(Chart chart, Button pauseButton) : this(chart, new Dictionary<string, Series>(), pauseButton)
        {
        }

        public ChartingActor(Chart chart, Dictionary<string, Series> seriesIndex, Button pauseButton)
        {
            _chart = chart;
            _seriesIndex = seriesIndex;
            _pauseButton = pauseButton;
            Charting();
        }


        private void Charting()
        {
            Receive<InitializeChart>(ic => HandleInitialize(ic));
            Receive<AddSeries>(addSeries => HandleAddSeries(addSeries));
            Receive<RemoveSeries>(removeSeries => HandleRemoveSeries(removeSeries));
            Receive<Metric>(metric => HandleMetrics(metric));

            // new receive handler for the TogglePause message type
            Receive<TogglePause>(pause =>
            {
                SetPauseButtonText(true);
                BecomeStacked(Paused);
            });
        }

        private void HandleAddSeries(AddSeries series)
        {
            if (!string.IsNullOrEmpty(series.Series.Name) &&
        !_seriesIndex.ContainsKey(series.Series.Name))
            {
                _seriesIndex.Add(series.Series.Name, series.Series);
                _chart.Series.Add(series.Series);
                SetChartBoundaries();
            }
        }

        private void HandleRemoveSeries(RemoveSeries series)
        {
            if (!string.IsNullOrEmpty(series.SeriesName) &&
                _seriesIndex.ContainsKey(series.SeriesName))
            {
                var seriesToRemove = _seriesIndex[series.SeriesName];
                _seriesIndex.Remove(series.SeriesName);
                _chart.Series.Remove(seriesToRemove);
                SetChartBoundaries();
            }
        }

        private void HandleMetrics(Metric metric)
        {
            if (!string.IsNullOrEmpty(metric.Series) &&
                _seriesIndex.ContainsKey(metric.Series))
            {
                var series = _seriesIndex[metric.Series];
                series.Points.AddXY(xPosCounter++, metric.CounterValue);
                while (series.Points.Count > MaxPoints) series.Points.RemoveAt(0);
                SetChartBoundaries();
            }
        }

        private void HandleMetricsPaused(Metric metric)
        {
            if(!string.IsNullOrEmpty(metric.Series)
                && _seriesIndex.ContainsKey(metric.Series))
            {
                var series = _seriesIndex[metric.Series];

                //set the Y value to zero when we're paused
                series.Points.AddXY(xPosCounter++, 0.0d);
                while (series.Points.Count > MaxPoints) series.Points.RemoveAt(0);
                SetChartBoundaries();
            }
        }


        private void HandleInitialize(InitializeChart ic)
        {
            if (ic.InitialSeries != null)
            {
                // swap the two series out
                _seriesIndex = ic.InitialSeries;
            }

            // delete any existing series
            _chart.Series.Clear();

            // set the axes up
            var area = _chart.ChartAreas[0];
            area.AxisX.IntervalType = DateTimeIntervalType.Number;
            area.AxisY.IntervalType = DateTimeIntervalType.Number;

            SetChartBoundaries();

            // attempt to render the initial chart
            if (_seriesIndex.Any())
            {
                foreach (var series in _seriesIndex)
                {
                    // force both the chart and the internal index to use the same names
                    series.Value.Name = series.Key;
                    _chart.Series.Add(series.Value);
                }
            }

            SetChartBoundaries();
        }

        private void SetChartBoundaries()
        {
            double maxAxisX, maxAxisY, minAxisX, minAxisY = 0.0d;
            var allPoints = _seriesIndex.Values.SelectMany(series => series.Points).ToList();
            var yValues = allPoints.SelectMany(point => point.YValues).ToList();
            maxAxisX = xPosCounter;
            minAxisX = xPosCounter - MaxPoints;
            maxAxisY = yValues.Count > 0 ? Math.Ceiling(yValues.Max()) : 1.0d;
            minAxisY = yValues.Count > 0 ? Math.Floor(yValues.Min()) : 0.0d;
            if (allPoints.Count > 2)
            {
                var area = _chart.ChartAreas[0];
                area.AxisX.Minimum = minAxisX;
                area.AxisX.Maximum = maxAxisX;
                area.AxisY.Minimum = minAxisY;
                area.AxisY.Maximum = maxAxisY;
            }
        }

        private void SetPauseButtonText(bool paused)
        {
            _pauseButton.Text = $"{(!paused ? "PAUSE ||" : "RESUME ->")}";
        }

        private void Paused()
        {
            //while paused, we need to stash AddSeries & RemoveSeries messages
            Receive<AddSeries>(addSeries => Stash.Stash());
            Receive<RemoveSeries>(removeSeries => Stash.Stash());
            Receive<Metric>(metric => HandleMetricsPaused(metric));
            Receive<TogglePause>(pause =>
            {
                SetPauseButtonText(false);
                UnbecomeStacked();

                //ChartingActor is leaving the Paused state, put message back
                //into mailbox for processing under new behavior
                Stash.UnstashAll();
            });
        }
    }
}
  • ChartingMessage.cs
using Akka.Actor;

namespace Akka_Test.Actors
{
    public class GatherMetrics { }

    public class Metric
    {
        public string Series { get; private set; }
        public float CounterValue { get; private set; }

        public Metric(string series, float counterValue)
        {
            CounterValue = counterValue;
            Series = series;
        }
    }

    public enum CounterType
    {
        Cpu,
        Memory,
        Disk
    }

    public class SubscribeCounter
    {
        public CounterType Counter { get; private set; }
        public IActorRef Subscriber { get; private set; }

        public SubscribeCounter(CounterType counter, IActorRef subscriber)
        {
            Subscriber = subscriber;
            Counter = counter;
        }
    }

    public class UnsubscribeCounter
    {
        public CounterType Counter { get; private set; }
        public IActorRef Subscriber { get; private set; }

        public UnsubscribeCounter(CounterType counter, IActorRef subscriber)
        {
            Subscriber = subscriber;
            Counter = counter;
        }
    }

    public class ChartingMessages
    {
    }
}
  • PerformanceCounterActor.cs
using Akka.Actor;
using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace Akka_Test.Actors
{
    public class PerformanceCounterActor : UntypedActor
    {
        private readonly string _seriesName;
        private readonly Func<PerformanceCounter> _performanceCounterGenerator;
        private PerformanceCounter _counter;

        private readonly HashSet<IActorRef> _subscriptions;
        private readonly ICancelable _cancelPublishing;

        public PerformanceCounterActor(string seriesName, Func<PerformanceCounter> performanceCounterGenerator)
        {
            _seriesName = seriesName;
            _performanceCounterGenerator = performanceCounterGenerator;
            _subscriptions = new HashSet<IActorRef>();
            _cancelPublishing = new Cancelable(Context.System.Scheduler);
        }

        protected override void PreStart()
        {
            //create a new instance of the performance counter
            _counter = _performanceCounterGenerator();
            Context.System.Scheduler.ScheduleTellRepeatedly(
                TimeSpan.FromMilliseconds(250),
                TimeSpan.FromMilliseconds(250),
                Self,
                new GatherMetrics(),
                Self,
                _cancelPublishing
                );
        }

        protected override void PostStop()
        {
            try
            {
                //terminate the scheduled task
                _cancelPublishing.Cancel(false);
                _counter.Dispose();
            }
            catch
            {
                // we don't care about additional "ObjectDisposed" exceptions
            }
            finally
            {
                base.PostStop();
            }
        }

        protected override void OnReceive(object message)
        {
            if(message is GatherMetrics)
            {
                //publish latest counter value to all subscribers
                var metric = new Metric(_seriesName, _counter.NextValue());
                foreach (var sub in _subscriptions)
                    sub.Tell(metric);
            }
            else if(message is SubscribeCounter)
            {
                // add a subscription for this counter
                var sc = message as SubscribeCounter;
                _subscriptions.Add(sc.Subscriber);
            }
            else if(message is UnsubscribeCounter)
            {
                //remove a subscription from this counter
                var uc = message as UnsubscribeCounter;
                _subscriptions.Remove(uc.Subscriber);
            }
        }
    }
}
  • PerformanceCounterCoordinatorActor.cs
using Akka.Actor;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms.DataVisualization.Charting;

namespace Akka_Test.Actors
{
    public class PerformanceCounterCoordinatorActor : ReceiveActor
    {
        public class Watch
        {
            public Watch(CounterType counter)
            {
                Counter = counter;
            }

            public CounterType Counter { get; private set; }
        }

        public class Unwatch
        {
            public Unwatch(CounterType counter)
            {
                Counter = counter;
            }

            public CounterType Counter { get; private set; }
        }

        private static readonly Dictionary<CounterType, Func<PerformanceCounter>>
            CounterGenerators = new Dictionary<CounterType, Func<PerformanceCounter>>()
        {
            {CounterType.Cpu, () => new PerformanceCounter("Processor",
                "% Processor Time", "_Total", true)},
            {CounterType.Memory, () => new PerformanceCounter("Memory",
                "% Committed Bytes In Use", true)},
            {CounterType.Disk, () => new PerformanceCounter("LogicalDisk",
                "% Disk Time", "_Total", true)},
        };

        private static readonly Dictionary<CounterType, Func<Series>> CounterSeries =
            new Dictionary<CounterType, Func<Series>>()
        {
            {CounterType.Cpu, () =>
            new Series(CounterType.Cpu.ToString()){
                 ChartType = SeriesChartType.SplineArea,
                 Color = Color.DarkGreen}},
            {CounterType.Memory, () =>
            new Series(CounterType.Memory.ToString()){
                ChartType = SeriesChartType.FastLine,
                Color = Color.MediumBlue}},
            {CounterType.Disk, () =>
            new Series(CounterType.Disk.ToString()){
                ChartType = SeriesChartType.SplineArea,
                Color = Color.DarkRed}},
        };

        private Dictionary<CounterType, IActorRef> _counterActors;

        private IActorRef _chartingActor;

        public PerformanceCounterCoordinatorActor(IActorRef chartingActor) :
            this(chartingActor, new Dictionary<CounterType, IActorRef>())
        {
        }

        public PerformanceCounterCoordinatorActor(IActorRef chartingActor,
            Dictionary<CounterType, IActorRef> counterActors)
        {
            _chartingActor = chartingActor;
            _counterActors = counterActors;

            Receive<Watch>(watch =>
            {
                if (!_counterActors.ContainsKey(watch.Counter))
                {
                    // create a child actor to monitor this counter if
                    // one doesn't exist already
                    var counterActor = Context.ActorOf(Props.Create(() =>
                        new PerformanceCounterActor(watch.Counter.ToString(),
                                CounterGenerators[watch.Counter])));

                    // add this counter actor to our index
                    _counterActors[watch.Counter] = counterActor;
                }

                // register this series with the ChartingActor
                _chartingActor.Tell(new ChartingActor.AddSeries(
                    CounterSeries[watch.Counter]()));

                // tell the counter actor to begin publishing its
                // statistics to the _chartingActor
                _counterActors[watch.Counter].Tell(new SubscribeCounter(watch.Counter,
                    _chartingActor));
            });

            Receive<Unwatch>(unwatch =>
            {
                if (!_counterActors.ContainsKey(unwatch.Counter))
                {
                    return; // noop
                }

                // unsubscribe the ChartingActor from receiving any more updates
                _counterActors[unwatch.Counter].Tell(new UnsubscribeCounter(
                    unwatch.Counter, _chartingActor));

                // remove this series from the ChartingActor
                _chartingActor.Tell(new ChartingActor.RemoveSeries(
                    unwatch.Counter.ToString()));
            });
        }
    }
}
  • App.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
   <configSections>
      <section name="akka" type="Akka.Configuration.Hocon.AkkaConfigurationSection, Akka" />
   </configSections>
   <akka>
      <hocon>
         <![CDATA[
          akka {
            actor{
              deployment{
                #used to configure our ChartingActor
                /charting{
                  dispatcher = akka.actor.synchronized-dispatcher #causes ChartingActor to run on the UI thread for WinForms
                }
              }
            }
          }
      ]]>
      </hocon>
   </akka>
</configuration>
  • ChartDataHelper.cs
using Akka.Util;
using System;
using System.Linq;
using System.Windows.Forms.DataVisualization.Charting;

namespace Akka_Test
{
    /// <summary>
    /// Helper class for creating random data for chart plots
    /// </summary>
    public static class ChartDataHelper
    {
        public static Series RandomSeries(string seriesName, SeriesChartType type = SeriesChartType.Line, int points = 100)
        {
            var series = new Series(seriesName) { ChartType = type };
            foreach (var i in Enumerable.Range(0, points))
            {
                var rng = ThreadLocalRandom.Current.NextDouble();
                series.Points.Add(new DataPoint(i, 2.0 * Math.Sin(rng) + Math.Sin(rng / 4.5)));
            }
            series.BorderWidth = 3;
            return series;
        }
    }
}
  • Form1.cs
using Akka.Actor;
using Akka.Util.Internal;
using Akka_Test.Actors;
using System.Collections.Generic;
using System.Windows.Forms;

namespace Akka_Test
{
    public partial class Form1 : Form
    {
        private IActorRef _chartActor;
        private readonly AtomicCounter _seriesCounter = new AtomicCounter(1);

        private IActorRef _coordinatorActor;
        private Dictionary<CounterType, IActorRef> _toggleActors = new Dictionary<CounterType,
            IActorRef>();


        public Form1()
        {
            InitializeComponent();
        }

        private void Main_Load(object sender, System.EventArgs e)
        {
            _chartActor = Program.ChartActors.ActorOf(Props.Create(() =>
            new ChartingActor(sysChart, btnPauseResume)), "charting");
            _chartActor.Tell(new ChartingActor.InitializeChart(null)); //no initial series

            _coordinatorActor = Program.ChartActors.ActorOf(Props.Create(() =>
            new PerformanceCounterCoordinatorActor(_chartActor)), "counters");

            //CPU Button toggle actor
            _toggleActors[CounterType.Cpu] = Program.ChartActors.ActorOf(
       Props.Create(() => new ButtonToggleActor(_coordinatorActor, btnCpu,
       CounterType.Cpu, false))
       .WithDispatcher("akka.actor.synchronized-dispatcher"));

            //Memory Button toggle actor
            _toggleActors[CounterType.Memory] = Program.ChartActors.ActorOf(
       Props.Create(() => new ButtonToggleActor(_coordinatorActor, btnMemory,
        CounterType.Memory, false))
        .WithDispatcher("akka.actor.synchronized-dispatcher"));

            //DISK Butto toggle actor
            _toggleActors[CounterType.Disk] = Program.ChartActors.ActorOf(
       Props.Create(() => new ButtonToggleActor(_coordinatorActor, btnDisk,
       CounterType.Disk, false))
       .WithDispatcher("akka.actor.synchronized-dispatcher"));

            //Set the CPU toggle to ON so we start getting some data
            _toggleActors[CounterType.Cpu].Tell(new ButtonToggleActor.Toggle());
        }

        private void Main_FormClosing(object sender, FormClosingEventArgs e)
        {
            //shut down the charting actor
            _chartActor.Tell(PoisonPill.Instance);

            //shut down the ActorSystem
            Program.ChartActors.Terminate();
        }

        private void btnCpu_Click(object sender, System.EventArgs e)
        {
            _toggleActors[CounterType.Cpu].Tell(new ButtonToggleActor.Toggle());
        }

        private void btnMemory_Click(object sender, System.EventArgs e)
        {
            _toggleActors[CounterType.Memory].Tell(new ButtonToggleActor.Toggle());
        }

        private void btnDisk_Click(object sender, System.EventArgs e)
        {
            _toggleActors[CounterType.Disk].Tell(new ButtonToggleActor.Toggle());
        }

        private void btnPauseResume_Click(object sender, System.EventArgs e)
        {
            _chartActor.Tell(new ChartingActor.TogglePause());
        }
    }
}
  • Form1.designer.cs

namespace Akka_Test
{
    partial class Form1
    {
        /// <summary>
        /// 필수 디자이너 변수입니다.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// 사용 중인 모든 리소스를 정리합니다.
        /// </summary>
        /// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form 디자이너에서 생성한 코드

        /// <summary>
        /// 디자이너 지원에 필요한 메서드입니다. 
        /// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
        /// </summary>
        private void InitializeComponent()
        {
            System.Windows.Forms.DataVisualization.Charting.ChartArea chartArea2 = new System.Windows.Forms.DataVisualization.Charting.ChartArea();
            System.Windows.Forms.DataVisualization.Charting.Legend legend2 = new System.Windows.Forms.DataVisualization.Charting.Legend();
            System.Windows.Forms.DataVisualization.Charting.Series series2 = new System.Windows.Forms.DataVisualization.Charting.Series();
            this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
            this.panel2 = new System.Windows.Forms.Panel();
            this.panel1 = new System.Windows.Forms.Panel();
            this.sysChart = new System.Windows.Forms.DataVisualization.Charting.Chart();
            this.btnCpu = new System.Windows.Forms.Button();
            this.btnMemory = new System.Windows.Forms.Button();
            this.btnDisk = new System.Windows.Forms.Button();
            this.btnPauseResume = new System.Windows.Forms.Button();
            this.tableLayoutPanel1.SuspendLayout();
            this.panel2.SuspendLayout();
            this.panel1.SuspendLayout();
            ((System.ComponentModel.ISupportInitialize)(this.sysChart)).BeginInit();
            this.SuspendLayout();
            // 
            // tableLayoutPanel1
            // 
            this.tableLayoutPanel1.ColumnCount = 2;
            this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 82.625F));
            this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 17.375F));
            this.tableLayoutPanel1.Controls.Add(this.panel2, 1, 0);
            this.tableLayoutPanel1.Controls.Add(this.panel1, 0, 0);
            this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
            this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 0);
            this.tableLayoutPanel1.Name = "tableLayoutPanel1";
            this.tableLayoutPanel1.RowCount = 1;
            this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
            this.tableLayoutPanel1.Size = new System.Drawing.Size(800, 450);
            this.tableLayoutPanel1.TabIndex = 0;
            // 
            // panel2
            // 
            this.panel2.Controls.Add(this.btnPauseResume);
            this.panel2.Controls.Add(this.btnDisk);
            this.panel2.Controls.Add(this.btnMemory);
            this.panel2.Controls.Add(this.btnCpu);
            this.panel2.Dock = System.Windows.Forms.DockStyle.Fill;
            this.panel2.Location = new System.Drawing.Point(664, 3);
            this.panel2.Name = "panel2";
            this.panel2.Size = new System.Drawing.Size(133, 444);
            this.panel2.TabIndex = 1;
            // 
            // panel1
            // 
            this.panel1.Controls.Add(this.sysChart);
            this.panel1.Dock = System.Windows.Forms.DockStyle.Fill;
            this.panel1.Location = new System.Drawing.Point(3, 3);
            this.panel1.Name = "panel1";
            this.panel1.Size = new System.Drawing.Size(655, 444);
            this.panel1.TabIndex = 0;
            // 
            // sysChart
            // 
            chartArea2.Name = "ChartArea1";
            this.sysChart.ChartAreas.Add(chartArea2);
            this.sysChart.Dock = System.Windows.Forms.DockStyle.Fill;
            legend2.Name = "Legend1";
            this.sysChart.Legends.Add(legend2);
            this.sysChart.Location = new System.Drawing.Point(0, 0);
            this.sysChart.Name = "sysChart";
            series2.ChartArea = "ChartArea1";
            series2.Legend = "Legend1";
            series2.Name = "Series1";
            this.sysChart.Series.Add(series2);
            this.sysChart.Size = new System.Drawing.Size(655, 444);
            this.sysChart.TabIndex = 0;
            this.sysChart.Text = "chart1";
            // 
            // btnCpu
            // 
            this.btnCpu.Font = new System.Drawing.Font("굴림", 9.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
            this.btnCpu.Location = new System.Drawing.Point(2, 259);
            this.btnCpu.Name = "btnCpu";
            this.btnCpu.Size = new System.Drawing.Size(130, 54);
            this.btnCpu.TabIndex = 0;
            this.btnCpu.Text = "CPU (ON)";
            this.btnCpu.UseVisualStyleBackColor = true;
            this.btnCpu.Click += new System.EventHandler(this.btnCpu_Click);
            // 
            // btnMemory
            // 
            this.btnMemory.Font = new System.Drawing.Font("굴림", 9.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
            this.btnMemory.Location = new System.Drawing.Point(2, 319);
            this.btnMemory.Name = "btnMemory";
            this.btnMemory.Size = new System.Drawing.Size(130, 54);
            this.btnMemory.TabIndex = 1;
            this.btnMemory.Text = "MEMORY (OFF)";
            this.btnMemory.UseVisualStyleBackColor = true;
            this.btnMemory.Click += new System.EventHandler(this.btnMemory_Click);
            // 
            // btnDisk
            // 
            this.btnDisk.Font = new System.Drawing.Font("굴림", 9.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
            this.btnDisk.Location = new System.Drawing.Point(3, 379);
            this.btnDisk.Name = "btnDisk";
            this.btnDisk.Size = new System.Drawing.Size(130, 54);
            this.btnDisk.TabIndex = 2;
            this.btnDisk.Text = "DISK (OFF)";
            this.btnDisk.UseVisualStyleBackColor = true;
            this.btnDisk.Click += new System.EventHandler(this.btnDisk_Click);
            // 
            // btnPauseResume
            // 
            this.btnPauseResume.Font = new System.Drawing.Font("굴림", 9.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
            this.btnPauseResume.Location = new System.Drawing.Point(2, 144);
            this.btnPauseResume.Name = "btnPauseResume";
            this.btnPauseResume.Size = new System.Drawing.Size(130, 54);
            this.btnPauseResume.TabIndex = 3;
            this.btnPauseResume.Text = "PAUSE ||";
            this.btnPauseResume.UseVisualStyleBackColor = true;
            this.btnPauseResume.Click += new System.EventHandler(this.btnPauseResume_Click);
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(800, 450);
            this.Controls.Add(this.tableLayoutPanel1);
            this.Name = "Form1";
            this.Text = "Form1";
            this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Main_FormClosing);
            this.Load += new System.EventHandler(this.Main_Load);
            this.tableLayoutPanel1.ResumeLayout(false);
            this.panel2.ResumeLayout(false);
            this.panel1.ResumeLayout(false);
            ((System.ComponentModel.ISupportInitialize)(this.sysChart)).EndInit();
            this.ResumeLayout(false);

        }

        #endregion

        private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
        private System.Windows.Forms.Panel panel2;
        private System.Windows.Forms.Panel panel1;
        private System.Windows.Forms.DataVisualization.Charting.Chart sysChart;
        private System.Windows.Forms.Button btnDisk;
        private System.Windows.Forms.Button btnMemory;
        private System.Windows.Forms.Button btnCpu;
        private System.Windows.Forms.Button btnPauseResume;
    }
}
  • Program.cs
using Akka.Actor;
using System;
using System.Windows.Forms;

namespace Akka_Test
{
    static class Program
    {
        /// <summary>
        /// ActorSystem we'll be using to publish data to charts
        /// and subscribe from performance counters
        /// </summary>
        public static ActorSystem ChartActors;

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            ChartActors = ActorSystem.Create("ChartActors");
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}
  • 실행 결과

728x90

이 글을 공유하기

댓글

Designed by JB FACTORY