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

Александр Тарасов — Автоматизация экспериментов...

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

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

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

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

Moscow JUG

April 05, 2018
Tweet

More Decks by Moscow JUG

Other Decks in Programming

Transcript

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

    начальника, коллег или других специалистов. 
 Все представленные в докладе сведения, примеры, выводы и другую информацию вы можете использовать на свой страх и риск. За все ваши действия ответственность несёте только вы сами. Этот доклад не совсем про Kotlin! 3 Минздрав предупреждает
  2. 9 71 млн Посещают нас в месяц 8000 Серверов Одноклассники

    в цифрах >10 тыс Выполненных экспериментов
  3. • Аватарок в WebP нет • в браузере пользователей •

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

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

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

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

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

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

    by partition Logs/ Monitoring observe by server by geo alerts
  10. 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
  11. • Релизный цикл • Нужно вспоминать, где и какие настройки

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

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

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

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

    а что в PMS написать в чатик написать комментарий внести изменения посмотреть графики
  16. • Консистентность настроек между средами • Непонятно в какой стадии

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

    эксперимент • Забыли написать комментарий? • Тяжело возвращаться после некоторого времени • Это как возвращаться к давно написанному коду 58 Проблемы #2
  18. • Описание эксперимента в виде некоторого DSL • Храним в

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

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

    посмотреть графики комментарии в чат и jira изменения в PMS
  21. • Быстро делать прототипы по автоматизации • Три универсальных модуля

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

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

    - include: step2.yml tags: - step2 … - include: stepN.yml tags: - stepN
  24. 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
  25. 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
  26. • Есть описание в виде DSL • Храним в git-е

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

    как код • Можем запустить из командной строки • Убрали рутинные действия 78 Итоги #1
  28. 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
  29. 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
  30. 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
  31. • Нужно писать модули • рекомендуется на питоне • я

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

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

    не знаю питон :) • Обобщённый DSL это хорошо для решения общих задач • частное решение позволяет писать компактнее и выразительнее • Не Java • просто так не воспользоваться готовыми библиотеками • мы любим Java! 85 Проблемы прототипа
  34. 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") } }
  35. 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") } }
  36. 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") } }
  37. 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") } }
  38. 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") } }
  39. 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") } }
  40. 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") } }
  41. 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") } }
  42. 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") } }
  43. 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") } }
  44. 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") } }
  45. • Java - знаем, любим, но вербозно • кто-то пишет

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

    • Но мы хотим: • в идеале писать скрипты и их обработку на одном языке • иметь статическую типизацию, • проверки на этапе компиляции • близкую к идеальной поддержку в IDE 102 А что вообще есть то?
  47. 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()
  48. 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()
  49. • Kotlin потому что: • синтаксический сахар • JSR223 •

    компиляция • статическая типизация • хороший code completion • хотелось попробовать новое и интересное ;) 105 Почему Kotlin?
  50. 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") } }
  51. 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 } }
  52. 110 Init function experiment { steps { // init function

    is called here partition("half"){ // init function is called here } } ... }
  53. 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 } }
  54. 112 Упрощаем восприятие experiment { steps { // call init

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

    steps { // call init here partition("half"){ // call init here } } … }
  56. 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 } }
  57. 118 С @DSLMarker experiment { steps { steps { }

    } ... } Шаги не могут быть вложенными
  58. 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") } }
  59. 121 Но дадим немного свободы experiment { steps { openByPartitionSteps(withBot

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

    prop("hosts")) { property("app.avatar.shouldUseWebP") property("app.avatar.anotherProperty") } }
  61. 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") } }
  62. 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") } }
  63. 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") } }
  64. 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") }) } }
  65. 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") }) } }
  66. 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") }) } }
  67. 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") }) } }
  68. 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") }) } }
  69. 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") }) } }
  70. 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") } }
  71. 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") } }
  72. 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") } }
  73. • Файл с расширением .kts • Имплицитные аргументы • args[0]

    • println(“hello!”) • Запускается через command line • kotlinc -script <script_name> -classpath <> • Запускается с помощью IDE 140 Kotlin Script
  74. 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") } } }
  75. • Нужно как-то подключить классы проекта • Нужно как-то подключить

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

    все зависимости проекта • Gradle не умеет (по крайней мере из коробки) • IDEA не умеет* 144 Kotlin Script. CMD kotlinc -script <script_name> -classpath <>
  77. 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.*
  78. 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.*
  79. 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") } } }
  80. 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") } } } }
  81. 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") } } }
  82. • Необходимо писать обвязки • Нельзя запустить из IDE (ну

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

    кроме как console) • Непонятно как подебажить скрипт, если очень нужно • Сложно переиспользовать описание эксперимента • для обновления описания задачи в Jira 154 Проблемы Kotlin Script
  84. 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"
  85. 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"
  86. 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
  87. 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
  88. 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
  89. 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(...)
  90. 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' }
  91. 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
  92. 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
  93. 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() }
  94. 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
  95. 184 Разделение ответственностей user model experiment { steps { openByPartitionSteps()

    } properties("odnoklassniki-web", prop("hosts")) { property("app.avatar.shouldUseWebP") property("app.avatar.anotherProperty") } }
  96. 186 “Плоская” модель исполнения execution model execution { steps {

    init { description='на никого' property { "applicationName": "odnoklassniki-web", "hostName": "host-odnoklassniki-web", "propertyName": "app.avatar.shouldUseWebP", "propertyValue": "" } property { ... } } ... all { ... } } definition='возможность отмечать друзей в настроении' }
  97. 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": "" } }
  98. 192 Вычисляем предполагаемое состояние val expectedPropertiesStates = stepsCollapser.collapse( plan.stageKeys .takeWhile

    { it != stageName } .flatMap { plan.stages[it].steps } ) expectedPropertiesStates.forEach { expected -> check(expected, PMS.getPropertyState(expected.key)) }
  99. 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 } } }
  100. 194 Вычисляем предполагаемое состояние val expectedPropertiesStates = stepsCollapser.collapse( plan.stageKeys .takeWhile

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

    validation model app.avatar.shouldUseWebP: 0-63 app.avatar.anotherProperty: 0-63 expected state S
  102. 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
  103. 200 Похоже на модель валидации val allSteps = plan.stageKeys.flatMap {

    plan.stages.get(it).steps } val description = stepsCollapser.collapse(allSteps) .map { //format here } .joinToString("\n")
  104. 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}
  105. 205 Основная модель - дополнительные модели • Модель исполнения •

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

    Модель валидации • Модель планирования • создание задач в Jira • визуализация плана • ??? • любые хотелки
  107. • Experiments as Code • Удобный и компактный DSL •

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

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

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

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

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

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

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