UntypedActor는, 이전 섹션에서 익힌 ReceiveActor 처럼 생성자에서 Type매칭에의해 메시지 처리기를 등록하는게 아닌 ,

기본 처리 메스드를 재정의하여 기본 메시지 처리외에 커스텀한 기능을 부여할시 사용됩니다.

이번장에서는 Actor의 기능을 확장하여 유용한 몇가지 기능들을 살펴보겠습니다.



OnReceive - 기본 메시지 처리 재정의

Untyped Actor
    public class MyActorSame : UntypedActor
    {        
        protected override void OnReceive(object message)
        {
            if(message is string)
            {
                if (message as string == "createChild")
                {
                    Context.ActorOf<MyActor>("myChild");
                    Sender.Tell("Create Child Succed:myChild");
                }
                else
                {
                    Sender.Tell("RE:" + message);
                }
            }
            else if(message is SomeMessage)
            {
                Sender.Tell("RE:" + (message as SomeMessage).message );
            }
        }
    }


ReceiveActor 가 수신가능 액터로 기본메시지 처리에 좀더 추상화가 되었다면

Untyped Actor 는 좀더 커스텀한 액터 설계시 상속을 받아 재정의가 많이 이루어질시 활용이되며

메시지수신부 OnReceive 를 직접 Override해서 사용하는것 외에는 차이가 없으며

아래에 설명된 확장 기능들은 모두 ReceveActor에서도 사용가능합니다.


Become/Unbecome


  사용목적 : 메시지 처리기가 어떠한 상태에따라 다른 메시지 처리기가 필요하다고 가정합시다.

일반적으로 상태값을 내부변수로 두고 상태 값에 따라 다중 IF분기가 일어날것입니다.

이는 코드 가독성이 떨어지거나, 변화 Flow를 파악하기 힘들어지며 유지보수가 어려운것으로 연결됩니다.

이경우, 메시지 처리기자체를 변경시킬수가 있어서 좀더 유연하게 메시지 설계가 가능해집니다.

FSM(Finite-state machine) 패턴이 반영되었다고 볼수있습니다.


  • Become : 메시지 처리기를 변경합니다.
  • Unbecome : 기본 메시지 처리로 변경합니다.

처리기가 변하는 Actor
	//foo 라고하면 행복해졌다가, bar라고하면 화나는 ACTOR , 동일한 상태에서는 상태변경없이 메시지전송
    public class HotSwapActor : UntypedActor
    {
		//기본탑재 로그,이제부터는 시간,스레드,액터정보등이 표시된 console.writeline대신 AKKA LOG를 사용하겠습니다.  using Akka.Event
        private ILoggingAdapter log = Context.GetLogger();          
        protected override void OnReceive(object message)
        {
            log.Info("NORMAL:"+message.ToString());
            switch (message as string)
            {
                case "foo":                    
                    log.Info("행복해짐");
                    Become(Happy);
                    break;
                case "bar":
                    log.Info("화남");
                    Become(Angry);
                    break;
            }
        }

        private void Angry(object message)
        {
            log.Info("Angry:" + message.ToString());
            switch (message as string)
            {
                case "foo":
                    log.Info("행복해짐");
                    Become(Happy);
                    break;
                case "bar":                    
                    log.Info("이미 화가났다.");
                    Sender.Tell("이미 화가났다.");
                    break;
            }
        }

        private void Happy(object message)
        {
            log.Info("Happy:" + message.ToString());
            switch (message as string)
            {
                case "foo":
                    log.Info("이미 행복하다.");
                    Sender.Tell("이미 행복하다.");
                    break;
                case "bar":
                    log.Info("화남");
                    Become(Angry);
                    break;
            }
        }
    }

TestCode:foo 라고하면 행복해졌다가, bar라고하면 화나는 ACTOR 결국 행복해짐
        protected void SomeTest3() //BecomeTest
        {
            IActorRef myActor = actorSystem.ActorOf<HotSwapActor>("myactor");            
            myActor.Tell("bar");
            myActor.Tell("foo");
            myActor.Tell("bar");
            myActor.Tell("foo");
            myActor.Tell("bar");
            myActor.Tell("bar");
            myActor.Tell("foo");
        }

[INFO][2017-09-05 오전 6:50:10][Thread 0005][[akka://ServiceA/user/myactor#16204
11193]] NORMAL:bar
[INFO][2017-09-05 오전 6:50:10][Thread 0005][[akka://ServiceA/user/myactor#16204
11193]] 화남
[INFO][2017-09-05 오전 6:50:10][Thread 0005][[akka://ServiceA/user/myactor#16204
11193]] Angry:foo
[INFO][2017-09-05 오전 6:50:10][Thread 0005][[akka://ServiceA/user/myactor#16204
11193]] 행복해짐
[INFO][2017-09-05 오전 6:50:10][Thread 0005][[akka://ServiceA/user/myactor#16204
11193]] Happy:bar
[INFO][2017-09-05 오전 6:50:10][Thread 0005][[akka://ServiceA/user/myactor#16204
11193]] 화남
[INFO][2017-09-05 오전 6:50:10][Thread 0005][[akka://ServiceA/user/myactor#16204
11193]] Angry:foo
[INFO][2017-09-05 오전 6:50:10][Thread 0005][[akka://ServiceA/user/myactor#16204
11193]] 행복해짐
[INFO][2017-09-05 오전 6:50:10][Thread 0005][[akka://ServiceA/user/myactor#16204
11193]] Happy:bar
[INFO][2017-09-05 오전 6:50:10][Thread 0005][[akka://ServiceA/user/myactor#16204
11193]] 화남
[INFO][2017-09-05 오전 6:50:10][Thread 0005][[akka://ServiceA/user/myactor#16204
11193]] Angry:bar
[INFO][2017-09-05 오전 6:50:10][Thread 0005][[akka://ServiceA/user/myactor#16204
11193]] 이미 화가났다.
[INFO][2017-09-05 오전 6:50:10][Thread 0005][[akka://ServiceA/user/myactor#16204
11193]] Angry:foo
[INFO][2017-09-05 오전 6:50:10][Thread 0005][[akka://ServiceA/user/myactor#16204
11193]] 행복해짐
[INFO][2017-09-05 오전 6:50:10][Thread 0006][akka://ServiceA/deadLetters] Messag


액터의 Lifecyle


    public class BasicActor : UntypedActor
    {
        private ILoggingAdapter log = Context.GetLogger();

        protected override void PreStart()
        {

        }

        protected override void PreRestart(Exception reason, object message)
        {

        }

        protected override void OnReceive(object message)
        {
            //handle messages here
            log.Info("BasicActor:GetSomeMessage " + message.ToString());
            Sender.Tell("OK");
        }

        protected override void PostStop()
        {

        }

        protected override void PostRestart(Exception reason)
        {

        }
    }

 메시지 수신처리외에 액터의 라이프 사이클과 관련된 Hooks 대한 처리가 가능합니다.


Hooks :

  • PreStart : 액터가 생성될시 호출이 됩니다.
  • PostStop : 액터의 작동을 멈출시 호출이 되며, 만약 멈출 액터에게 메시지를 보내면 DeadLeeters가 발생됩니다.
  • PreRestart : 액터문제로인해 복구를위해 재시작이 될때, 실패 메시지를 통보받으며 새롭게 준비하는데 좋은장소입니다.
  • PostRestart : 재시작을 야기한 예외를 가지고 호출되며, 기본적으로 정상적인 시작 사례와 유사합니다.


Actors-Fault Tolerance Part에서 좀더 자세한 장애처리 전략 설명

Monitoring

 어떠한 액터에게, 특정액터를 감시하게 하거나 kill을 할수 있는 권한을 준다고 했을시

Context.Watch에 감시대상 액터를 등록함으로 , 대상이 사라질지 감지가 가능합니다.


심플한 감시자 액터
    public class WatchActor : UntypedActor
    {
        private ILoggingAdapter log = Context.GetLogger();
        private IActorRef child;
        private IActorRef lastSender = Context.System.DeadLetters;

        public WatchActor(IActorRef targetActor)
        {
            child = targetActor;
            Context.Watch(targetActor); // <-- this is the only call needed for registration
        }

        protected override void OnReceive(object message)
        {
            log.Info("WatchActor:GetSomeMessage " + message.ToString());

            if (message is string)
            {
                switch(message as string)
                {
                    case "kill":
                        Context.Stop(child);
                        lastSender = Sender;
                     break;
                }
            }

            if(message is Terminated)
            {
                if( ((Terminated)message).ActorRef.Equals(child))
                {
                    log.Info("감시대상이 사라짐");
                }
            }
        }

        public static Props Props(IActorRef stoking)
        {
            return Akka.Actor.Props.Create(() => new WatchActor(stoking));
        }

    }
테스트
            IActorRef myActor = actorSystem.ActorOf<BasicActor>("myactor");
            Props watchProps = WatchActor.Props(myActor);
            IActorRef watcher = actorSystem.ActorOf(watchProps,"watcher");

            var result = myActor.Ask("나는 살아있다.").Result;
            actorSystem.Stop(myActor); //myActor를 임의로 Stop하여 감시가되는지 체크

[INFO][2017-09-06 오전 4:53:45][Thread 0006][[akka://ServiceA/user/myactor#14600
16350]] BasicActor:GetSomeMessage 나는 살아있다.

[INFO][2017-09-06 오전 4:53:45][Thread 0004][[akka://ServiceA/user/watcher#11499
84538]] WatchActor:GetSomeMessage <Terminated>: [akka://ServiceA/user/myactor#14
60016350] - ExistenceConfirmed=True

[INFO][2017-09-06 오전 4:53:45][Thread 0004][[akka://ServiceA/user/watcher#11499
84538]] 감시대상이 사라짐


Forward message


target.Forward(result, Context);


tatget : 다른 액터
Context : me

다른액터로 받은 메시지를 전달할때 사용합니다.  이것은 라우터,로드밸런스,복제 등에서 유용하게 쓰입니다.

액터종료방법


Stop

Context.Stop(child);
actorSystem.Stop(child);

 동일한 System내에서, 또는 자신이 가지고 있는 하위 액터중

자신의 하위액터를 중지 시킵니다.

PoisonPill

myActor.Tell(PoisonPill.Instance, Sender);

메시지를 통해 액터를 멈추고자 할때 이용됩니다. 일반적이고 안정적입니다.


Killing

// kill the 'victim' actor
victim.Tell(Akka.Actor.Kill.Instance, ActorRef.NoSender);

Kill메시지는  액터를 ActorKilledException를 발생 강제로 죽여서, 액터를 죽입니다.

그리고 관리자에게 액터를 다시 시작시킬것인가 혹은 종료시킬것인가? 의 설정을 통해 다음 종료전략이 실행됩니다.


GracefulStop

await manager.GracefulStop(TimeSpan.FromMilliseconds(5), "shutdown");



안전한 액터 종료(Graceful Stop)


 관리자 액터가 , 어떠한 액터를 감시하고있고, 모두 안전하게 종료후 자기자신도 마지막에

안전하게 종료할시 사용됩니다. 여기서 안전하다란 종료는 어플리케이션 레벨에서 설계해야하며

GracefulStop()은 종료시도와함께, 종속된 Actor( Watch에 등록된)가 모두 종료되었는가를 확인할수 있습니다. 

이것은 안전한 어플리케이션 종료시에 활용이됩니다.


using Akka.Actor;
using Akka.TestKit;
using AkkaDotModule.ActorSample;
using AkkaNetCoreTest;
using System;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;

namespace TestAkkaDotModule.TestActors
{
    public class HelloActor2 : ReceiveActor
    {        
        private string MyName { get; set; }

        private IActorRef target = null;

        public HelloActor2(string name)
        {
            MyName = name;

            ReceiveAsync<string>(async message =>
            {
                string inComeMessage = $"[{MyName}] : {message}";

            });

        }
    }

    public class TestManager : UntypedActor
    {
        IActorRef probe;

        private IActorRef worker = Context.Watch(Context.ActorOf(
            Props.Create(() => new HelloActor2("hello")), "worker" ));

        public TestManager(IActorRef _probe)
        {
            probe = _probe;
        }

        protected override void OnReceive(object message)
        {
            switch (message)
            {
                case "hello":
                    worker.Tell("hello");
                    break;
                case "shutdown":                    
                    worker.Tell(PoisonPill.Instance, Self);
                    Context.Become(ShuttingDown);   //종료모드로 메시지처리 상태변경
                    break;                
            }
        }

        private void ShuttingDown(object message)
        {
            switch (message)
            {
                case "hello": //어떤 잡을 시키려고 할시,셧다운중임을 알립니다.
                    Sender.Tell("service unavailable, shutting down", Self);
                    break;
                case Terminated t:
                    probe.Tell("SafeClose");
                    Context.Stop(Self);
                    break;
            }
        }
    }

    public class LifeCycleActorTest : TestKitXunit
    {
        protected TestProbe probe;

        protected IActorRef manager;

        public LifeCycleActorTest(ITestOutputHelper output) : base(output)
        {
            Setup();
        }

        public void Setup()
        {
            //테스트 관찰자..
            probe = this.CreateTestProbe();

            //GraceFulDown을 위한 Manager액터 생성
            manager = Sys.ActorOf(Props.Create(() => new TestManager(probe)));
            
        }

        [Theory(DisplayName = "GracefulStopTest")]
        [InlineData(5)]
        public async Task GtaceFulStopAreOK(int waitTimeSec)
        {
            //Step:
            // 1.GracefulStop 을통한 종료 시그널 발생 
            // 2.자식 액터종료(PoisonPill, 지금까지 받은메시지까지만 처리하고)
            // 3.GracefulStop , Terminated 될때까지 대기
            // 검증 : 안전한 종료메시지가 왔는지 검사

            await manager.GracefulStop(TimeSpan.FromMilliseconds(3), "shutdown");

            probe.ExpectMsg("SafeClose", TimeSpan.FromSeconds(waitTimeSec));
        }
    }
}







액터접근 주소체계



  ActorPath는 총 4가지로 구분이 되며  http의 RestAPI의 주소체계와 거의 흡사하다고 볼수 있습니다.

여기서 Protocol,ActorSystem,Address 요소를 생략하면 ex>'user/actorName1'  자신의 LocalSystem에만 접근하겠다란 의미입니다.


4요소

  • Protocol : AKKA를 사용하는 프로토콜임을 알림
  • ActorSystem : 주로 어플리케이션 하나의 이름으로, ActorSystem 최초 생성시 이름지정
  • Address : 네트워크 주소로 ip:port 로 표현
  • Path : 서비스 개발자가, 계층적으로 배치한 Actor 이름


RestAPI호출 접근법과는 큰차이가 없어보이나 내부적으로는 피어투피어 실시간 통신으로 메시지가 처리가됩니다.

다음 RemoteActor실습시 가상의 서비스 시나리오를 만들어 성능 차이를 프로파일링 비교해보겠습니다. 

  • RESTAPI 호출 VS RemoteActor 호출
  • 항상 DB Read 가 발생하는 RESTAPI VS Actor를 사용하여 DB Read가 없는 RESTAPI
  • Unable to render Jira issues macro, execution error.