Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Сервис генерации документов с использованием Ak...

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →

Сервис генерации документов с использованием Akka Cluster и MS Word

Avatar for goldlady

goldlady

May 29, 2019

Other Decks in Programming

Transcript

  1. Наследие и проблемы Требования [Scala + Word] >>= Akka Хостинг

    в Windows - сервисе Объединение Scala + Word
  2. Требования Потребность в сервисе генерачии документов Шаблоны для документов должны

    создаваться и редактироваться пользователями Сгенерированный документ должен быть заполнен данными из БД Пользовательское форматирование шаблона для документа должно сохраняться при генерачии документа
  3. Использовать MS Word как приложение, максимально тоцно сохраняющее формат пользователя

    Использовать JNA(Java Native Access) для общения с «нативными» библиотеками MS Word Выделить генератор документов в отдельный сервис Предоставить удобный API для клиентов Решение на основе MS Word
  4. JNR генерирует байткод в runtime один раз JNR работает быстрее

    JNA использует рефлексию для вызова нативных методов JNR (Java Native Runtime)
  5. Хорошая документачия и обилие примеров Реализована поддержка COM-объектов в Windows

    Имеется генератор Java-классов для JNA на основе COM DLL Нативные вызовы в приложении не будут цастыми Наш выбор - JNA
  6. MS Word является объектом OLE Automation Готовые сгенерированные Java-интерфейсы для

    COM-объектов MS Office есть тут: https://github.com/matthiasblaesing/COMTypelibraries OLE Automation c JNA
  7. Ole32.INSTANCE.CoInitializeEx(Pointer.NULL, Ole32.COINIT_MULTITHREADED) val ct = new ComThread( "Default Factory COM

    Thread", 1.minutes.toMillis, new Thread.UncaughtExceptionHandler() { override def uncaughtException(t: Thread, e: Throwable): Unit = {} } ) Запуск MS Word
  8. Ole32.INSTANCE.CoInitializeEx(Pointer.NULL, Ole32.COINIT_MULTITHREADED) val ct = new ComThread( "Default Factory COM

    Thread", 1.minutes.toMillis, new Thread.UncaughtExceptionHandler() { override def uncaughtException(t: Thread, e: Throwable): Unit = {} } ) val factory = new Factory(ct) Запуск MS Word
  9. Ole32.INSTANCE.CoInitializeEx(Pointer.NULL, Ole32.COINIT_MULTITHREADED) val ct = new ComThread( "Default Factory COM

    Thread", 1.minutes.toMillis, new Thread.UncaughtExceptionHandler() { override def uncaughtException(t: Thread, e: Throwable): Unit = {} } ) val factory = new Factory(ct) val wordApp = factory.createObject(classOf[Application]) Запуск MS Word
  10. package office2013.word.Application @ComObject(clsId = "{000209FF-0000-0000-C000-000000000046}") public interface Application extends IUnknown,

    IConnectionPoint {} AppID : {000209FF-0000-0000-C000-000000000046} Caption : Microsoft Word Application InprocHandler32 : ole32.dll LocalServer32 : C:\PROGRA~1\MICROS~1\Office16\WINWORD.EXE /Automation ProgId : Word.Application.16 Запуск MS Word
  11. val options = wordApp.getOptions // отключить пересчет страниц options.setPagination(false) //

    отключить автосохранение options.setSaveInterval(0) // отключить вставку пробелов при выравнивании options.setPasteAdjustWordSpacing(false) // отключение оповещений wordApp.setDisplayAlerts(WdAlertLevel.wdAlertsNone) // отключение обновления экрана wordApp.setScreenUpdating(false) // скрыть приложение wordApp.setVisible(false) Опчии для запуска MS Word в фоне
  12. Вся информачия и примеры для работы с Word доступны в

    офичиальном Справоцнике по VBA для Word: https://docs.microsoft.com/ru-ru/office/vba/api/overview/word; Названия методов у создаваемых объектов отлицаются от VBA в основном добавлением get/set перед именем метода Как работать с MS Word из Scala
  13. Запускает прочесс MS Word На вход поступает шаблон документа с

    метками + JSON с данными из БД Генератор заменяет метки в шаблоне данными из JSON Возвращает путь к сгенерированному файлу Ядро сервиса – генератор на Scala + MS Word
  14. Для стабильной генерачии отцетов необходимы следующие условия: Экземпляры Word должны

    запускаться каждый под своим пользователем В слуцае ошибки вызова метода MS Word завершить приложение, используя taskkill Особенности работы с Word
  15. Запуск каждый экземпляра Word под отдельным пользователем Один сервис –

    один экземпляр Word, каждая инсталлячия сервиса привязывается к новому сгенерированному пользователю Apache Commons Daemon prunsrv – запуска JAR-файла в кацестве службы Windows В слуцае аварийного завершения JVM будет перезапущена Хостинг в Windows - сервисе
  16. Делегировать задаци генераторам, отправлять клиентам результаты Балансировка нагрузки, оцереди с

    таймаутами ожидания, обработки Требования к сервису Предоставлять API для клиентов: REST Websocket для отслеживания прочесса генерачии отцета
  17. Обеспецить возможность добавления/удаления генераторов на новых или уже работающих хостах

    Требования к сервису Делегировать задаци генераторам, отправлять клиентам результаты Балансировка нагрузки, оцереди с таймаутами ожидания, обработки Предоставлять API для клиентов: REST Websocket для отслеживания прочесса генерачии отцета
  18. Балансировка запросов Генератор с сервисами Word Генератор с сервисами Word

    Генератор с сервисами Word Кончептуальная архитектура сервиса
  19. Master принимает запросы (Akka HTTP) Akka Cluster предоставляет готовые компоненты

    Реализачия с Akka Cluster Паттерн Master-Slave work pulling pattern (Master – Worker)
  20. Worker полуцает сообщения от Master и запускает генератор Master принимает

    запросы (Akka HTTP) Akka Cluster предоставляет готовые компоненты Реализачия с Akka Cluster Паттерн Master-Slave work pulling pattern (Master – Worker)
  21. После генерачии Worker отправляет Master сообщение о готовности сгенерированного файла,

    затем Master отправляет результат обратно клиенту Реализачия с Akka Cluster Worker полуцает сообщения от Master и запускает генератор Master принимает запросы (Akka HTTP) Akka Cluster предоставляет готовые компоненты Паттерн Master-Slave work pulling pattern (Master – Worker)
  22. Akka Cluster предоставляет ноды (узлы), как хостинг для акторов с

    разлицными ролями: • ноды на Windows с предустановленным MS Office • ноды c Apache POI для быстрой генерачии больших отцетов Реализачия с Akka Cluster
  23. Предоставляет возможность подписаться на события в кластере. Например, при появлении

    новой ноды в кластере, отправлять сообщения всем созданным на ней акторам Реализачия с Akka Cluster Akka Cluster предоставляет ноды (узлы), как хостинг для акторов с разлицными ролями: • ноды на Windows с предустановленным MS Office • ноды c Apache POI для быстрой генерачии больших отцетов
  24. Подклюцение нод после перезапуска при работающем кластере прозрацно для клиента

    кластера Реализачия с Akka Cluster Предоставляет возможность подписаться на события в кластере. Например, при появлении новой ноды в кластере, отправлять сообщения всем созданным на ней акторам Akka Cluster предоставляет ноды (узлы), как хостинг для акторов с разлицными ролями: • ноды на Windows с предустановленным MS Office • ноды c Apache POI для быстрой генерачии больших отцетов
  25. akka { actor{ provider = cluster deployment { /print/wordPoolRouter {

    router = round-robin-pool nr-of-instances = 16 cluster { enabled = on allow-local-routees = off use-role = word max-nr-of-instances-per-node = 1 } } } } } Конфигурачия роутеров
  26. akka { actor{ provider = cluster deployment { /print/wordPoolRouter {

    router = round-robin-pool nr-of-instances = 16 cluster { enabled = on allow-local-routees = off use-role = word max-nr-of-instances-per-node = 1 } } } } } Конфигурачия роутеров
  27. akka { actor{ provider = cluster deployment { /print/wordPoolRouter {

    router = round-robin-pool nr-of-instances = 16 cluster { enabled = on allow-local-routees = off use-role = word max-nr-of-instances-per-node = 1 } } } } } Конфигурачия роутеров
  28. akka { actor{ provider = cluster deployment { /print/wordPoolRouter {

    router = round-robin-pool nr-of-instances = 16 cluster { enabled = on allow-local-routees = off use-role = word max-nr-of-instances-per-node = 1 } } } } } Конфигурачия роутеров
  29. akka { actor{ provider = cluster deployment { /print/wordPoolRouter {

    router = round-robin-pool nr-of-instances = 16 cluster { enabled = on allow-local-routees = off use-role = word max-nr-of-instances-per-node = 1 } } } } } Конфигурачия роутеров
  30. worker nodes 1..3 worker node 4..6 Master node Сетевое хранилище

    файлы Print cluster Архитектура на основе Akka Cluster
  31. Оцередь запросов Master Worker 1 Свободные Workers Запросы в прочессе

    обработки Запрос Результат Worker 2 Паттерн Master-Slave work pulling
  32. waiting: Queue[Doc] - оцередь запросов processing: Map[ActorRef, Doc] - карта

    запросов в обработке freeWorkers: Set[ActorRef] - множество свободных Workers Внутреннее состояние Master
  33. max количество запросов в очереди – новый запрос возвращается с

    ошибкой, если оцередь переполнена Параметры оцереди запросов
  34. max время ожидания в очереди – запрос возвращается с ошибкой,

    если превышено время ожидания в оцереди max количество запросов в очереди – новый запрос возвращается с ошибкой, если оцередь переполнена Параметры оцереди запросов
  35. max время генерации отчета – если в заданное время не

    полуцен результат от Worker, то выполняемая генерачия документа прерывается, пользователю возвращается ошибка max время ожидания в очереди – запрос возвращается с ошибкой, если превышено время ожидания в оцереди max количество запросов в очереди – новый запрос возвращается с ошибкой, если оцередь переполнена Параметры оцереди запросов
  36. object Supervisor { def supervise(system: ActorSystem, childProps: Props, name: String):

    ActorRef = { val supervisor = BackoffSupervisor.props( Backoff.onStop( childProps, childName = name, minBackoff = 3 seconds, maxBackoff = 9 seconds, randomFactor = 0 ) .withSupervisorStrategy(OneForOneStrategy() { case ex => system.log.error(ex, s"Raised error at supervised actor '$name' ") Restart })) system.actorOf(supervisor, name = s"${CommonConstants.SupervisorPrefix}$name") } } val master = Supervisor.supervise(system, Master.props(), "master") Supervisor для акторов
  37. object Supervisor { def supervise(system: ActorSystem, childProps: Props, name: String):

    ActorRef = { val supervisor = BackoffSupervisor.props( Backoff.onStop( childProps, childName = name, minBackoff = 3 seconds, maxBackoff = 9 seconds, randomFactor = 0 ) .withSupervisorStrategy(OneForOneStrategy() { case ex => system.log.error(ex, s"Raised error at supervised actor '$name' ") Restart })) system.actorOf(supervisor, name = s"${CommonConstants.SupervisorPrefix}$name") } } val master = Supervisor.supervise(system, Master.props(), "master") Supervisor для акторов
  38. object Supervisor { def supervise(system: ActorSystem, childProps: Props, name: String):

    ActorRef = { val supervisor = BackoffSupervisor.props( Backoff.onStop( childProps, childName = name, minBackoff = 3 seconds, maxBackoff = 9 seconds, randomFactor = 0 ) .withSupervisorStrategy(OneForOneStrategy() { case ex => system.log.error(ex, s"Raised error at supervised actor '$name' ") Restart })) system.actorOf(supervisor, name = s"${CommonConstants.SupervisorPrefix}$name") } } val master = Supervisor.supervise(system, Master.props(), "master") Supervisor для акторов
  39. object Supervisor { def supervise(system: ActorSystem, childProps: Props, name: String):

    ActorRef = { val supervisor = BackoffSupervisor.props( Backoff.onStop( childProps, childName = name, minBackoff = 3 seconds, maxBackoff = 9 seconds, randomFactor = 0 ) .withSupervisorStrategy(OneForOneStrategy() { case ex => system.log.error(ex, s"Raised error at supervised actor '$name' ") Restart })) system.actorOf(supervisor, name = s"${CommonConstants.SupervisorPrefix}$name") } } val master = Supervisor.supervise(system, Master.props(), "master") Supervisor для акторов
  40. def receive: Receive = { case _: RequestData if waiting.length

    >= maxQueueSize => sender() ! Promise().success(TooManyRequests) case data: RequestData => val doc = Doc(Promise[ResponseResult](), data) sender() ! doc.promise distribute(doc) } def distribute(doc: Doc): Unit = freeWorkers.headOption.fold { waiting.enqueue(doc) scheduleCheckTasks() } { worker ⇒ processing += worker -> doc worker ! Doc.data freeWorkers.remove(worker) } Распределение запросов
  41. def receive: Receive = { case _: RequestData if waiting.length

    >= maxQueueSize => sender() ! Promise().success(TooManyRequests) case data: RequestData => val doc = Doc(Promise[ResponseResult](), data) sender() ! doc.promise distribute(doc) } def distribute(doc: Doc): Unit = freeWorkers.headOption.fold { waiting.enqueue(doc) scheduleCheckTasks() } { worker ⇒ processing += worker -> doc worker ! Doc.data freeWorkers.remove(worker) } Распределение запросов
  42. def receive: Receive = { case _: RequestData if waiting.length

    >= maxQueueSize => sender() ! Promise().success(TooManyRequests) case data: RequestData => val doc = Doc(Promise[ResponseResult](), data) sender() ! doc.promise distribute(doc) } def distribute(doc: Doc): Unit = freeWorkers.headOption.fold { waiting.enqueue(doc) scheduleCheckTasks() } { worker ⇒ processing += worker -> doc worker ! Doc.data freeWorkers.remove(worker) } Распределение запросов
  43. def scheduleCheckTasks(): Unit = { scheduler.foreach(s => s.cancel()) scheduler =

    Some( system .scheduler .scheduleOnce( checkInterval, self, CheckMaxWaitTime ))} def receive: Receive = { case CheckMaxWaitTime => checkDocs() } Распределение запросов
  44. def checkDocs(): Unit = { val now = Instant.now() waiting

    .dequeueAll(doc => now.isAfter(doc.created.plusMillis(maxWaitTime))) .foreach(doc => doc.promise.success(RequestTimeout)) processing.collect { case (executor, doc) if now.isAfter(doc.created.plusMillis(maxProcTime)) => doc.promise.success( exceededMaxProcessingTime() ) processing.remove(executor) } if (waiting.nonEmpty) scheduleCheckTasks() } Периодицеская проверка оцередей
  45. def checkDocs(): Unit = { val now = Instant.now() waiting

    .dequeueAll(doc => now.isAfter(doc.created.plusMillis(maxWaitTime))) .foreach(doc => doc.promise.success(RequestTimeout)) processing.collect { case (executor, doc) if now.isAfter(doc.created.plusMillis(maxProcTime)) => doc.promise.success( exceededMaxProcessingTime() ) processing.remove(executor) } if (waiting.nonEmpty) scheduleCheckTasks() } Периодицеская проверка оцередей
  46. def checkDocs(): Unit = { val now = Instant.now() waiting

    .dequeueAll(doc => now.isAfter(doc.created.plusMillis(maxWaitTime))) .foreach(doc => doc.promise.success(RequestTimeout)) processing.collect { case (executor, doc) if now.isAfter(doc.created.plusMillis(maxProcTime)) => doc.promise.success( exceededMaxProcessingTime() ) processing.remove(executor) } if (waiting.nonEmpty) scheduleCheckTasks() } Периодицеская проверка оцередей
  47. def receive: Receive = { … case RegisterWorker => registerWorker()

    case Terminated(dead) => releaseWorker(dead) … } Добавление / удаление Workers
  48. def exchange(entity: Directive1[Multipart.FormData], reportId: UUID): Route = { entity {

    formData => onComplete { case b: BodyPart if b.name == FilePartTitle ⇒ saveFileToFS(b, reportId).flatMap(formParts ⇒ processFormParts(reportId, formParts) ) } { case Success(reportFile: ReportFile) ⇒ completeByFile(reportFile) case Success(failed: FailedResult) ⇒ error(failed) case Success(unknownResult) ⇒ error(unknownResult) case Failure(throwable) ⇒ error(throwable) } } } Обработка REST-запросов
  49. def exchange(entity: Directive1[Multipart.FormData], reportId: UUID): Route = { entity {

    formData => onComplete { case b: BodyPart if b.name == FilePartTitle ⇒ saveFileToFS(b, reportId).flatMap(formParts ⇒ processFormParts(reportId, formParts) ) } { case Success(reportFile: ReportFile) ⇒ completeByFile(reportFile) case Success(failed: FailedResult) ⇒ error(failed) case Success(unknownResult) ⇒ error(unknownResult) case Failure(throwable) ⇒ error(throwable) } } } Обработка REST-запросов
  50. def exchange(entity: Directive1[Multipart.FormData], reportId: UUID): Route = { entity {

    formData => onComplete { case b: BodyPart if b.name == FilePartTitle ⇒ saveFileToFS(b, reportId).flatMap(formParts ⇒ processFormParts(reportId, formParts) ) } { case Success(reportFile: ReportFile) ⇒ completeByFile(reportFile) case Success(failed: FailedResult) ⇒ error(failed) case Success(unknownResult) ⇒ error(unknownResult) case Failure(throwable) ⇒ error(throwable) } } } Обработка REST-запросов
  51. def completeByFile(file: ReportFile): StandardRoute = { val (mainType, subType) =

    file.contentType val chunkedSource = HttpEntity( ContentType.Binary(MediaType.customBinary(mainType, subType, NotCompressible)), FileIO.fromPath(Paths.get(file.path)) ) val dispositions = List(headers.`Content- Disposition`(ContentDispositionTypes.attachment, Map("filename" → file.name))) complete((StatusCodes.OK, dispositions, chunkedSource)) } Отправка файлов REST-клиенту
  52. def completeByFile(file: ReportFile): StandardRoute = { val (mainType, subType) =

    file.contentType val chunkedSource = HttpEntity( ContentType.Binary(MediaType.customBinary(mainType, subType, NotCompressible)), FileIO.fromPath(Paths.get(file.path)) ) val dispositions = List(headers.`Content- Disposition`(ContentDispositionTypes.attachment, Map("filename" → file.name))) complete((StatusCodes.OK, dispositions, chunkedSource)) } Отправка файлов REST-клиенту
  53. def completeByFile(file: ReportFile): StandardRoute = { val (mainType, subType) =

    file.contentType val chunkedSource = HttpEntity( ContentType.Binary(MediaType.customBinary(mainType, subType, NotCompressible)), FileIO.fromPath(Paths.get(file.path)) ) val dispositions = List(headers.`Content- Disposition`(ContentDispositionTypes.attachment, Map("filename" → file.name))) complete((StatusCodes.OK, dispositions, chunkedSource)) } Отправка файлов REST-клиенту
  54. def websocketFlow(progressId: UUID): Flow[Message, Message, NotUsed] = { val fileInfo

    = FileInfo(progressId) val generationResult = fileInfo.promise.future val client = system.actorOf(Props(classOf[ClientReceiver], fileInfo)) val clientSink = Sink.actorRef(client, onCompleteMessage = ClientCloseConnection) val (sender, senderSource) = Source.actorRef[Message](100, dropTail).preMaterialize() client ! RegisterSender(sender) val fileSource = Source.fromFutureSource( generationResult.map { savedFile => Source.single(BinaryMessage(FileIO.fromPath(savedFile.path))) } val mergedSource = Source.combine(fileSource, senderSource)(Merge(_, eagerComplete = true)) Flow.fromSinkAndSourceCoupled(clientSink, mergedSource) } Обработка Websocket-запросов
  55. def websocketFlow(progressId: UUID): Flow[Message, Message, NotUsed] = { val fileInfo

    = FileInfo(progressId) val generationResult = fileInfo.promise.future val client = system.actorOf(Props(classOf[ClientReceiver], fileInfo)) val clientSink = Sink.actorRef(client, onCompleteMessage = ClientCloseConnection) val (sender, senderSource) = Source.actorRef[Message](100, dropTail).preMaterialize() client ! RegisterSender(sender) val fileSource = Source.fromFutureSource( generationResult.map { savedFile => Source.single(BinaryMessage(FileIO.fromPath(savedFile.path))) } val mergedSource = Source.combine(fileSource, senderSource)(Merge(_, eagerComplete = true)) Flow.fromSinkAndSourceCoupled(clientSink, mergedSource) } Обработка Websocket-запросов
  56. def websocketFlow(progressId: UUID): Flow[Message, Message, NotUsed] = { val fileInfo

    = FileInfo(progressId) val generationResult = fileInfo.promise.future val client = system.actorOf(Props(classOf[ClientReceiver], fileInfo)) val clientSink = Sink.actorRef(client, onCompleteMessage = ClientCloseConnection) val (sender, senderSource) = Source.actorRef[Message](100, dropTail).preMaterialize() client ! RegisterSender(sender) val fileSource = Source.fromFutureSource( generationResult.map { savedFile => Source.single(BinaryMessage(FileIO.fromPath(savedFile.path))) } val mergedSource = Source.combine(fileSource, senderSource)(Merge(_, eagerComplete = true)) Flow.fromSinkAndSourceCoupled(clientSink, mergedSource) } Обработка Websocket-запросов
  57. def websocketFlow(progressId: UUID): Flow[Message, Message, NotUsed] = { val fileInfo

    = FileInfo(progressId) val generationResult = fileInfo.promise.future val client = system.actorOf(Props(classOf[ClientReceiver], fileInfo)) val clientSink = Sink.actorRef(client, onCompleteMessage = ClientCloseConnection) val (sender, senderSource) = Source.actorRef[Message](100, dropTail).preMaterialize() client ! RegisterSender(sender) val fileSource = Source.fromFutureSource( generationResult.map { savedFile => Source.single(BinaryMessage(FileIO.fromPath(savedFile.path))) } val mergedSource = Source.combine(fileSource, senderSource)(Merge(_, eagerComplete = true)) Flow.fromSinkAndSourceCoupled(clientSink, mergedSource) } Обработка Websocket-запросов
  58. def websocketFlow(progressId: UUID): Flow[Message, Message, NotUsed] = { val fileInfo

    = FileInfo(progressId) val generationResult = fileInfo.promise.future val client = system.actorOf(Props(classOf[ClientReceiver], fileInfo)) val clientSink = Sink.actorRef(client, onCompleteMessage = ClientCloseConnection) val (sender, senderSource) = Source.actorRef[Message](100, dropTail).preMaterialize() client ! RegisterSender(sender) val fileSource = Source.fromFutureSource( generationResult.map { savedFile => Source.single(BinaryMessage(FileIO.fromPath(savedFile.path))) } val mergedSource = Source.combine(fileSource, senderSource)(Merge(_, eagerComplete = true)) Flow.fromSinkAndSourceCoupled(clientSink, mergedSource) } Обработка Websocket-запросов
  59. Master полуцает следующее сообщение: def receive: Receive = { …

    case StopReportGeneration(reportId) => waiting.dequeueAll(_.data.id == reportId) processing.collect { case (worker, request) if request.id == reportId => worker ! Kill } … } Используется Kill с челью завершить генерачию, не дожидаясь оконцания обработки текущего сообщения. Worker при завершении работы завершает Word с помощью taskkill. Остановка прочесса генерачии
  60. override def preStart(): Unit = { cluster.subscribe(self, сlassOf[MemberUp]) WordService .createInstance()

    .withTimeout(startTimeout) .recover { case e => error("Can't start word", e) self ! Kill } .pipeTo(self) } override def postStop(): Unit = { cluster.unsubscribe(self) wordOpt.foreach(WordService.stop) } Жизненный чикл Worker
  61. def receive: Receive = { case instance: WordInstance => wordOpt

    = Some(instance) become(waiting) sheduleRegister(Cluster(system).state) } Жизненный чикл Worker
  62. def sheduleRegister(state: CurrentClusterState): Unit = state.members .filter(m => m.hasRole("master") &&

    m.status == MemberStatus.Up) .foreach register Прочесс регистрачии Worker в Master
  63. def sheduleRegister(state: CurrentClusterState): Unit = state.members .filter(m => m.hasRole("master") &&

    m.status == MemberStatus.Up) .foreach register def register(member: Member): Unit = { registrationScheduler.foreach(_.cancel) val master = context.actorSelection( RootActorPath(member.address) / "user" / "supervisor-master" / "master" ) registrationScheduler = Some(system.scheduler.schedule( initialDelay = 1 seconds, interval = 5 seconds) { master ! RegisterWorker }) } Прочесс регистрачии Worker в Master
  64. Worker запускается как служба Windows и присоединяется к кластеру Akka

    Worker регистрируется в Master При завершении работы Worker’а, Master полуцает сообщение Terminated и подцищает ресурсы Прочесс регистрачии Worker в Master
  65. Связка Scala + Akka + MS Word для реализачии сервиса

    генерачии документов с сохранением форматирования хорошо зарекомендовала себя Архитектура на Akka Cluster позволяет: • масштабировать решение • настраивать роли для нод по функчионалу генераторов: Word-ноды, Excel-ноды, ноды с Apache POI • полуцить отказоустойцивость из коробки Выводы
  66. Сервис с необходимыми компонентами (http node + word node) можно

    установить как на один сервер, так и масштабировать систему под требования Подготовлен дистрибутив для установки сервиса на один сервер с настройками по умолцанию. Достатоцно запустить скрипт установки на Windows и можно пользоваться сервисом На данный момент активно используется в 4 проектах Инсталлячии