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

Александр Тарасов — Автоматизация экспериментов с помощью Kotlin DSL

Александр Тарасов — Автоматизация экспериментов с помощью Kotlin DSL

В большом проекте нельзя просто так взять и сделать фичу доступной всем клиентам. Чтобы изменения применялись постепенно, плавно и без риска для жизни, мы проводим эксперименты, в результате которых фича может быть открыта на клиентов, заморожена для дополнительной оптимизации или прекратить своё существование в проекте.

Процесс проведения эксперимента состоит из нескольких этапов, в рамках которых нужно выполнить некоторое количество работы вручную, что ведёт к издержкам по времени и, порой, выполнению ошибочных действий. Чтобы убрать эти факторы мы решили автоматизировать подготовку и запуск экспериментов.

В докладе автор расскажет:
- почему был выбран Котлин, а не классические инструменты управления конфигурацией как Ansible;
- почему хороший DSL и инструментарий критически важен для этой задачи;
- какие проблемы пришлось преодолеть, чтобы всё заработало, как изначально задумано.

3fc5b5eb32bd3b48d7810fd67b37f9a1?s=128

Moscow JUG

April 05, 2018
Tweet

Transcript

  1. Автоматизация экспериментов с Kotlin DSL #jugmsk edition

  2. 2 Кто я?

  3. Мнение докладчика может не совпадать с официальной позицией его работодателя,

    начальника, коллег или других специалистов. 
 Все представленные в докладе сведения, примеры, выводы и другую информацию вы можете использовать на свой страх и риск. За все ваши действия ответственность несёте только вы сами. Этот доклад не совсем про Kotlin! 3 Минздрав предупреждает
  4. -1| Пара вопросов залу

  5. 0| Вместо предисловия

  6. 6 Что мы (программисты) любим? Java

  7. 7 Что есть на самом деле? ОколоJava Java

  8. 1| Что такое эксперимент?

  9. 9 71 млн Посещают нас в месяц 8000 Серверов Одноклассники

    в цифрах >10 тыс Выполненных экспериментов
  10. 10 Очень простой пайплайн пишем код QA тесты, тесты, тесты

    Deployment накатываем Development
  11. 11 Обложимся тестами! пишем код QA тесты, тесты, тесты Deployment

    накатываем Development
  12. 12 Очень простая задача avatarSrc = images.get(id, JPEG)

  13. 13 JPEG уже не торт avatarSrc = images.get(id, JPEG)

  14. 14 WebP наше всё avatarSrc = images.get(id, WebP)

  15. 15 Немного же изменений, да? avatarSrc = images.get(id, WebP) avatarSrc

    = images.get(id, JPEG) //git diff
  16. 16 Поехали! Tests Deploy Commit -> Build

  17. 17 Поехали! Deploy Tests Commit -> Build

  18. 18 Здесь должны быть счастливые пользователи Deploy Tests Commit ->

    Build
  19. • Аватарок в WebP нет • в браузере пользователей •

    в кэшах CDN-а • во внутренних кэшах 19 Нет, потому что
  20. • Аватарок в WebP нет • в браузере пользователей •

    в кэшах CDN-а • во внутренних кэшах • Нужно потратить • трафик на скачивание • ресурсы CPU и памяти для нарезки 20 Нет, потому что
  21. 21 Нельзя просто так взять и выкатить фичу

  22. 22 Есть ли жизнь после деплоймента? Deploy ? Pre-Deploy разработка

    внедрение
  23. 23 Место экспериментов в пайплайне Deploy Post-Deploy Pre-Deploy разработка внедрение

    эксперименты
  24. • Затем, что есть риски • Технические риски • Продуктовые

    риски 24 Зачем нужны эксперименты?
  25. • Затем, что есть риски • Технические риски • Продуктовые

    риски • Запуск новых фич происходит поэтапно • Наполнение кэшей • А/B/…/Z-тестирование 25 Зачем нужны эксперименты?
  26. • Затем, что есть риски • Технические риски • Продуктовые

    риски • Запуск новых фич происходит поэтапно • Наполнение кэшей • А/B/…/Z-тестирование • Процесс запуска фичи называется экспериментом 26 Зачем нужны эксперименты?
  27. 27 Эксперимент в деталях Code Branches if / switch

  28. 28 Эксперимент в деталях Code Branches if / switch Config

    by partition
  29. 29 Эксперимент в деталях Code Branches if / switch Config

    by partition by server
  30. 30 Эксперимент в деталях Code Branches if / switch Config

    by partition by server by geo
  31. 31 Эксперимент в деталях Code Branches if / switch Config

    by partition Logs/ Monitoring observe by server by geo
  32. 32 Эксперимент в деталях Code Branches if / switch Config

    by partition Logs/ Monitoring observe by server by geo alerts
  33. 33 Убери за собой Code Branches if / switch Config

    by partition Logs/ Monitoring observe by server by geo alerts
  34. 34 Пример очень простого эксперимента if (avatarConf.shouldUseWebP(userId)) { return images.get(id,

    WebP); }
  35. 35 Пример очень простого эксперимента if (avatarConf.shouldUseWebP(userId)) { return images.get(id,

    WebP); }
  36. 36 Пример очень простого эксперимента if (avatarConf.shouldUseWebP(userId)) { return images.get(id,

    WebP); } else { return images.get(id, JPEG); }
  37. 37 Включение фичи one partition //by partition app.avatar.shouldUseWebP: 0

  38. 38 Включение фичи one partition //by partition app.avatar.shouldUseWebP: 0-63 several

    partitions
  39. 39 Включение фичи one partition //by partition app.avatar.shouldUseWebP: 0-255 several

    partitions all partitions
  40. 40 Всё очень просто one partition //step#1 app.avatar.shouldUseWebP: 0 //step#2

    app.avatar.shouldUseWebP: 0-63 //step#3 app.avatar.shouldUseWebP: 0-255 several partitions all partitions
  41. 2| Проблемы с экспериментами

  42. • Релизный цикл • Нужно вспоминать, где и какие настройки

    нужно включить/прописать 42 Проблемы
  43. 43 > 50 действий только на проде :)

  44. • Релизный цикл • Нужно вспоминать, где и какие настройки

    нужно включить/прописать • Хранение экспериментов • Разные нотации обозначения настроек • Отдельный тип задач в Jira 44 Проблемы
  45. • Релизный цикл • Нужно вспоминать, где и какие настройки

    нужно включить/прописать • Хранение экспериментов • Разные нотации обозначения настроек • Отдельный тип задач в Jira • Запуск экспериментов • руками через UI • комментарий в Jira • сообщение в ТамТам 45 Проблемы
  46. 46 Схема запуска эксперимента посмотреть что там в jira

  47. 47 Схема запуска эксперимента посмотреть что там в jira посмотреть,

    а что в PMS
  48. 48 Схема запуска эксперимента посмотреть что там в jira посмотреть,

    а что в PMS написать в чатик
  49. 49 Схема запуска эксперимента посмотреть что там в jira посмотреть,

    а что в PMS написать в чатик написать комментарий
  50. 50 Схема запуска эксперимента посмотреть что там в jira посмотреть,

    а что в PMS написать в чатик написать комментарий внести изменения
  51. 51 Схема запуска эксперимента посмотреть что там в jira посмотреть,

    а что в PMS написать в чатик написать комментарий внести изменения посмотреть графики
  52. 52 И так не один раз

  53. • Консистентность настроек между средами 53 Проблемы #2

  54. 54 Разные среды - похожие действия t dev test prod

  55. 55 Разные среды - похожие действия t dev test prod

  56. 56 Разные среды - похожие действия t dev test prod

  57. • Консистентность настроек между средами • Непонятно в какой стадии

    эксперимент • Забыли написать комментарий? 57 Проблемы #2
  58. • Консистентность настроек между средами • Непонятно в какой стадии

    эксперимент • Забыли написать комментарий? • Тяжело возвращаться после некоторого времени • Это как возвращаться к давно написанному коду 58 Проблемы #2
  59. 3| Концепт решения

  60. • Описание эксперимента в виде некоторого DSL • Храним в

    git-е как код 60 Чего бы хотелось?
  61. • Описание эксперимента в виде некоторого DSL • Храним в

    git-е как код • Автоматизированный накат/откат настроек • Запускаем через CLI • Используем API системы хранения конфигурации 61 Чего бы хотелось?
  62. • Описание эксперимента в виде некоторого DSL • Храним в

    git-е как код • Автоматизированный накат/откат настроек • Запускаем через CLI • Используем API системы хранения конфигурации • Автоматизация сопутствующих действий • Постим комментарии в Jira, TamTam 62 Чего бы хотелось?
  63. 63 Схемка с автоматизацией эксперимента запустить скрипт

  64. 64 Схемка с автоматизацией эксперимента запустить скрипт посмотреть графики

  65. 65 Схемка с автоматизацией эксперимента запустить скрипт проверка в PMS

    посмотреть графики комментарии в чат и jira изменения в PMS
  66. Демо вперёд

  67. Теперь узнаем как это работает :)

  68. 4| Первый блин комом

  69. • Быстро делать прототипы по автоматизации 69 Ansible - почти

    идеальный кандидат
  70. • Быстро делать прототипы по автоматизации • Три универсальных модуля

    • sh • uri • template 70 Ansible - почти идеальный кандидат
  71. • Быстро делать прототипы по автоматизации • Три универсальных модуля

    • sh • uri • template • Готовый DSL, механизмы запуска, ограничений и т.д. 71 Ansible - почти идеальный кандидат
  72. 72 Описание эксперимента --- - include: step1.yml tags: - step1

    - include: step2.yml tags: - step2 … - include: stepN.yml tags: - stepN
  73. 73 Описание эксперимента --- - include: step1.yml tags: - step1

    - include: step2.yml tags: - step2 … - include: stepN.yml tags: - stepN //step#1 app.avatar.shouldUseWebP: 0 //step#2 app.avatar.shouldUseWebP: 0-63 //step#3 app.avatar.shouldUseWebP: 0-255
  74. 74 Каждый шаг имеет своё описание --- - hosts: local

    gather_facts: no tasks: - name: Init include_role: name: pms vars: step_definition: "проинициализирую настройки" application_name: odnoklassniki-web properties: useWebP: name: "app.avatar.useWebP" value: "" anotherProperty: name: "app.avatar.anotherProperty" value: "" with_items: - "{{ host_common }}" loop_control: loop_var: host_name --- - include: step1.yml tags: - step1 - include: step2.yml tags: - step2 … - include: stepN.yml tags: - stepN
  75. 75 Запуск эксперимента одной строкой ansible-playbook -i dev-inv —extra-vars="..." exp.yml

    --tags step0
  76. • Есть описание в виде DSL • Храним в git-е

    как код 76 Итоги #1
  77. • Есть описание в виде DSL • Храним в git-е

    как код • Можем запустить из командной строки 77 Итоги #1
  78. • Есть описание в виде DSL • Храним в git-е

    как код • Можем запустить из командной строки • Убрали рутинные действия 78 Итоги #1
  79. А где же Kotlin чувак?

  80. 80 Проблема копипаста. Step#1 --- - hosts: local gather_facts: no

    tasks: - name: Init include_role: name: pms vars: step_definition: "проинициализирую настройки" application_name: odnoklassniki-web properties: useWebP: name: "app.avatar.useWebP" value: "" anotherProperty: name: "app.avatar.anotherProperty" value: "" with_items: - "{{ host_common }}" loop_control: loop_var: host_name
  81. 81 Проблема копипаста. Step#2 --- - hosts: local gather_facts: no

    tasks: - name: Init include_role: name: pms vars: step_definition: "на одну партицию" application_name: odnoklassniki-web properties: useWebP: name: "app.avatar.useWebP" value: "63" anotherProperty: name: "app.avatar.anotherProperty" value: "63" with_items: - "{{ host_common }}" loop_control: loop_var: host_name
  82. 82 Проблема копипаста. Step#N --- - hosts: local gather_facts: no

    tasks: - name: Init include_role: name: pms vars: step_definition: "на всех" application_name: odnoklassniki-web properties: useWebP: name: "app.avatar.useWebP" value: "0-255" anotherProperty: name: "app.avatar.anotherProperty" value: "0-255" with_items: - "{{ host_common }}" loop_control: loop_var: host_name
  83. • Нужно писать модули • рекомендуется на питоне • я

    не знаю питон :) 83 Проблемы прототипа
  84. • Нужно писать модули • рекомендуется на питоне • я

    не знаю питон :) • Обобщённый DSL это хорошо для решения общих задач • частное решение позволяет писать компактнее и выразительнее 84 Проблемы прототипа
  85. • Нужно писать модули • рекомендуется на питоне • я

    не знаю питон :) • Обобщённый DSL это хорошо для решения общих задач • частное решение позволяет писать компактнее и выразительнее • Не Java • просто так не воспользоваться готовыми библиотеками • мы любим Java! 85 Проблемы прототипа
  86. 5| Начнем сначала

  87. Модель всему голова

  88. 88 DSL Design. Список шагов эксперимента steps { empty("off") {}

    user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }
  89. 89 Инициализация/Отключка/Откат в исходное состояние steps { empty("off") {} user("onebot")

    { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }
  90. 90 Открываем на бота steps { empty("off") {} user("onebot") {

    bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }
  91. 91 Открытие по партициям steps { empty("off") {} user("onebot") {

    bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }
  92. 92 Ура! Теперь для всех steps { empty("off") {} user("onebot")

    { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }
  93. 93 DSL Design. А что будем менять то? steps {

    empty("off") {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }
  94. 94 Настройки. Отделяем мух от котлет experiment { steps {

    empty("off") {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") property("app.avatar.anotherProperty") } }
  95. 95 Step#1 experiment { steps { empty("off") {} user("onebot") {

    bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") property("app.avatar.anotherProperty") } }
  96. 96 Step#N experiment { steps { empty("off") {} user("onebot") {

    bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") property("app.avatar.anotherProperty") } }
  97. 97 Последний шаг experiment { steps { empty("off") {} user("onebot")

    { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") property("app.avatar.anotherProperty") } }
  98. 98 Компактно и понятно experiment { steps { empty("off") {}

    user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") property("app.avatar.anotherProperty") } }
  99. Осталось реализовать :)

  100. • Java - знаем, любим, но вербозно • кто-то пишет

    скриптинг на Java? • JVM - очень здорово • интероп с уже написанными библиотеками • знакомая платформа 100 Что же выбрать для реализации?
  101. • Groovy, JRuby, Javascript, … • синтаксический сахар • JSR223

    101 А что вообще есть то?
  102. • Groovy, JRuby, Javascript, … • синтаксический сахар • JSR223

    • Но мы хотим: • в идеале писать скрипты и их обработку на одном языке • иметь статическую типизацию, • проверки на этапе компиляции • близкую к идеальной поддержку в IDE 102 А что вообще есть то?
  103. 103 JSR223. Как заиспользовать Java-класс //javascript var exp = Java.type("one.exps.Experiment");

    //JRuby require "java" java_import "one.exps.Experiment" exp = Experiment.new //Groovy def exp = new Experiment() //Kotlin val exp = Experiment()
  104. 104 JSR223. Не все йогурты одинаково полезны //javascript var exp

    = Java.type("one.exps.Experiment"); //JRuby require "java" java_import "one.exps.Experiment" exp = Experiment.new //Groovy def exp = new Experiment() //Kotlin val exp = Experiment()
  105. • Kotlin потому что: • синтаксический сахар • JSR223 •

    компиляция • статическая типизация • хороший code completion • хотелось попробовать новое и интересное ;) 105 Почему Kotlin?
  106. 6| Кишки DSL

  107. 107 Как реализовать такой DSL? experiment { steps { empty("off")

    {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") property("app.avatar.anotherProperty") } }
  108. 108 Древовидная структура experiment steps properties

  109. 109 Древовидная структура. Код @DslMarker annotation class ElementMarker @ElementMarker abstract

    class Element { val children = arrayListOf<Element>() fun <T : Element> make(element: T, init: T.() -> Unit): T { element.init() children.add(element) return element } }
  110. 110 Init function experiment { steps { // init function

    is called here partition("half"){ // init function is called here } } ... }
  111. 111 Init function @DslMarker annotation class ElementMarker @ElementMarker abstract class

    Element { val children = arrayListOf<Element>() fun <T : Element> make(element: T, init: T.() -> Unit): T { element.init() children.add(element) return element } }
  112. 112 Упрощаем восприятие experiment { steps { // call init

    here partition("half"){ } } ... } fun partition(name: String, init: PartitionStep.() -> Unit) = make(PartitionStep(name), init)
  113. 113 Lateinit vars experiment { description = "very cool experiment”

    steps { // call init here partition("half"){ // call init here } } … }
  114. 114 А давайте… experiment { steps { steps { }

    } ... }
  115. • @DSLMarker 115 Как обеспечить безопасность?

  116. • @DSLMarker • Валидация скоупа внутри DSL • вложенность элементов

    116 Как обеспечить безопасность?
  117. 117 @DSLMarker @DslMarker annotation class ElementMarker @ElementMarker abstract class Element

    { val children = arrayListOf<Element>() fun <T : Element> make(element: T, init: T.() -> Unit): T { element.init() children.add(element) return element } }
  118. 118 С @DSLMarker experiment { steps { steps { }

    } ... } Шаги не могут быть вложенными
  119. Оптимизация DSL

  120. 120 Оптимизация DSL experiment { steps { empty("off") {} user("onebot")

    { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") property("app.avatar.anotherProperty") } }
  121. 121 Но дадим немного свободы experiment { steps { openByPartitionSteps(withBot

    = false, withEmployees = false) } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") property("app.avatar.anotherProperty") } }
  122. 122 Засахарим немного experiment { steps { openByPartitionSteps() } properties("odnoklassniki-web",

    prop("hosts")) { property("app.avatar.shouldUseWebP") property("app.avatar.anotherProperty") } }
  123. 123 Extension Function - это очень удобно fun Steps.openByPartitionSteps(withBot: Boolean

    = true, withEmployees: Boolean = true) { empty("off") {} if (withBot) { user("bot") { bot(prop("botId")) } } if (withEmployees) { user("employees") { bot(betaUsers().joinToString(",")) } } partition("partition") { part("64") } partition("quarter") { part("64-127") } partition("half") { part("0-127") } partition("on") { part("0-255") } }
  124. 124 И ещё посахарим experiment { steps { openByPartitionSteps() }

    web { "app.avatar.shouldUseWebP" } }
  125. 125 Краткость - сестра таланта experiment { steps { openByPartitionSteps()

    } web { "app.avatar.shouldUseWebP" } }
  126. 126 Кастомные свойства experiment { steps { custom("setup", "сделаю преднастройку

    эксперимента") { properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.useWebP.quality", “100") } } openByPartitionSteps() } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") property("app.avatar.anotherProperty") } }
  127. 127 Кастомные свойства experiment { steps { custom("setup", "сделаю преднастройку

    эксперимента") { properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.useWebP.quality", "100") } } openByPartitionSteps() } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") property("app.avatar.anotherProperty") } }
  128. 128 Это всё-таки язык программирования custom("42everywhere", "всем по 42") {

    properties("odnoklassniki-web", prop("hosts")) { calcProperty("app.complex.property", { PMS.property("odnoklassniki-web", "app.complex.property") .lines() .map { Converter.SET_STRING.convert(it) .minus("42").plus("42") .joinToString(",") } .joinToString("\n") }) } }
  129. 129 Шаг custom("42everywhere", "всем по 42") { properties("odnoklassniki-web", prop("hosts")) {

    calcProperty("app.complex.property", { PMS.property("odnoklassniki-web", "app.complex.property") .lines() .map { Converter.SET_STRING.convert(it) .minus("42").plus("42") .joinToString(",") } .joinToString("\n") }) } }
  130. 130 Набор изменяемых настроек custom("42everywhere", "всем по 42") { properties("odnoklassniki-web",

    prop("hosts")) { calcProperty("app.complex.property", { PMS.property("odnoklassniki-web", "app.complex.property") .lines() .map { Converter.SET_STRING.convert(it) .minus("42").plus("42") .joinToString(",") } .joinToString("\n") }) } }
  131. 131 Вычисляемая настройка custom("42everywhere", "всем по 42") { properties("odnoklassniki-web", prop("hosts"))

    { calcProperty("app.complex.property", { PMS.property("odnoklassniki-web", "app.complex.property") .lines() .map { Converter.SET_STRING.convert(it) .minus("42").plus("42") .joinToString(",") } .joinToString("\n") }) } }
  132. 132 Обрабатываем построчно custom("42everywhere", "всем по 42") { properties("odnoklassniki-web", prop("hosts"))

    { calcProperty("app.complex.property", { PMS.property("odnoklassniki-web", "app.complex.property") .lines() .map { Converter.SET_STRING.convert(it) .minus("42").plus("42") .joinToString(",") } .joinToString("\n") }) } }
  133. 133 Идемпотентное внесение изменений custom("42everywhere", "всем по 42") { properties("odnoklassniki-web",

    prop("hosts")) { calcProperty("app.complex.property", { PMS.property("odnoklassniki-web", "app.complex.property") .lines() .map { Converter.SET_STRING.convert(it) .minus("42").plus("42") .joinToString(",") } .joinToString("\n") }) } }
  134. 134 Оптимизация DSL#2 experiment { steps { custom("setup") { properties("avatar-app",

    prop("avatarHosts")) { property("enableWebP", "true") } } openByPartitionSteps() } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") } }
  135. 135 Сделаем эксперименту часть experiment { part("setup") { steps {

    custom("setup") { properties("avatar-app", prop("avatarHosts")) { property("enableWebP", "true") } } } } steps { openByPartitionSteps() } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") } }
  136. 136 Основной эксперимент это тоже часть experiment { part("setup") {

    steps { custom("setup") { properties("avatar-app", prop("avatarHosts")) { property("enableWebP", "true") } } } } steps { openByPartitionSteps() } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") } }
  137. Kotlin Script

  138. • Файл с расширением .kts 138 Kotlin Script

  139. • Файл с расширением .kts • Имплицитные аргументы • args[0]

    • println(“hello!”) 139 Kotlin Script
  140. • Файл с расширением .kts • Имплицитные аргументы • args[0]

    • println(“hello!”) • Запускается через command line • kotlinc -script <script_name> -classpath <> • Запускается с помощью IDE 140 Kotlin Script
  141. 141 Kotlin Script. Ожидание import runner.* runner.run { experiment {

    steps { empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") } } }
  142. • Нужно как-то подключить классы проекта 142 Kotlin Script. CMD

    kotlinc -script <script_name> -classpath <>
  143. • Нужно как-то подключить классы проекта • Нужно как-то подключить

    все зависимости проекта 143 Kotlin Script. CMD kotlinc -script <script_name> -classpath <>
  144. • Нужно как-то подключить классы проекта • Нужно как-то подключить

    все зависимости проекта • Gradle не умеет (по крайней мере из коробки) • IDEA не умеет* 144 Kotlin Script. CMD kotlinc -script <script_name> -classpath <>
  145. 145 Kotlin Script. IDEA

  146. 146 Kotlin Script. IDEA java -Dfile.encoding=UTF-8 \ -classpath "kotlin-compiler.jar:kotlin-reflect.jar: kotlin-stdlib.jar:kotlin-script-runtime.jar"

    org.jetbrains.kotlin.cli.jvm.K2JVMCompiler -script ~/experiments/src/main/kotlin/one/exps/scripts/XPRM-10222.kts XPRM-10222.kts:1:8: error: unresolved reference: one import one.exps.*
  147. 147 Kotlin Script. IDEA java -Dfile.encoding=UTF-8 \ -classpath "kotlin-compiler.jar:kotlin-reflect.jar: kotlin-stdlib.jar:kotlin-script-runtime.jar"

    org.jetbrains.kotlin.cli.jvm.K2JVMCompiler -script ~/experiments/src/main/kotlin/one/exps/scripts/XPRM-10222.kts XPRM-10222.kts:1:8: error: unresolved reference: one import one.exps.*
  148. 148 https://github.com/andrewoma/kotlin-script #!/bin/sh exec kotlins -cp `mvncp <lib>` !# import

    runner.* runner.run { experiment { steps { empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") } } }
  149. 149 https://github.com/andrewoma/kotlin-scripting-kickstarter #!/usr/bin/env kotlin-script.sh import runner.* fun main(args: Array<String>) {

    runner.run { experiment { steps { empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") } } } }
  150. 150 https://github.com/holgerbrandl/kscript #!/usr/bin/env kscript //DEPS one:experiments:1.0.0 import runner.* runner.run {

    experiment { steps { empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") } } }
  151. • Необходимо писать обвязки 151 Проблемы Kotlin Script

  152. • Необходимо писать обвязки • Нельзя запустить из IDE (ну

    кроме как console) 152 Проблемы Kotlin Script
  153. • Необходимо писать обвязки • Нельзя запустить из IDE (ну

    кроме как console) • Непонятно как подебажить скрипт, если очень нужно 153 Проблемы Kotlin Script
  154. • Необходимо писать обвязки • Нельзя запустить из IDE (ну

    кроме как console) • Непонятно как подебажить скрипт, если очень нужно • Сложно переиспользовать описание эксперимента • для обновления описания задачи в Jira 154 Проблемы Kotlin Script
  155. 155 https://github.com/aatarasoff/kotlin-script-starter //JSR223 //ConsoleExecutor.kt fun main(args : Array<String>) { ScriptEngineManager().getEngineByExtension("kts")!!

    .eval(getResource("${args[0]}.kts").readText()) } //build.gradle compile "org.jetbrains.kotlin:kotlin-compiler:$kotlin_version"
  156. 156 https://github.com/aatarasoff/kotlin-script-starter //JSR223 //ConsoleExecutor.kt fun main(args : Array<String>) { ScriptEngineManager().getEngineByExtension("kts")!!

    .eval(getResource("${args[0]}.kts").readText()) } //build.gradle compile "org.jetbrains.kotlin:kotlin-compiler:$kotlin_version"
  157. 157 Когда нету Engine-a //JSR223 //ConsoleExecutor.kt fun main(args : Array<String>)

    { ScriptEngineManager().getEngineByExtension("kts")!! .eval(getResource("${args[0]}.kts").readText()) } Exception in thread "main" kotlin.KotlinNullPointerException
  158. 158 https://github.com/aatarasoff/kotlin-script-starter //JSR223 //ConsoleExecutor.kt fun main(args : Array<String>) { ScriptEngineManager().getEngineByExtension("kts")!!

    .eval(getResource("${args[0]}.kts").readText()) } //build.gradle compile "org.jetbrains.kotlin:kotlin-compiler:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-script-util:$kotlin_version" //META-INF/services/javax.script.ScriptEngineFactory org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory
  159. 159 А что если в зависимостях Guava? //build.gradle compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version") compile("com.google.guava:guava:$guava_version")

    //META-INF/services/javax.script.ScriptEngineFactory org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory
  160. 160 Метод есть, но нам говорят что нет //build.gradle compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version")

    compile("com.google.guava:guava:$guava_version") //META-INF/services/javax.script.ScriptEngineFactory org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory java.lang.NoSuchMethodError: com.google.common.base.Preconditions.checkState(...)
  161. 161 Всё дело в компиляторе //build.gradle compile("org.jetbrains.kotlin:kotlin-compiler:$kotlin_version") compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version") compile("com.google.guava:guava:$guava_version") Компилятор

    содержит классы с теми же пакетами и с теми же именами, что и Guava
  162. 162 В интернетах советуют Embeddable-компилятор //build.gradle compile "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-script-util:$kotlin_version"

  163. 163 Если у вас Kotlin < 1.2.20 //build.gradle compile "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlin_version"

    compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version") { exclude module: 'kotlin-compiler' }
  164. 164 Если у вас Kotlin < 1.2.20 //build.gradle compile "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlin_version"

    compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version") { exclude module: 'kotlin-compiler' } java.lang.NoClassDefFoundError: com/intellij/openapi/util/Disposer
  165. 165 Если у вас Kotlin < 1.2.20 //build.gradle compile "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlin_version"

    compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version") { exclude module: 'kotlin-compiler' } compile("com.google.guava:guava:$guava_version") org.jetbrains.kotlin.com.intellij.openapi.util.Disposer
  166. 166 “Скопируем” классы фабрики JSR223 //KotlinJsr223JvmLocalScriptEngine.kt //KotlinJsr223JvmLocalScriptEngineFactory.kt

  167. 167 Ctrl+R //KotlinJsr223JvmLocalScriptEngine.kt //KotlinJsr223JvmLocalScriptEngineFactory.kt replace com.intellij.openapi.util.Disposer to org.jetbrains.kotlin.com.intellij.openapi.util.Disposer

  168. 168 Заиспользуем модификацию //KotlinJsr223JvmLocalScriptEngine.kt //KotlinJsr223JvmLocalScriptEngineFactory.kt replace com.intellij.openapi.util.Disposer to org.jetbrains.kotlin.com.intellij.openapi.util.Disposer //META-INF/services/javax.script.ScriptEngineFactory

    your.package.here.KotlinJsr223JvmLocalScriptEngineFactory
  169. 169 Хитрый процессинг скриптов в ресурсы //build.gradle processResources { from

    'src/main/kotlin/one/exps/scripts' }
  170. 170 Определим основной исполняемый класс //build.gradle processResources { from 'src/main/kotlin/one/exps/scripts'

    } mainClassName = 'one.exps.execution.ConsoleExecutorKt'
  171. 171 Передаём аргументы через Gradle //build.gradle processResources { from ‘src/main/kotlin/one/exps/scripts'

    } mainClassName = 'one.exps.execution.ConsoleExecutorKt' run { standardInput = System.in args System.getProperty("exec.args", "").split() }
  172. Launch it

  173. 173 IDEA

  174. 174 CMD + Gradle ./gradlew run -Dexec.args="run XPRM-9756 -e dev

    -s all"
  175. 175 CMD + Docker docker run --rm -it one.experiment/experiments:1.0 run

    XPRM-9756 -e dev -s all https://github.com/bmuschko/gradle-docker-plugin
  176. 176 Консольный лог

  177. 177 Отчётность

  178. 178 Обновление описания эксперимента в Jira ./gradlew run -Dexec.args="update XPRM-9756

    -e prod"
  179. 179 Jira

  180. 180 Генерация PlantUML ./gradlew run -Dexec.args="graph XPRM-9756 -e prod"

  181. 181 PlantUML

  182. Давай демо ещё раз

  183. 7| Разделяй и властвуй

  184. 184 Разделение ответственностей user model experiment { steps { openByPartitionSteps()

    } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") property("app.avatar.anotherProperty") } }
  185. 185 Переход одной модели в другую user model execution model

    Трансформация
  186. 186 “Плоская” модель исполнения execution model execution { steps {

    init { description='на никого' property { "applicationName": "odnoklassniki-web", "hostName": "host-odnoklassniki-web", "propertyName": "app.avatar.shouldUseWebP", "propertyValue": "" } property { ... } } ... all { ... } } definition='возможность отмечать друзей в настроении' }
  187. 187 Модель исполнения. Один шаг init { description='на никого' property

    { "applicationName": "odnoklassniki-web", "hostName": "host-odnoklassniki-web", "propertyName": "app.avatar.shouldUseWebP", "propertyValue": "" } property { "applicationName": "odnoklassniki-web", "hostName": "host-odnoklassniki-web", "propertyName": "app.avatar.anotherProperty", "propertyValue": "" } }
  188. 188 Транзитивный переход user model execution model validation model

  189. 189 Переход модели в ещё одну модель execution model validation

    model Трансформация
  190. 190 Модель пользователя. Шаг 4 execution model 1 2 3

    4
  191. 191 Модель валидации. Мердж предыдущих шагов validation model 1 2

    3 C
  192. 192 Вычисляем предполагаемое состояние val expectedPropertiesStates = stepsCollapser.collapse( plan.stageKeys .takeWhile

    { it != stageName } .flatMap { plan.stages[it].steps } ) expectedPropertiesStates.forEach { expected -> check(expected, PMS.getPropertyState(expected.key)) }
  193. 193 Схлопываем шаги - оставляем последний object stepsCollapser { fun

    collapse(steps: List<ExecutionStep>) : List<ExecutionStep> { val collapsed = linkedMapOf<String, ExecutionStep>() steps.forEach { collapsed.put(it.key(), it) } return collapsed.map { it.value } } }
  194. 194 Вычисляем предполагаемое состояние val expectedPropertiesStates = stepsCollapser.collapse( plan.stageKeys .takeWhile

    { it != stageName } .flatMap { plan.stages[it].steps } ) expectedPropertiesStates.forEach { expected -> check(expected, PMS.getPropertyState(expected.key)) }
  195. 195 Сверка реального и ожидаемого состояния 1 2 3 S

    validation model app.avatar.shouldUseWebP: 0-63 app.avatar.anotherProperty: 0-63 expected state S
  196. 196 Кто-то вмешался в наш эксперимент! app.avatar.shouldUseWebP: 0-63 app.avatar.anotherProperty: 0-63

    expected state S app.avatar.shouldUseWebP: 0-127 app.avatar.anotherProperty: 0-63 real state S
  197. 197 Не прошли верификацию. Увы :(

  198. 198 Принудительное исполнение ./gradlew run -Dexec.args="run XPRM-9756 -e dev -s

    all --force"
  199. 199 Сделаем красиво в Jira execution model description model

  200. 200 Похоже на модель валидации val allSteps = plan.stageKeys.flatMap {

    plan.stages.get(it).steps } val description = stepsCollapser.collapse(allSteps) .map { //format here } .joinToString("\n")
  201. 201 Только финальные значения app.avatar.shouldUseWebP: 0-255 app.avatar.anotherProperty: 0-255 final state

    S
  202. 202 Только финальные значения app.avatar.shouldUseWebP: 0-255 app.avatar.anotherProperty: 0-255 final state

    S *odnoklassniki-web@host-odnoklassniki-web* {Code} > app.avatar.shouldUseWebP: 0-255 > app.avatar.anotherProperty: 0-255 {Code}
  203. 203 Основная модель - дополнительные модели • Модель исполнения

  204. 204 Основная модель - дополнительные модели • Модель исполнения •

    Модель валидации
  205. 205 Основная модель - дополнительные модели • Модель исполнения •

    Модель валидации • Модель планирования • создание задач в Jira • визуализация плана
  206. 206 Основная модель - дополнительные модели • Модель исполнения •

    Модель валидации • Модель планирования • создание задач в Jira • визуализация плана • ??? • любые хотелки
  207. >| Выводы

  208. • Experiments as Code 208 Итоги

  209. • Experiments as Code • Удобный и компактный DSL •

    потому что знаем предметную область • компиляция, code completion 209 Итоги
  210. • Experiments as Code • Удобный и компактный DSL •

    потому что знаем предметную область • компиляция, code completion • Автоматизированный запуск • проверка корректности • уменьшение количества рутинных действий 210 Итоги
  211. • Experiments as Code • Удобный и компактный DSL •

    потому что знаем предметную область • компиляция, code completion • Автоматизированный запуск • проверка корректности • уменьшение количества рутинных действий • Персональное удовольствие от программирования (!) 211 Итоги
  212. • Нужно уметь программировать • или хотя бы понимать инструментарий

    и основные принципы 212 Trade-Offs
  213. • Нужно уметь программировать • или хотя бы понимать инструментарий

    и основные принципы • Нужно поддерживать всё самим • постоянные доработки • баги 213 Trade-Offs
  214. • Нужно уметь программировать • или хотя бы понимать инструментарий

    и основные принципы • Нужно поддерживать всё самим • постоянные доработки • баги • Парадокс синтаксического сахара • баланс между компактностью и сложностью освоения 214 Trade-Offs
  215. • Нужно уметь программировать • или хотя бы понимать инструментарий

    и основные принципы • Нужно поддерживать всё самим • постоянные доработки • баги • Парадокс синтаксического сахара • баланс между компактностью и сложностью освоения • Инструменты не идеальны • а так хотелось 215 Trade-Offs
  216. 216 Мы ещё в начале пути ;) t MVP CLI

    Jira, TamTam Auto Launch Jira custom fields and charts Auto Feedback Charts Smart monitoring
  217. Links

  218. 218 Стартер для скриптов на Kotlin-е https://github.com/aatarasoff/kotlin-script-starter

  219. 219 Напочитать http://developerblog.info/2017/10/02/kotlin-dsl-i-vsie-vsie-vsie http://developerblog.info/2017/10/26/kotlin-dsl-i-nosuchmethod http://developerblog.info/2017/10/24/nieochievidnyie-zamietki-1

  220. 220 Q&A

  221. None