Для разных — задач разные инструменты.

Наш опыт переезда на Elixir

Черепанов Сергей

Размахнин Никита

Fullstack Development

План

  1. Описание задачи.
  2. Почему именно Elixir и из чего мы выбирали 
  3. Основы функционального программирования
  4. Основы межпроцессного взаимодействия
  5. Live Coding

Предостережение

Бизнес-логика

​У нас курьерское приложение и много асинхронной логики во время выполнения заказа

?

Аналог Uber, только по доставке не людей

курьеров

400+

заказов в день

23мин

поиск курьера

За год по Москве и Питеру

Проект состоит из

REST API на Django

TS React+Redux SSR приложение формы заказа

Telegram бот на Python (asyncio)

Flow React-Native Android приложение

​Elixir Worker

AWS

AWS

CloudFormation

EC2 + AutoScaling

LoadBalancing

RDS

VPC with Bastion

SQS

Как это работает

Заказ через форму

Отправка данных на REST Server

Отправка сообщений в SQS

Worker подхватывает сообщение о новом заказе

Броадкаст курьерам

Performance:

7 couriers per second

Курьеры оставляют отклики

Берем первого готового и останавливаем броадкаст

Если курьер откажется от заказа,

броадкаст начинается заново

но уже не рассылается тем, кто его пропустил

Если за 5 минут курьер не выехал и не отказался,

шлем СМС клиенту

Если курьер выехал, шлем другое СМС

(если первая не была отправлена, то ее отменяем)

Отмена заказа

Остановка броадкаста

 Уведомления в чат-админку и курьеру

Таймауты

Не найден курьер после броадкаста в течение 3-х минут

Время заказа вышло, а он не завершен

...

Все это действо должно работать и для бота, и для приложения

Выбирая

подходящий язык

C#

Система типов

Богатая экосистема

Core — сыроват?

но есть отзывы переноса крупных проектов, и небольшой опыт

Язык хорош и зрел

Java/Scala

Система типов

...давайте вы будете уметь готовить еще и Java

Python

Быстро и круто, хайпово и удобно

Опыт

Асинхронность?

Динамическая типизация?

Haskell

Мощная система типов

ФП

Экосистема?

Асинхронность??

Скорость разработки???

Node.JS

Шарим в JS

Один поток в 2017???

Масштабироваться почти только железом

оверхеды по памяти и системным процессам

JavaScript :(

Elixir

Whatsap, Heroku, Amazon, Wargaming, Discord

Автогенерируемые, довольно подробные

Большой мир публикаций на Медиуме, есть чуть-чуть на Хабре

Доки

Статьи на Хабре

ПОСМОТРЕЛИ НА

Написан в середине 80-х в лабораториях компании Ericsson для работы в сфере телекома

Erlang

По требованиям ПО в телекоме: быстро, эффективно, параллельно и отказоустойчиво

Развивается инженерами по сей день

ПОСМОТРЕЛИ НА

Разработчиков не так много, как на js, но они есть

Экосистему

Основное количество библиотек — весьма высокое качество

ПОСМОТРЕЛИ НА

В 90% случаев есть тесты, которые зачастую лучше документации

Тесты

ПОСМОТРЕЛИ НА

Пулл реквесты

ПОСМОТРЕЛИ НА

4к закрытых пуллерквестов, большинство которых - code style & docs

Остальное время комьюнити обсуждает красивости кода (лично мой взгляд)

Как у нас развивался воркер.

...потом оказалось

Долго запускали супервизор

--no-halt

Состоянию уделено большое внимание в Erlang: качественные абстракции для работы со стейтом

Долго понимали, как работать со стейтом

Первым сделана работа с очередями сообщений

Интеграция с нашим REST

Столкнулись с косяками AWS SQS (накрутили Meta Storage)

Релиз настроили быстро и безболезненно,

но с трудом поставили сам Эликсир на Amazon Linux

В начале осени: допокрыли тестами и разнесли работу с курьерами

Сделали два рефакторинга

В конце осени: выделили под каждый заказ по отдельному процессу

Продолжаем переносить бизнес-логику из телеграм бота на воркер

Основы функционального программирования

Основные принципы

Побочные эффекты:

Чистота функций

  • Вывод в консоль
  • Запись в базу данных

Недетерминированность

  • Time.now()
  • Чтение из базы

High Order Function

sum = fn a, b -> a + b end

10 == Enum.reduce([1, 2, 3, 4], sum)

Частичное применения

add2 = fn number -> sum(number, 2) end

[3, 4, 5] == Enum.map([1, 2, 3], add2)

Никто не перетрет ваши переменные в промежутке между строками кода

Иммутабельность из коробки

Быстрое сравнение по ссылке

Формализованность, которая улучшает статический анализ

Не нужны локи :)

Иммутабельность из коробки

man = %{name: "john"}
try_mutate_if_you_can(man)
assert man.name == "john"

Инкапсуляция работы со стейтом

Pattern Matching

[head | tail] = [1, 2, 3, 4]

man = %{gender: :male, name: "john", age: 34}
greet(man)

def greet (%{name: name} = man) do
 ..
end

Рекурсия

def loop(state) do
  receive do -> 
    :inc -> 
      loop(state + 1)
    :stop ->
      state
  end
end

Elixir

Piping

datetime
  |> Timex.parse!("{ISO:Extended}")
  |> Timex.Timezone.convert(@timezone)
  |> Timex.format!("{h24}:{m}")

Function capturing

couriers = [
  %{name: "joe", id: 1}, 
  %{name: "mike", id: 2},
]

slot = Enum.map(couriers, &(&1.id))

# fn courier -> courier.id end

Real world

slot = {1400, [123, 126, 129]}

couriers_slots = couriers
  |> Enum.sort(&(&1.priority > &2.priority))
  |> Enum.chunk_by(&(&1.priority))
  |> Enum.map(fn (slot = [head | _]) -> {
       head.priority, 
       Enum.map(slot, &(&1.id))
     } end)

Макросы 

defmodule Worker.Models.CourierTest do
  use ExUnit.Case

  test "is_has_permission_for_order" do
    assert Courier.is_has_permission_for_order(
      @default_courier, @default_order
    ) == true
    assert Courier.is_has_permission_for_order(
      @default_courier, @car_required_order
    ) == false
  end
end

Основы межпроцессного взаимодействия

О процессах

Об эрланговом шедулере и зачем он нужен

О сообщениях и стандартных паттернах работы с ними

Очереди

Send

send(pid, {:hello, :world})

Receive

message = 
  receive do
    {:hello, name} -> name 
  end

Timeout

{:ok, message} = 
  receive do
    {:hello, name} -> {:ok, name}
  after
    1_000 -> {:error, :timeout}
  end

Ref для ответа

{from, ref, name} = 
  receive do
    {:hello, from, ref, name} -> {from, ref, name} 
  end

send(from, {:aloha, ref, name})

Back Pressure

{:message_queue_len, length} = 
  :erlang.process_info(pid, :message_queue_len)

Fault Tolerance

Let it crash!

Позови меня с собой

Умирая, процесс шлет сигнал смерти всем слинкованным процессам

spawn_link(fn ... end)

Мониторинг процессов

Умирая, процесс шлет уведомление, что он умер

ref = Process.monitor(pid)

Супервизоры

Слушают уведомления о дочерних процессах и перезапускают их

Деревья наблюдения

Супервизоры можно композировать

Другие супервизоры будут ветвями, а рабочие процессы — листьями

Пример supervisor tree:

Перезапуск после краша 

One For One

One For All

Rest For One

Мониторинг

Erlang позволяет мониторить почти что все

Общее состояние VM

Количество процессов 8782
Run queue 0
Error logger queue 0
Memory  149Mb
Bytes in/out 0
GC count 0
Reductions  12010​
Sheduler Usage 1%

Системные сообщения

busy_port |
busy_dist_port |
{long_gc, 50} |
{long_schedule, 50} |
{large_heap, 1_000_000}

Мониторинг каждого отдельного процесса

  • reductions,
  • heap_size,
  • memory_used,
  • message_queue_len
iex --sname baz --remsh worker@sergey-laptop

Hot Code Reload

У Эрланга есть подмена кода на лету

То есть да, можно без остановки сервера и приложения на нем сделать релиз

Но надо ручками почти во всех модулях написать хуки, которые умеют мигрировать стейт, например.

И еще много другой боли перетерпеть, мы туда даже не лезем...

Выводы

Live coding ​

How we start elixir development

By Sergey Cherepanov

How we start elixir development

We have learned Elixir and use it in our courier delivery service Ptichka. It is a talk about our experience

  • 565