Compilable configuration of a distributed system

У цьому випадку ми можемо відзначити інтерес до розлучення з визначенням розповсюдженої системи.
Налаштування є репрезентованим безпосередньо в Scala language в type safe manner. An example implementation is described in details. Різні аспекти прохідних є розглянуті, включаючи influence on overall development process.

Compilable configuration of a distributed system

(російською)

Вступ

Building robust distributed systems requires the use of correct and coherent configuration on all nodes. Типовим розв'язанням є використання текстуального розгортання опису (terraform, неможливий або деякий суперечність) і автоматично створеного налаштування файлів (often — dedicated for each node/role). Вони можуть бути використані для тих самих протоколів для тих самих версій на всіх комунікаційних повідомлень (інші, як правило, існують у спільній якості). В JVM world this means that at least the messaging library should be the same version on all communicating nodes.

What about testing the system? Окрім курсу, ми повинні мати спільні тести для всіх компонентів перед тим, як integration tests. Щоб бути можливим для extrapolate test results on runtime, we should make sure that the versions of all libraries are kept identical in both runtime and testing environments.

Коли керує введенням тестів, це позначається дуже простий, щоб мати саму категорію path on all nodes. Ви повинні скористатися тим, що саме класи path is used on deployment. (Це можливо для використання різних класів на різних nodes, але це більше різниця в тому, що ця configuration і коректно розробляє це.) У відповідності до поглинання цих простих буде тільки розглядати однакові класи на всіх nodes.

Configuration tends to evolve together з software. We usually use versions до identify various
stages of software evolution. Вона має відповідний спосіб, щоб повернути configuration під version management and identify different configurations with some labels. Якщо це тільки один параметр у виробництві, ми можемо використовувати єдину версію як ідентифікатор. Деякий час ми можемо мати велику продукцію навколишнього середовища. І для кожного навколишнього середовища ми потребуємо окремої літери configuration. З налаштуваннями можна лаборувати з парадком і версією до uniquely identify different configurations. Всі літери версії і версії відповідають єдиним поєднанням distributed nodes, ports, external resources, classpath library versions on each node. Тут можна тільки поєднати малу браму і identify configurations за трьома component decimal version (1.2.3), в той же час як інші матеріали.

У сучасних умовах налаштування файлів не змінюються автоматично. Typically we generate
config files at deployment time and never touch them післянагород. Яким чином я можу використовувати текст format for configuration files? Додаткові параметри є місцем призначення в межах compilation unit and benefit from compile-time configuration validation.

У цій статті ми будемо вивчати ідею керування налаштуваннями в складному матеріалі.

Compilable configuration

У цьому розділі ми будемо розраховувати на прикладі статевого налаштування. Two simple services — echo service and the client of the echo service are being configured and implemented. Існують два різних розподілені системи з сервісними послугами, які instantiated. Один є для одного node configuration і інший для двох nodes configuration.

A типові розповсюджені системи consists of a few nodes. The nodes could be identified using some type:

sealed trait NodeId
case object Backend extends NodeId
case object Frontend extends NodeId

або просто

case class NodeId(hostName: String)

або навіть

object Singleton
type NodeId = Singleton.type

Ці nodes виконують різні ролі, несуть деякі послуги і повинні бути здатні до комунікації з іншими nodes за допомогою TCP/HTTP connections.

Для TCP зв'язку в межах доданого номера необхідно. Ви також хотіли б зробити те, що клієнт і сервер є те, що той самий протокол. У ордері до моделі зв'язок між ліжками літа позначається на наступному класі:

case class TcpEndPoint[Protocol](node: NodeId, port: Port[Protocol])

де Port це просто Int within the allowed range:

type PortNumber = Refined[Int, Closed[_0, W.`65535`.T]]

Refined types

Читати рафінований library. У шорті, це дозволяє до звільнення часу часу позбавлятися інших типів. У цьому випадку Int is only allowed to have 16-bit values ​​that can represent port number. Це не потребує використання цієї library for this configuration approach. Це вже добре для fit дуже добре.

Для HTTP (REST) ​​ми повинні також потрібні шлях служби:

type UrlPathPrefix = Refined[String, MatchesRegex[W.`"[a-zA-Z_0-9/]*"`.T]]
case class PortWithPrefix[Protocol](portNumber: PortNumber, pathPrefix: UrlPathPrefix)

Phantom type

У відповідності до identify protocol під час compilation ми використовуємо Scala feature of declaring type argument Protocol that is not used in the class. It's a so called phantom type. У ході часу потрібна потреба в протоколі ідентифікатора, що це не буде. Під час compilation this phantom type gives additional type safety. Неможна pass port with incorrect protocol.

Один з найбільш широко використовуваних протоколів є REST API with Json serialization:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

де RequestMessage is the base type of messages that client can send to server and ResponseMessage is the response message from server. З курсу, ми можемо створити інші protocol descriptions що specify the communication protocol with desired precision.

Для цілей цієї програми ми повинні використовувати виняткову версію протоколу:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

У цьому протоколі запиту message is appended to url and response message is returned as plain string.

Пристрій налаштування може бути описаний за допомогою служби name, колекції портів і деяких dependences. Там є кілька можливих способів, як показати всі ці елементи в Scala (for instance, HList, algebraic data types). Для пристосування цих повідомлень буде використовувати Cake Pattern і репрезентувати комбіновані pieces (modules) as traits. (Cake Pattern is not requirement for this compilable configuration approach. It just one possible implementation of the idea.)

Взаємини можуть бути представлені, використовуючи Cake Pattern як кінчики інших nodes:

  type EchoProtocol[A] = SimpleHttpGetRest[A, A]

  trait EchoConfig[A] extends ServiceConfig {
    def portNumber: PortNumber = 8081
    def echoPort: PortWithPrefix[EchoProtocol[A]] = PortWithPrefix[EchoProtocol[A]](portNumber, "echo")
    def echoService: HttpSimpleGetEndPoint[NodeId, EchoProtocol[A]] = providedSimpleService(echoPort)
  }

Echo Service тільки потребує port configured. And we declare що цей port supports echo protocol. Зверніть увагу, що ми не потрібні для особливих особливостей на цьому моменті, тому що trait's allows abstract methods declarations. Якщо ми використовуємо abstract methods, compiler буде потребувати implementation in a configuration instance. Here we have provided the implementation (8081) and it will be used as the default value if we skip it in a concrete configuration.

We can declare a dependency in the configuration of the echo service client:

  trait EchoClientConfig[A] {
    def testMessage: String = "test"
    def pollInterval: FiniteDuration
    def echoServiceDependency: HttpSimpleGetEndPoint[_, EchoProtocol[A]]
  }

Dependency has the same type as the echoService. У особливих випадках, це відповідають тому ж протоколу. Hence, ми можемо думати, що якщо вони підключаються до двох dependences, вони будуть працювати правильно.

Services implementation

Пристрій потребує функцій для запуску і чудово shutdown. (Якість для випробування сервісу є критичною для тестування.) У зв'язку з цим є кілька варіантів особливих таких функцій для забезпечення функціонування (для додатків, ми повинні використовувати типи classes). Для цього повідомлення буде використано Cake Pattern. We can represent a service using cats.Resource ,які можуть бути bracketing and resource release. В ордер для отримання ресурсу ми повинні здійснювати налаштування і деякий поточний контекст. Якщо ви працюєте з функцією might look like:

  type ResourceReader[F[_], Config, A] = Reader[Config, Resource[F, A]]

  trait ServiceImpl[F[_]] {
    type Config
    def resource(
      implicit
      resolver: AddressResolver[F],
      timer: Timer[F],
      contextShift: ContextShift[F],
      ec: ExecutionContext,
      applicative: Applicative[F]
    ): ResourceReader[F, Config, Unit]
  }

де

  • Config — type of configuration that is required by this service starter
  • AddressResolver — a runtime object that has the ability to obtain real address of other nodes (keep reading for details).

the other types comes from cats:

  • F[_] - effect type (In the simplest case F[A] could be just () => A. In this post we'll use cats.IO.)
  • Reader[A,B] — is more or less a synonym for a function A => B
  • cats.Resource — has ways to acquire and release
  • Timer — allows to sleep/measure time
  • ContextShift - analog of ExecutionContext
  • Applicative — wrapper of functions in effect (досить monad) (we might eventually replace it with something else)

За допомогою цього відношення ми можемо виконувати кілька послуг. Для того, щоб служити, що це не так:

  trait ZeroServiceImpl[F[_]] extends ServiceImpl[F] {
    type Config <: Any
    def resource(...): ResourceReader[F, Config, Unit] =
      Reader(_ => Resource.pure[F, Unit](()))
  }

(Див. Вихідний код for other services implementations echo service,
echo client та lifetime controllers.)

Node is a single object that runs a few services (starting a chain of resources is enabled by Cake Pattern):

object SingleNodeImpl extends ZeroServiceImpl[IO]
  with EchoServiceService
  with EchoClientService
  with FiniteDurationLifecycleServiceImpl
{
  type Config = EchoConfig[String] with EchoClientConfig[String] with FiniteDurationLifecycleConfig
}

Note that in the node we specify the exact type of configuration that is needed by this node. Компілятор не буде скористатись тим, що ви будувати об'єкт (Cake) з insufficient type, тому що їх сервісний trait розраховується на питання Config тип. Крім того, не може бути запущений дзвінок без виконання повної configuration.

Node address resolution

В ордер для встановлення connection буде потрібна реальна host address for each node. It might be known later than other parts of the configuration. Hence, нам потрібний спосіб отримати mapping між node id and it's actual address. Це mapping is a function:

case class NodeAddress[NodeId](host: Uri.Host)
trait AddressResolver[F[_]] {
  def resolve[NodeId](nodeId: NodeId): F[NodeAddress[NodeId]]
}

Там є кілька можливих способів до виконання такої функції.

  1. Якщо ви знайдете поточні адреси перед розробкою, протягом кількох hosts instantiation, то ми можемо генерувати стандартні коди з поточними адресами і керувати після завершення роботи (які реалізують швидкі терміни перевірки і беруть участь у integration test suite). У цьому випадку наша mapping функція є відома статичне і може бути simplified до деякихвідповідь як Map[NodeId, NodeAddress].
  2. Певні часи будуть отримувати поточні адреси тільки на останній момент, коли шрифт є фактично started, або ми не повинні писати адреси повідомлень, які повинні бути встановлені. У цьому випадку ми маємо відповідь на службу, яка ведеться до всіх інших повідомлень і всіх повідомлень, щоб advertise його адреса в цій службі і підписується на dependences.
  3. If we can modify /etc/hosts, we can use predefined host names (like my-project-main-node та echo-backend) and just asociation this name with ip address at deployment time.

У цьому випадку ми не можемо поширювати ці умови в багато деталей. In fact in our toy example all nodes will have the same IP address — 127.0.0.1.

У цьому дослідженні розглядаються два distributed system layouts:

  1. Single node layout, де всі послуги є placed on single node.
  2. Два node layout, де служба і клієнт є на різних nodes.

The configuration for a єдиний вузол layout is as follows:

Single node configuration

object SingleNodeConfig extends EchoConfig[String] 
  with EchoClientConfig[String] with FiniteDurationLifecycleConfig
{
  case object Singleton // identifier of the single node 
  // configuration of server
  type NodeId = Singleton.type
  def nodeId = Singleton

  /** Type safe service port specification. */
  override def portNumber: PortNumber = 8088

  // configuration of client

  /** We'll use the service provided by the same host. */
  def echoServiceDependency = echoService

  override def testMessage: UrlPathElement = "hello"

  def pollInterval: FiniteDuration = 1.second

  // lifecycle controller configuration
  def lifetime: FiniteDuration = 10500.milliseconds // additional 0.5 seconds so that there are 10 requests, not 9.
}

Тут ми створили спільне налаштування, що розширення з сервером і клієнтом configuration. Also we configure lifecycle controller that will normally terminate client and server after lifetime interval passes.

Те ж саме серія служб implementations і configurations може бути використана для створення системи з посиланням на дві окремі nodes. We just need to create two separate node configs with the appropriate services:

Two nodes configuration

  object NodeServerConfig extends EchoConfig[String] with SigTermLifecycleConfig
  {
    type NodeId = NodeIdImpl

    def nodeId = NodeServer

    override def portNumber: PortNumber = 8080
  }

  object NodeClientConfig extends EchoClientConfig[String] with FiniteDurationLifecycleConfig
  {
    // NB! dependency specification
    def echoServiceDependency = NodeServerConfig.echoService

    def pollInterval: FiniteDuration = 1.second

    def lifetime: FiniteDuration = 10500.milliseconds // additional 0.5 seconds so that there are 10 request, not 9.

    def testMessage: String = "dolly"
  }

See how we specify the dependency. We mention інших node's provided service як dependency of the current node. Type of dependency is checked because it contains phantom type that describes protocol. And at runtime we'll have the correct node id. Це один з важливих аспектів відповідних configuration approach. Це забезпечується з здатністю до набору порт тільки на одному і тому, що ви бачите відповідний порт.

Two nodes implementation

Для цього налаштування ми можемо використовувати ті ж самі послуги здійснення. No changes at all. However, we create XNUMX different node implementations that contain different set of services:

  object TwoJvmNodeServerImpl extends ZeroServiceImpl[IO] with EchoServiceService with SigIntLifecycleServiceImpl {
    type Config = EchoConfig[String] with SigTermLifecycleConfig
  }

  object TwoJvmNodeClientImpl extends ZeroServiceImpl[IO] with EchoClientService with FiniteDurationLifecycleServiceImpl {
    type Config = EchoClientConfig[String] with FiniteDurationLifecycleConfig
  }

Перший код implements server і його тільки потрібний сервер side config. У другій зоні implements client and needs another part of config. Both nodes require some lifetime specification. Для тих, хто придбає цей post service node will have infinite lifetime that could be terminated using SIGTERM, while echo client буде terminate after the configured finite duration. See the starter application for details.

Overall development process

Let's see how this approach changes the way we work with configuration.

Налаштування як коду буде складено і виробляється в artifact. Існує згодні на те, щоб відокремити налаштування artifact від інших code artifacts. Ось ми можемо множити параметри налаштувань на той самий код base. And of course, we can have multiple versions of various configuration branches. У налаштування ми можемо вибрати конкретні версії libraries and this will remain constant whenever we deploy this configuration.

A configuration change becomes code change. So it should be covered by the same quality assurance process:

Ticket -> PR -> review -> merge -> continuous integration -> continuous deployment

There are the following consequences of the approach:

  1. Налаштування є взаємним для особливої ​​системи встановлення. Це означає, що це не може бути неправильним з'єднанням між nodes.
  2. Це не дає змогу змінити параметри just in one node. Вона має нерозпізнаний, щоб log in and change some text files. So configuration drift becomes less possible.
  3. Невеликі параметри зміни не є приємними.
  4. Більшість configuration changes буде йти за тим самим розвитком процесів, і це буде pass some review.

Do we need a separate repository for production configuration? Виробництво configuration might contain sensitive information that we would like to keep out of reach of many people. Будь-який мій worth keeping separat repository with restricted access that will contain the production configuration. We may split the configuration into XNUMX parts — one that contains most open parameters of production and one that contains the secret part of configuration. Це може бути забезпечений доступом до провідних розробників до величезної ваги параметрів, коли обмеження доступу до реально чутливих думок. Це дуже зручно пристосувати це до використання intermediate traits with default parameter values.

Варіації

Let's see pros and cons of the proposed approach compared to thether configuration management techniques.

Перший, всі сторінки, кілька варіантів до різних аспектів пропонованого способу ділитися з configuration:

  1. Text file on the target machine.
  2. Centralized key-value storage etcd/zookeeper).
  3. Subprocess components that could be reconfigured/restarted without restarting process.
  4. Configuration outside artifact and version control.

Text file gives деяка flexibility в термінах ad-hoc fixes. Системний адміністратор може підписати до target node, зробити зміни і перезапустити службу. Це не може бути добре для великих систем. Чи не трасами є лівий під зміною. Зміна не reviewed by another pair of eyes. It might be difficult to find out what have caused the change. It has not been tested. Від distributed system's perspective an administrator може simple forget to update the configuration in one of the other nodes.

(Btw, якщо можливе, буде потрібно, щоб запустити, використовуючи текстові налаштування файлів, ми тільки повинні бути запрограмовані + + validator that could produce the same Config type and that would be enough to start using text configs. (Це також показує, що складність терміну налаштування є коротким смаллером, що складність тексту-основаних параметрів, тому що в текстовому базі версії буде потрібна деяка відповідна код.)

Centralized key-value storage is a good mechanism for distributing application meta parameters. Тут ми повинні думати про те, що ми знаємо, як визначити значення і які є just data. Given a function C => A => B we usually call rarely changing values C «configuration», while frequently changed data A - Just input data. Configuration should be provided to the функція earlier than the data A. Given this idea we can say that it's expected frequency of changes what could be used to distinguish configuration data from just data. Також ці дані зазвичай містяться з одного джерела (user) і configuration comes from a different source (admin). Dealing with parameters that can be changed after process initialization leads to an increase of application complexity. Для таких параметрів ми повинні керувати своїми діючими механізмами, пірсування і validation, handling incorrect values. Hence, в порядку до скорочення program complexity, ми повинні скоротити число параметрів, які можуть змінюватися в ході часу (або навіть eliminate them altogether).

З точки зору цього подання ми повинні відповідати між статевими і динамічними параметрами. Якщо сервіс логічних потреб раптово змінює деякі параметри в ході часу, то ми можемо зателефонувати ним динамічним параметрам. Іншівикористання вони є статичними і можуть бути configured using the proposed approach. Для динамічної регенерації інших пристроїв можуть бути необхідні. Для прикладу, частини системи мають бути відновлені з новими налаштуваннями параметрів в такій мірі, щоб відновити окремі процеси з distributed system.
(My humble opinion is to avoid runtime reconfiguration because it increases complexity of the system.
It' might be more straightforward to just rely on OS support of restarting processes. Though, it might not always be possible.)

Одним важливим аспектом використання static configuration є те, що деякі люди вважають динамічне налаштування (без інших конкретних умов) є служба впродовж тривалого часу налаштування update. Безумовно, якщо ми будемо змінювати зміни до статевого налаштування, ми повинні відновити систему з тим, що нові значення стають ефективними. Потрібні для нижчої варіації для різних систем, тому це не може бути критичним. Якщо це є критичним, вони повинні бути заплановані вперед для будь-якої системи перезапуску. For instance, we could implement AWS ELB connection draining. У цьому сенсорі, колинеобхідно для запуску системи, буде запуском нової системи системи в parallel, буде перемикати ELB до нього, при поводженні з попередньою системою до повного обслуговування існуючих з'єднань.

What o keeping configuration uvnitř versioned artifact or outside? Keeping configuration всередині artifact means в most of the cases that this configuration has passed the same quality assurance process as other artifacts. Так що один слід думати, що налаштування є хорошим якістю і тяжкістю. На відповідному налаштуванні в окремому файлі методи, які не мають доступу до тих, хто виконує зміни до цього файлу. Чи є це важливо? Будьте впевнені, що для найбільшої системи виробництва це буде бути стабільним і високою якістю configuration.

Version of artifact allows to find out when it was created, what values ​​it contains, what features are enabled/disabled, whes was responsible for making each change in the configuration. Це потрібний деякий ефект для керування налаштуванням всередині архітектури і це в якості дизайну, щоб зробити.

Плюси мінуси

Тут ми можемо зробити високу освіту кількох додатків і до розмови про якісь розповсюдження з відповідних прикладів.

Переваги

Характеристики складної configuration of complete distributed system:

  1. Static check of configuration. Це дає високий рівень confidence, що configuration is correct given type constraints.
  2. Rich language of configuration. Типово інші налаштування approaches є обмеженим до максимальної заміни.
    Використовуючи Scala, один може використовувати широке коло позначок, щоб зробити налаштування більше. Для того, щоб скористатися послугами, що дають змогу зменшити значення, об'єкти до set different scope, we can refer to vals defined only once в зовнішності (DRY). Це може бути використано літеральні sequences, або instances of certain classes (Seq, MapІ т.д.).
  3. DSL. Scala має повну підтримку для DSL writers. Один може використовувати ці особливості для встановлення configuration language, що є більш сприятливим і кінцевим користувачем, так, що final configuration is readable by domain users.
  4. Integrity and coherence across nodes. Один з benefits of configuration for whole distributed system in one place is that all values ​​are defined strictly once and then reused in all places where we need them. Також тип надійних портів повідомлень про те, що в усіх можливих коректних налаштуваннях системних повідомлень буде виявляти саму мову. Там є explicit dependencies між nodes, які роблять це hard to forget to provide some services.
  5. High quality of changes. Попередній спосіб проходження налаштувань зміни через звичайні PR процеси встановлюють найвищі стандарти якості і в налаштуваннях.
  6. Simultaneous configuration changes. Whenever we make any changes in configuration automatic deployment ensures that all nodes are being updated.
  7. Application simplification. Застосування необхідне для повторення і validate configuration and handle incorrect configuration values. Це загрожує повним застосуванням. (Якщо комплексність зростає в налаштуванні його, але це важлива торговельна сфера до сфери безпеки.) Це приємне право на повернення до послідовного налаштування — just add the missing pieces. Це назавжди, щоб отримати started with compiled configuration and postpone implementation of additional pieces to some later times.
  8. Versioned configuration. Здійснює факт, що налаштування змінюється за тим самим розвитком процесів, як результат може бути оформлений з унікальною версією. Це дозволяє використовувати параметри настройки back if needed. Ми можемо продовжити розробку, яка була використана протягом року і вона буде працювати виключно в той же час. Стабільне налаштування не дає змоги конфігурації та надійності distributed system. Налаштування fixed at compile time and cannot be easily tampered on a production system.
  9. Модульність. Запропоновані кадрові роботи є modular and modules could be combined in various ways to
    support different configurations (setups/layouts). У особливих випадках, це можливо, щоб мати невеликий штриховий денний node layout і великий кінець multi node setting. Це reasonable для того, щоб мати велику продукцію виконання.
  10. Тестування. Для випробування витрачається один простий здійснити міксерські послуги і використовувати його як dependence в type safe way. Більшість різних випробувань терміни з різними частинами, що переміщаються по мішках може бути maintained simultaneously.
  11. Integration testing. Певні часи в розподілених системах це неоднозначно до run integration tests. За допомогою описаних прикладів до типу надійного налаштування повного distributed system, ми можемо скористатися всіма розподіленими частинами на одному сервері в controllable way. It's easy to emulate the situation
    коли один з послуг стає неможливим.

Недоліки

Compiled configuration approach is different from "normal" configuration and it might not suit all needs. Тут ми знаємо, що є невід'ємними параметрами:

  1. Static configuration. Це не може бути прийнятним для всіх функцій. У деяких випадках вони потребують швидкого fixing configuration in production bypassing all safety measures. Це approach makes it more difficult. Compilation and redeployment є необхідним після виконання будь-якого зміни в configuration. Це є як feature and burden.
  2. Configuration generation. Коли config генерується за допомогою автоматичного інструменту, цей інструмент потребує subsequent compilation (який might in turn fail). Це важлива потреба додаткового ефекту до впровадження цього додаткового кроку в будову системи.
  3. Інструменти. Там є безліч інструментів, використовуючи саме те, що посилання на текстові бази configs. Один з них
    won't be applicable when configuration is compiled.
  4. A shift in mindset is needed. Розробники та DevOps є рідною мовою з текстовими параметрами файлів. The idea of ​​compiling configuration might appear strange to them.
  5. Після встановлення складного налаштування високої якості розвитку програмного забезпечення розробників необхідно.

Там є деякі обмеження з implemented example:

  1. Якщо ви надаєте ще один налаштування, що не відповідає вимогам до node implementation, комп'ютер не буде вказати на те, що не вдасться implementation. This could be addressed by using HList або ADTs (case classes) для node configuration instead of traits and Cake Pattern.
  2. We have to provide some boilerplate in config file: (package, import, object declarations;
    override def's для параметрів, що мають default values). Цей пункт може бути спочатку addressed using a DSL.
  3. У цьому положенні ми не можемо подолати динамічну перебудову clusters подібних nodes.

Висновок

У цьому питанні ми повинні розглянути ідею репрезентації налаштування безпосередньо в джерелі коду в типовому порядку. Програма може бути використана в багатьох програмах як заміна xml-і інших текстових configs. Неприпустимо, що в нашому дослідженні буде зроблено в Scala, це може бути переведено до інших комп'ютерних мов (як Kotlin, C#, Swift, etc.). Один може зробити це рішення в новому проекті і, в разі, що це не fit well, перемикання на old fashioned way.

З курсу, compilable configuration requires high quality development process. In return it promises to provide equally високої quality robust configuration.

Цей спосіб може бути розширений в різних випадках:

  1. Один може використовувати macros для ефективного визначення validation and fail at compile time in case of any business-logic constraints failures.
  2. A DSL може бути реалізована до відповідного налаштування в domain-user-friendly way.
  3. Dynamic resource management with automatic configuration adjustments. Для того, щоб помітити номер cluster nodes we might want (1) the nodes to obtain slightly modified configuration; (2) cluster manager to receive new nodes info.

Дякую

Я хотів, щоб отримати те, що ви Andrey Saksonov, Pavel Popov, Anton Nehaev для того, щоб віщати inspirational feedback на початку цього повідомлення, що help me make it clearer.

Джерело: habr.com