Announcement

지난 포스트 Akka 공부하기 - 01.3 Props와 and IActorRef에 이어지는 내용입니다.
이 포스트는 github petabridge/akka-bootcamp/…/Unit-1/lesson4를 번역&정리한 글입니다.

Intro

이번 레슨은 액터 모델의 동작과 코드베이스의 특징을 이해하는 데 큰 도움을 줄 것입니다.
바로 들어가 봅시다!

Key concepts / background

액터 계층 구조에 대해 깊게 들어가기 전에 알아봅시다: 왜 우리는 계층구조가 필요하죠?

계층 구조를 사용하는 두 가지 중요한 키 포인트가 있습니다:

  1. 작업을 원자화하고 대용량 데이터를 처리하기 쉬운 양으로 변환하기 위해
  2. 에러를 억제하고 시스템을 회복력 있게 만들기 위해

계층 구조의 작업 원자화

계층 구조를 갖는 것은 아주 작은 조각으로 작업을 쪼개고, 서로 다른 계층 레벨에서 다른 전문 기술을 활용할 수 있게 합니다.

액터 시스템에서 이것이 실현되는 일반적으로 방법은 큰 데이터 스트림을 원자화하는 것입니다, 그것이 작고 쉽게 처리할 만한 코드 조각이 될 때까지 계속 원자화를 반복합니다.

트위터를 예로 들어봅시다(JVM Akka의 사용자). Akka를 사용하면 그들의 대용량 데이터 수집을 작고 처리하기 쉬운 정보의 흐름으로 분해할 수 있습니다.

예를 들어서, 트위터의 경우에는 거대한 소방호스를 통해 뿜어져 나오는 것 같은 트윗들을 각 유저들의 타임라인에 작은 물줄기 하나하나로 나누어 분산할 수 있고, Akka를 사용하여 해당 유저의 스트림으로 도착한 메시지를 websocket 등을 통해 푸시할 수도 있습니다.

계층 구조와 복원력 있는 시스템

계층 구조를 사용함으로써 위험의 계층화와 전문화가 가능합니다.

군대가 어떻게 일하는지 생각해봅시다. 군대는 일반적인 전략을 세우고 모든 것을 감독하지만, 모두가 위험이 가장 많은 전투의 최전선에 서지 않습니다. 그러나 그들은 폭넓ㅇ느 영향력을 가지고 모든 것을 지도합니다. 동시에, 최전선에 있는 낮은 계급의 병사들은 위험한 작전을 수행하고 그들이 받은 명령을 수행합니다.

이것은 액터 시스템의 동작과 정확히 일치합니다.

상위 계층의 액터는 본질적으로 좀더 감독적(supervisional)이고, 이러한 점은 액터 시스템이 위험을 낮추고 가장자리로 밀어내게 해줍니다. 계층의 가장자리로 위험한 명령을 밀어 넣음으로써, 시스템은 리스크를 고립시키고 오류로부터 시스템 크래싱없이 복구할 수 있습니다.

이 두 개념은 모두 중요하지만, 이 레슨의 나머지 부분에서는 액터 시스템이 계층 구조를 사용해서 복원력을 발휘할 방법에 대해 중점을 두겠습니다.

어떻게 이룰 수 있냐고요? Supervision.

Supervision이란? 왜 신경써야 하나

Supervision은 액터 시스템이 오류(failures)에서 빠르고 신속하게 격리와 회복이 이루어질 수 있게 하는 기본 개념입니다. 모든 액터는 자신을 감독(supervision)하는 다른 액터를 가지고 있으며, 이 액터들은 에러가 발생했을 때 복구할 수 있도록 돕습니다. 이것은 계층 구조의 최상단부터 최하단까지 모두에게 해당합니다.

supervision은 애플리케이션이 예기치 않은 오류(unhandled exception, network timeout, etc.)가 발생할 경우, 액터 계층 구조에서 해당 계층에만 영향이 가해지도록 제어합니다.

모든 다른 액터들은 아무 일도 없었던 것처럼 동작할 것입니다. 우리는 이것을 “failure isolation” 또는 “containment”라고 부를 것입니다.

이게 어떻게 이루어질까요? 지금부터 알아봅시다.

액터의 계층 구조

첫 번째, 키 포인트: 모든 액터는 부모를 가지고 있습니다, 그리고 일부 액터는 자식을 가지고 있습니다. 부모는 자식을 감독(supervise)합니다.

부모가 자식을 감독한다는 것은 모든 액터가 감독자(supervisor)를 가진다는 것입니다. 또한 모든 액터가 감독자(supervisor)가 될 수 있음을 말하기도 합니다.

액터 시스템 내에서 액터는 계층 구조로 정리됩니다. ActorSystem 자체에 직접 보고하는 “최상위” 액터와 다른 액터에게 보고하는 “자식(child)” 액터가 있음을 의미합니다.

전반적인 계층 구조는 다음과 같습니다. (잠시 후 하나씩 알아보겠습니다):

hierarchy-overview

계층 구조의 레벨이란

모든 것의 기본은: “Guardians”

“Guardians”는 전체 시스템의 근원(root) 액터입니다.

계층 구조의 최상단 3개의 액터를 말하고 있습니다:

guardians

/ 액터

/ 액터는 액터 시스템 전체의 기본 액터입니다, 그리고 “The Root Guardian.”라고 불립니다. 이 액터는 /system/user 액터(다른 “Guardians”)를 감독(supervises)합니다.

이 하나를 제외한 모든 액터는 그들의 부모를 필요로 합니다. 이 액터는 전형적인 액터 시스템의 바깥(“out of the bubble”)에 있기 때문에 때때로 “bubble-walker”라고 불립니다.

/system 액터

/system 액터는 “The System Guardian”라고 불립니다. 이 액터의 주 역할은 정상적인 방법으로 시스템이 종료되고, 프레임 워크 레벨 기능 및 유틸리티(logging, etc)로 구현된 다른 시스템 액터를 유지/감독합니다. system guardian과 시스템 액터 계층 구조에 대해서는 나중에 포스팅하겠습니다.

/user 액터

여기가 바로 파티가 시작되는 곳입니다! 그리고 당신이 개발자로서 모든 시간을 보낼 곳입니다.

/user액터는 “The Guardian Actor”로 불립니다. 하지만 사용자 관점에서 볼 때 /user는 액터 시스템의 루트이며, 단지 “root actor.”라고 부릅니다.

일반적으로, “root actor”라고 하면 /user액터입니다.

사용자는 Guardians에 대해 걱정할 필요가 없습니다. 우리는 어떠한 예외도 Guardians까지 거품을 일으켜 전체 시스템에 크래시가 발생하지 않도록 /user아래에서 적절하게 감독합니다.

/user 액터 계층 구조

액터 계층 구조의 주요 포인트입니다 : 당신 애플리케이션 내에서 당신이 정의한 모든 액터들이 속합니다.

user_actors

/user 액터의 직계 자식을 “최상위 액터(top level actors.)”라고 부릅니다.

액터는 항상 다른 배우의 자식으로 만들어집니다.

액터 시스템 자체의 컨텍스트(context)를 활용해 직접 배우를 만들면, 새 액터는 최상위 액터가 됩니다:

// create the top level actors from above diagram
IActorRef a1 = MyActorSystem.ActorOf(Props.Create<BasicActor>(), "a1");
IActorRef a2 = MyActorSystem.ActorOf(Props.Create<BasicActor>(), "a2");

이제, a2 컨텍스트 안쪽에서 a2의 자식 액터들을 만들어 봅시다:

// create the children of actor a2
// this is inside actor a2
IActorRef b1 = Context.ActorOf(Props.Create<BasicActor>(), "b1");
IActorRef b2 = Context.ActorOf(Props.Create<BasicActor>(), "b2");

액터 주소(Actor path) == 액터 계층 구조에서의 위치

모든 액터는 주소를 가집니다. 액터에서 다른 액터로 메세지를 보내려면, 주소(일명 “ActorPath”)를 알아야 합니다. 완전한 액터 주소는 다음과 같습니다:

actor_path

“Path” 부분은 해당 액터가 액터 계층 구조의 어디에 있는지 알려줍니다. 각 계층 레벨은 슬래시(‘/’)로 구분합니다.

예를 들어, localhost에서 구동햇을 경우, b2 액터의 완전한 주소는 akka.tcp://MyActorSystem@localhost:9001/user/a2/b2입니다.

여기서 “내 액터 클래스가 특정 계층 위치에서만 살아야 하나요?”라는 질문이 많습니다. 예를 들어 내가 FooActor라는 액터 클래스를 FooActor 자식으로만 배포할 수 있는가? 아니면 어디에나 배포할 수 있는가?

그것에 대한 대답은 어떤 액터든 액터 계층 구조의 어디에나 배치할 수 있다는 것입니다.

자, 이제 흥미로운 것을 해봅시다!

액터 계층 구조에서 supervision은 어떻게 동작하는가

이제 우리는 액터가 어떤 체계로 되어있는지 배웠습니다: 액터는 자식을 감독합니다. 하지만, 그들은 액터 계층 구조에서 바로 아래 단계만 감독(supervise)합니다(액터는 손자, 증손자 등을 감독하지 않습니다).

액터는 계층 구조상 바로 아래 단계의 자식만 감독합니다.

오류가 발생했을 때 감독(supervision)을 시작한다

뭔가 잘못되었을 때, 그때입니다! 자식 배우가 unhandled exception이나 크래시가 발생할 때, 부모에게 도움을 청하고 무엇을 해야 하는지 알려줍니다.

특히, 자식은 Failure클래스의 메세지를 부모에게 보냅니다. 그리고 부모는 무엇을 해야 하는지 결정합니다.

부모가 어떻게 오류를 해결할 수 있는가

여기에 오류 해결법을 결정하는 두 가지 요소가 있습니다:

  1. 자식이 어떻게 실패했는가 (자식이 부모에게 보낸 Failure 메세지에 어떤 타입의 Exception이 포함되었는가)
  2. 부모 액터가 자식의 Failure에 응답으로 어떤 지시가 실행되는가. 이것은 부모의 SupervisionStrategy에 의해 결정됩니다.

다음은 오류가 발생했을 때 이벤트의 순서입니다:

  1. 자식 액터에서 Unhandled exception이 발생(c1), 부모(b1)에 의해 관리(supervised)됩니다.
  2. c1은 작업을 중지합니다.
  3. 시스템은 Failure 메세지에 Exception을 담아 c1에서 b1으로 전달합니다.
  4. b1c1에게 무엇을 할 지 지시합니다.
  5. 삶은 계속되고, 시스템의 영향 받은 부분은 집 전체를 태우지 않고 스스로 치유됩니다. 고양이와 유니콘에게 무료 아이스크림과 커피를 나누어주고, 푹신한 무기재 위에서 안도합니다. 예이!

Supervision directives

자식에게서 에러를 받으면, 부모는 한 가지 행동을 취할 수 있습니다(“지시 directives”). 감독 전략(supervision strategy)은 예외 타입에 따라 지시를 매핑하므로, 여러 에러의 유형에 따라 적절하게 처리할 수 있습니다.

다음은 감독 전략(supervision directives, 즉 감독자가 할 수 있는 결정)의 종류입니다:

  • Restart the child (default): 일반적인 경우이며, 기본값입니다.
  • Stop the child: 자식 액터를 영구히 종료합니다.
  • Escalate the error (and stop itself): 이건 부모가 다음과 같이 말하는겁니다. “뭘 해야 할지 모르겠어! 모든 것을 멈추고 ‘내’ 부모에게 물어봐야겠어!”
  • Resume processing (ignores the error): 일반적으로 사용하지 않습니다. 일단 무시합니다.

여기서 알아야 할 중요한 점은 부모에게 취해진 행동이 자식들에게 전파된다는 것입니다. 만약 부모가 정지된다면, 모든 자식은 정지합니다. 부모가 재시작된다면, 모든 자식은 재시작됩니다.

Supervision strategies

두 가지 기본 감독 전략(Supervision strategies)이 있습니다:

  1. One-For-One Strategy (default)
  2. All-For-One Strategy

이 두 가지의 기본적인 차이점은 에러 해결 지시 효과가 얼마나 널리 퍼지는지입니다.

One-For-One 부모의 지시가 실패한 자식 액터에게만 적용됩니다. 실패한 자식의 형제자매에게 영향을 끼치지 않습니다. 이것은 특별히 지정하지 않는 이상 기본값으로 동작합니다. (또한 당신은 커스텀 감독 전략을 정의할 수도 있습니다.)

All-For-One 부모의 지시가 실패한 자식과 실패한 자식의 모든 형제자매에게 적용됩니다.

감독 전략에서 또 다른 중요한 선택은, 자식이 얼마의 시간 동안에 몇 번의 실패를 허용하는가입니다. (e.g. “60초 이내에 10번 이하의 실패를 허용하고, 초과할 경우 종료합니다.”).

다음은 감독 전략의 예시입니다:

public class MyActor : UntypedActor
{
    // if any child of MyActor throws an exception, apply the rules below
    // e.g. Restart the child, if 10 exceptions occur in 30 seconds or
    // less, then stop the actor
    protected override SupervisorStrategy SupervisorStrategy()
    {
        return new OneForOneStrategy(// or AllForOneStrategy
            maxNrOfRetries: 10,
            withinTimeRange: TimeSpan.FromSeconds(30),
            localOnlyDecider: x =>
            {
                // Maybe ArithmeticException is not application critical
                // so we just ignore the error and keep going.
                if (x is ArithmeticException) return Directive.Resume;

                // Error that we have no idea what to do with
                else if (x is InsanelyBadException) return Directive.Escalate;

                // Error that we can't recover from, stop the failing child
                else if (x is NotSupportedException) return Directive.Stop;

                // otherwise restart the failing child
                else return Directive.Restart;
            });
    }

    ...
}

핵심은 Containment

감독 전략과 지시의 전체적인 핵심은 시스템 내에서 오류를 포함하고 자가 치유하는 것이므로, 시스템 전체에 크래시가 발생하지 않는 것입니다. 어떻게 해야 합니까?

우리는 잠재적으로 위험한 작업을 부모에서 자식에게 전달합니다. 이 작업은 위험한 작업을 수행하는 것입니다.

예를 들어, 우리가 월드컵 동안에 통계 시스템을 운영한다고 해봅시다. 월드컵의 점수와 플레이어 통계를 운영합니다.

월드컵이 진행되는 동안 처리 한계에 다다를 정도의 엄청난 양의 API 요청이 발생할 겁니다. 때로는 크래시가 발생하기도 하고요.(FIFA에 대한 비하가 아닙니다, 우리는 피파와 월드컵을 사랑합니다) 독일 - 가나전을 예시로 들어보겠습니다.

스코어 키퍼는 게임이 진행되는 동안 정기적으로 데이터를 업데이트해야 합니다. FIFA가 관리하는 외부 API를 호출해서 필요한 데이터를 가져온다고 가정합니다.

이러한 네트워크 호출은 위험합니다! 만약 요청이 오류를 발생시키면, 호출을 시작한 액터는 크래시가 발생합니다. 그러면 어떻게 보호해야 할까요?

부모 액터가 상태를 보관하고, 형편없는 네트워크 호출은 자식 액터에게 밀어 넣습니다. 그렇게 하면, 자식 액터가 크래시나더라도, 중요한 데이터를 보관하고 있는 부모에게 영향을 끼치지 않습니다. 우리는 __오류를 지역화(localizing the failure)__하여 시스템 전체에 퍼지는 것을 방지합니다.

액터 계층 구조에서 안전성을 이루는 방법의 예시입니다: error_kernel

이러한 구조를 게임당 하나씩 만들어 병렬로 처리할 수 있다는 점을 상기하세요. 새로운 코드를 작성할 필요가 없습니다!

사람들이 “오류 커널(error kernel)”이라는 용어로 부르기도 합니다. 오류의 영향을 받는 시스템의 양을 나타냅니다. 또한 “오류 커널 패턴(error kernel pattern)”이라는 말도 있습니다. 부모를 격리/보호하기 위해 위험을 자식에게 푸시하는 방식의 약어입니다.

실습

시작하기 전에, 우리의 시스템을 약간 업그레이드 해야 합니다. 우리의 액터 시스템이 파일의 변화를 모니터링 할 수 있게 구성 요소를 추가합니다. 우리가 필요한 대부분의 클래스를 이미 가지고 있지만, 몇 가지 유틸리티 코드를 추가해야 합니다: TailCoordinatorActor, TailActor 그리고 FileObserver.

이번 실습의 목표는 부모/자식 액터 관계를 어떻게 만드는지 보여주는 것입니다.

1단계: 첫 번째 부모/자식 액터 만들기

우리는 클래스에 부모/자식 관계를 만들 준비가 되었습니다.

우리가 바라는 계층 구조에서 TailCoordinatorActor가 자식 액터가 파일을 모니터하고 Tail을 만든다고 연상합시다.

지금은 하나의 TailActor지만 미래에는 쉽게 많은 자식으로 확장하여 여러 파일을 관찰하거나 tailing할 수 있습니다.

TailCoordinatorActor추가

TailCoordinatorActor라는 파일을 만들고 똑같은 이름의 클래스를 생성합니다.

그리고 다음의 코드를 추가합니다. (첫 번째 부모 액터가 될) 우리의 coordinator 액터입니다.

// TailCoordinatorActor.cs
using System;
using Akka.Actor;

namespace WinTail
{
    public class TailCoordinatorActor : UntypedActor
    {
        #region Message types
        
        /// <summary>
        /// Start tailing the file at user-specified path.
        /// </summary>
        public class StartTail
        {
            public StartTail(string filePath, IActorRef reporterActor)
            {
                FilePath = filePath;
                ReporterActor = reporterActor;
            }

            public string FilePath { get; private set; }

            public IActorRef ReporterActor { get; private set; }
        }

        /// <summary>
        /// Stop tailing the file at user-specified path.
        /// </summary>
        public class StopTail
        {
            public StopTail(string filePath)
            {
                FilePath = filePath;
            }

            public string FilePath { get; private set; }
        }
        
        #endregion

        protected override void OnReceive(object message)
        {
            if (message is StartTail)
            {
                var msg = message as StartTail;
                // YOU NEED TO FILL IN HERE
            }
        }
    }
}

TailActor 추가

이제, TailActor라는 클래스를 똑같은 이름의 파일에 추가합니다. 이 액터는 실제로 주어진 파일에 tailing을 수행합니다. TailActor는 적절한 시기에 TailCoordinatorActor에 의해 만들어지고 지시를 받습니다.

TailActor.cs에 다음과 같은 코드를 작성해주세요:

// TailActor.cs
using System.IO;
using System.Text;
using Akka.Actor;

namespace WinTail
{
    /// <summary>
    /// Monitors the file at <see cref="_filePath"/> for changes and sends
    /// file updates to console.
    /// </summary>
    public class TailActor : UntypedActor
    {
        #region Message types

        /// <summary>
        /// Signal that the file has changed, and we need to 
        /// read the next line of the file.
        /// </summary>
        public class FileWrite
        {
            public FileWrite(string fileName)
            {
                FileName = fileName;
            }

            public string FileName { get; private set; }
        }

        /// <summary>
        /// Signal that the OS had an error accessing the file.
        /// </summary>
        public class FileError
        {
            public FileError(string fileName, string reason)
            {
                FileName = fileName;
                Reason = reason;
            }

            public string FileName { get; private set; }

            public string Reason { get; private set; }
        }

        /// <summary>
        /// Signal to read the initial contents of the file at actor startup.
        /// </summary>
        public class InitialRead
        {
            public InitialRead(string fileName, string text)
            {
                FileName = fileName;
                Text = text;
            }

            public string FileName { get; private set; }
            public string Text { get; private set; }
        }

        #endregion

        private readonly string _filePath;
        private readonly IActorRef _reporterActor;
        private readonly FileObserver _observer;
        private readonly Stream _fileStream;
        private readonly StreamReader _fileStreamReader;

        public TailActor(IActorRef reporterActor, string filePath)
        {
            _reporterActor = reporterActor;
            _filePath = filePath;

            // start watching file for changes
            _observer = new FileObserver(Self, Path.GetFullPath(_filePath));
            _observer.Start();

            // open the file stream with shared read/write permissions
            // (so file can be written to while open)
            _fileStream = new FileStream(Path.GetFullPath(_filePath),
                FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
            _fileStreamReader = new StreamReader(_fileStream, Encoding.UTF8);

            // read the initial contents of the file and send it to console as first msg
            var text = _fileStreamReader.ReadToEnd();
            Self.Tell(new InitialRead(_filePath, text));
        }

        protected override void OnReceive(object message)
        {
            if (message is FileWrite)
            {
                // move file cursor forward
                // pull results from cursor to end of file and write to output
                // (this is assuming a log file type format that is append-only)
                var text = _fileStreamReader.ReadToEnd();
                if (!string.IsNullOrEmpty(text))
                {
                    _reporterActor.Tell(text);
                }

            }
            else if (message is FileError)
            {
                var fe = message as FileError;
                _reporterActor.Tell(string.Format("Tail error: {0}", fe.Reason));
            }
            else if (message is InitialRead)
            {
                var ir = message as InitialRead;
                _reporterActor.Tell(ir.Text);
            }
        }
    }
}

TailCoordinatorActor의 Child로 TailActor 추가

빠르게 정리해보면: TailActorTailCoordinatorActor의 자식이 되고, TailCoordinatorActor에 의해 지시를 받습니다.

즉, TailActorTailCoordinatorActor의 컨텍스트(context)에서 생성되어야 합니다.

TailCoordinatorActor.cs로 가서 OnReceive()를 다음과 같이 고칩니다.

// TailCoordinatorActor.OnReceive
protected override void OnReceive(object message)
{
    if (message is StartTail)
    {
        var msg = message as StartTail;
		// here we are creating our first parent/child relationship!
		// the TailActor instance created here is a child
		// of this instance of TailCoordinatorActor
        Context.ActorOf(Props.Create(
          () => new TailActor(msg.ReporterActor, msg.FilePath)));
    }
}

BAM!

당신은 당신의 첫 번째 부모/자식 관계를 성립시켰습니다!

2단계: 간단한 준비

ValidationActorFileValidatorActor로 바꾸세요

우리는 파일을 찾고 있기 때문에, ValidationActorFileValidatorActor로 바꿉니다.

그리고 FileValidatorActor이 코드 처럼바꿔주세요:

// FileValidatorActor.cs
using System.IO;
using Akka.Actor;

namespace WinTail
{
    /// <summary>
    /// Actor that validates user input and signals result to others.
    /// </summary>
    public class FileValidatorActor : UntypedActor
    {
        private readonly IActorRef _consoleWriterActor;
        private readonly IActorRef _tailCoordinatorActor;

        public FileValidatorActor(IActorRef consoleWriterActor,
            IActorRef tailCoordinatorActor)
        {
            _consoleWriterActor = consoleWriterActor;
            _tailCoordinatorActor = tailCoordinatorActor;
        }

        protected override void OnReceive(object message)
        {
            var msg = message as string;
            if (string.IsNullOrEmpty(msg))
            {
                // signal that the user needs to supply an input
                _consoleWriterActor.Tell(new Messages.NullInputError("Input was blank.
                Please try again.\n"));

                // tell sender to continue doing its thing (whatever that may be,
                // this actor doesn't care)
                Sender.Tell(new Messages.ContinueProcessing());
            }
            else
            {
                var valid = IsFileUri(msg);
                if (valid)
                {
                    // signal successful input
                    _consoleWriterActor.Tell(new Messages.InputSuccess(
                        string.Format("Starting processing for {0}", msg)));

                    // start coordinator
                    _tailCoordinatorActor.Tell(new TailCoordinatorActor.StartTail(msg,
                        _consoleWriterActor));
                }
                else
                {
                    // signal that input was bad
                    _consoleWriterActor.Tell(new Messages.ValidationError(
                        string.Format("{0} is not an existing URI on disk.", msg)));

                    // tell sender to continue doing its thing (whatever that
                    // may be, this actor doesn't care)
                    Sender.Tell(new Messages.ContinueProcessing());
                }
            }
        }

        /// <summary>
        /// Checks if file exists at path provided by user.
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        private static bool IsFileUri(string path)
        {
            return File.Exists(path);
        }
    }
}

TailCoordinatorActorIActorRef 만들기

Main()에서 TailCoordinatorActor를 위한 IActorRef를 만들고, fileValidatorActorProps에 넘겨줍니다:

// Program.Main
// make tailCoordinatorActor
Props tailCoordinatorProps = Props.Create(() => new TailCoordinatorActor());
IActorRef tailCoordinatorActor = MyActorSystem.ActorOf(tailCoordinatorProps,
    "tailCoordinatorActor");

// pass tailCoordinatorActor to fileValidatorActorProps (just adding one extra arg)
Props fileValidatorActorProps = Props.Create(() =>
new FileValidatorActor(consoleWriterActor, tailCoordinatorActor));
IActorRef validationActor = MyActorSystem.ActorOf(fileValidatorActorProps,
    "validationActor");

DoPrintInstructions 수정

약간의 수정만 가하면 됩니다. 사용자의 입력을 받는 대신 디스크에서 텍스트 파일을 이용할 것이기 때문입니다.

DoPrintInstructions()를 다음과 같이 수정합니다:

// ConsoleReaderActor.cs
private void DoPrintInstructions()
{
    Console.WriteLine("Please provide the URI of a log file on disk.\n");
}

FileObserver 추가

이건 우리가 당신에게 제공하는 유틸리티 클래스입니다. 파일의 변화를 감시하는 낮은 레벨의 작업을 수행합니다.

FileObserver라는 새로운 클래스를 생성하고 FileObserver.cs를 작성하세요. 만약 당신이 Mono에서 동작하는 경우, Start()메소드에 있는 추가 환경 변수 주석을 해제하세요:

// FileObserver.cs
using System;
using System.IO;
using Akka.Actor;

namespace WinTail
{
    /// <summary>
    /// Turns <see cref="FileSystemWatcher"/> events about a specific file into
    /// messages for <see cref="TailActor"/>.
    /// </summary>
    public class FileObserver : IDisposable
    {
        private readonly IActorRef _tailActor;
        private readonly string _absoluteFilePath;
        private FileSystemWatcher _watcher;
        private readonly string _fileDir;
        private readonly string _fileNameOnly;

        public FileObserver(IActorRef tailActor, string absoluteFilePath)
        {
            _tailActor = tailActor;
            _absoluteFilePath = absoluteFilePath;
            _fileDir = Path.GetDirectoryName(absoluteFilePath);
            _fileNameOnly = Path.GetFileName(absoluteFilePath);
        }

        /// <summary>
        /// Begin monitoring file.
        /// </summary>
        public void Start()
        {
            // Need this for Mono 3.12.0 workaround
            // uncomment next line if you're running on Mono!
            // Environment.SetEnvironmentVariable("MONO_MANAGED_WATCHER", "enabled");

            // make watcher to observe our specific file
            _watcher = new FileSystemWatcher(_fileDir, _fileNameOnly);

            // watch our file for changes to the file name,
            // or new messages being written to file
            _watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite;

            // assign callbacks for event types
            _watcher.Changed += OnFileChanged;
            _watcher.Error += OnFileError;

            // start watching
            _watcher.EnableRaisingEvents = true;
        }

        /// <summary>
        /// Stop monitoring file.
        /// </summary>
        public void Dispose()
        {
            _watcher.Dispose();
        }

        /// <summary>
        /// Callback for <see cref="FileSystemWatcher"/> file error events.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void OnFileError(object sender, ErrorEventArgs e)
        {
            _tailActor.Tell(new TailActor.FileError(_fileNameOnly,
                e.GetException().Message),
                ActorRefs.NoSender);
        }

        /// <summary>
        /// Callback for <see cref="FileSystemWatcher"/> file change events.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void OnFileChanged(object sender, FileSystemEventArgs e)
        {
            if (e.ChangeType == WatcherChangeTypes.Changed)
            {
                // here we use a special ActorRefs.NoSender
                // since this event can happen many times,
                // this is a little microoptimization
                _tailActor.Tell(new TailActor.FileWrite(e.Name), ActorRefs.NoSender);
            }
        }
    }
}

3단계: SupervisorStrategy

드디어 당신의 새로운 부모 TailCoordinatorActor에 감독 전략을 추가할 때입니다.

기본 SupervisorStrategy은 One-For-One에 Restart directive입니다.

TailCoordinatorActor의 밑에 다음의 코드를 추가해주세요:

// TailCoordinatorActor.cs
protected override SupervisorStrategy SupervisorStrategy()
{
    return new OneForOneStrategy (
        10, // maxNumberOfRetries
        TimeSpan.FromSeconds(30), // withinTimeRange
        x => // localOnlyDecider
        {
            //Maybe we consider ArithmeticException to not be application critical
            //so we just ignore the error and keep going.
            if (x is ArithmeticException) return Directive.Resume;

            //Error that we cannot recover from, stop the failing actor
            else if (x is NotSupportedException) return Directive.Stop;

            //In all other cases, just restart the failing actor
            else return Directive.Restart;
        });
}

4단계: Build and Run!

드디어 동작을 볼 시간입니다!

Tail 동작을 수행할 텍스트 파일을 준비합니다

우리는 이것과 같은 로그 파일을 추천합니다, 하지만 당신은 당신이 원하는 아무 텍스트 파일을 사용해도 괜찮습니다.

텍스트 파일을 스크린 한쪽에 열어주세요.

동작

화면에 나타나는 것을 확인해주세요

애플리케이션을 실행하면 콘솔 윈도우에 당신의 로그파일의 내용이 나타나야 합니다. 우리가 제공하는 로그 파일을 사용하는 경우 화면은 다음과 같아야 합니다:

screenshot1

콘솔과 파일을 둘 다 열어둔 채로…

텍스트를 추가하고 tail이 잘 작동하는지 확인합니다! 텍스트 몇 줄을 추가하고, 저장합니다. 그리고 tail이 동작하는지 봅니다!

다음과 같이 보일겁니다: screenshot2

축하합니다! 해냈습니다!

모든 과정이 끝났을 경우,

작성한 코드와 Complated의 코드를 비교하며 샘플에 어떤 것이 추가 및 수정되었는지 확인해봅시다.

수고하셨습니다! 레슨5 차례입니다.

수고하셨습니다! 레슨4을 무사히 끝냈습니다. 레슨5를 향해 나아가봅시다.

Lesson 5 - Looking up Actors by Address with ActorSelection.

Reference