Особенности разработки Telegram бота с Google API в Docker

Коротко о боте: получает список YouTube-каналов пользователя и уведомляет о новых видео с возможностью напомнить о нем позже.

В статье расскажу об особенностях написания этого бота и взаимодействия с Google API. Я люблю краткость, поэтому в статье будет мало «воды».
На какие вопросы ответит статья:

  • Где взять внешний адрес сайта для Webhook
  • Где взять HTTPS-сертификат как его использовать, чтобы Telegram ему доверял
  • Как передавать данные и обрабатывать нажатия на Inline-кнопки
  • Как получить вечный OAuth токен для Google API
  • Как передать данные пользователя через OAuth callback url
  • Как получить бесплатный домен 3 уровня

Стэк:

  1. Back-end: Node.js + Express.js
  2. БД: Mongo.js + mongoose
  3. Пакетный менеджер: Yarn (он действительно быстрый)
  4. Telegram-бот фреймворк: Telegraf
  5. Продакшн: Docker + Docker Compose + Vscale.io

Особенности при разработке бота

Получать команды от Telegram можно с помощью Long-polling и Webhook. Судя по отзывам в интернете Long-polling через некоторое время перестает работать — Telegram возвращает 500 ошибку, поэтому я решил сразу делать через Webhook.

Нужен внешний адрес сайта для Webhook

Webhook — это адрес, на который Telegram будет отправлять команды и сообщения от пользователей, поэтому он должен быть внешний, а как тогда разрабатывать локально?
Тут приходят на помощь сервисы такие как: ngrok и Localtunnel (ссылка 1, ссылка 2).

Оба этих сервиса генерируют случайный домен 3 уровня. Если хочется статический, то в ngrok надо будет заплатить, а в Localtunnel — нет.
Мне нужно было формировать OAuth Callback Url, который привязывался к идентификатору клиента OAuth 2.0 в Google API, поэтому удобнее если он будет статический. По этой причине я использовал именно Localtunnel.

Оба этих сервиса предоставляют HTTPS с нормальным валидным сертификатом, поэтому проблем с Telegram не будет.

В продакшене нужен будет HTTPS

Telegram позволяет использовать самоподписанные сертификаты. Инструкция есть на их официальном сайте. Но тогда браузеры не будут доверять ему, а веть этот же сертификат будет использоваться для OAuth Callback Url, поэтому нужен был валидный сертификат.
На помощь приходит Let’s Encrypt.

Сгенерировать сертификат не проблема в интернете полно инструкций. Единственное, на сколько я понял, его надо генерировать на сервере где он будет использоваться (поправьте, если это не так).
На ubunte я воспользовался пакетом letsencrypt и выполнил команду.

letsencrypt certonly -n -d domain1.com -d domain2.ru --email admin@domain.ru --standalone --noninteractive --agree-tos

Какой сертификат и как его передать Telegram

Для работы Webhook Telegram нужно передать сертификат УЦ, чтобы Telegram начал ему доверять.
В случае с самоподписанным сертификатом — это нужно делать обязательно и передать нужно открытый ключ.

В случае с Let’s Encrypt ничего передавать не нужно, но нужно правильно настроить HTTPS на веб-сервере.
Let’s Encrypt сгенерирует 4 сертификата:

  • cert.pem — открытый ключ
  • chain.pem — сертификат УЦ
  • fullchain.pem — открытый ключ + сертификат УЦ
  • privkey.pem — закрытый ключ

Именно privkey.pem+fullchain.pem нужны для HTTPS, если вы используете HAProxy (скорее всего и для других нужно настраивать аналогично), чтобы Telegram начал доверять нашему боту.

Передать этот сертификат через Telegraf можно следующим образом:

let cert = { source: '/path/public.pem' }; app.telegram.setWebhook(config.webHookUrl + '/' + config.webHookSecretPath, cert);

Передача данных при нажатии Inline-кнопок

Отправить сообщение с кнопкой не проблема (можно использовать Telegraf Markup & Extra). Сложности начинаются с передачей данных и отловом нажатия на эту кнопку.
Согласно документации InlineKeyboardButton максимальных размер данных всего 64 байта. В большинстве случаев этого хватает, просто учтите при разработке своего бота.
Дальше нужно эти данные обработать, все callback приходят в одну функцию, поэтому разбирать на какой тип кнопки нажали приходится в ней. А значит вместе с данными нужно еще и тип этот передавать. Напрашивается роутер. В Telegraf этот роутер уже частично реализовали — это класс Router. Его можно создать, но парсить команду и параметры нужно будет самому (пример). Разделитель параметров тоже нужно придумывать самому. На мой взгляд это прошлый век, но с этим можно жить.

Взаимодействие с Google API

Боту нужно получать список каналов пользователя, а для этого нужен токен. Вот так вы сможете его получить:

  1. Создаем проект через Google Developers Console
  2. Выбираем нужный API. В моем случае это Youtube Data API
  3. Создаем идентификатор клиента OAuth 2.0. В поле «Разрешенные URI перенаправления» можно указать localhost с нужным портом
  4. В результате нам дадут ClientId и ClientSecret
  5. Ставим Google APIs Node.js Client
  6. Генерируем на основе этой пары ссылку для пользователя с помощью метода googleapis.auth.OAuth2.generateAuthUrl. Об этом подробно и с примерами написано в описании пакета
  7. После того как пользователь перейдет по ссылке и даст разрешение, мы получим access-токен

Почему access-токен живет только 1 час

По идее Google с access_type: «offline» должен предоставлять refresh-токен, но так просто он этого не сделает. Мне надо было обновлять список каналов пользователя в фоне, поэтому погуглив я нашел такой вариант: в метод generateAuthUrl передать опцию approval_prompt: ‘force’. Тогда Google будет запрашивать у пользователя автономный доступ к аккаунту и, если пользователь согласится, то даст нам нужный refresh-токен. С помощью него мы в любой момент времени сможем обновлять access-токен и получать список каналов.

Как передать данные пользователя через callback url?

Для этого в метод generateAuthUrl можно передать опцию state. Передавать можно только строку, поэтому все объекты нужно сериализовать/кодировать/шифровать.
По переданным данным можно сохранить токен в БД и потом получать его уже по ИД пользователя.

Google не даст токен если callback url это IP

Тут есть 2 пути: купить домен или попробовать зарегистрировать бесплатный.

Сначала я искал бесплатный, поэтому поделюсь с вами ссылкой на один из таких сервисов 4nmv — он дает домен 3 уровня бесплатно и позволяет настроить для его любые ns-записи. Наверняка есть и другие сервисы (поделитесь ссылочками, пожалуйста), но меня устроил и этот.

Но потом я всё же купил домен 2 уровня .com за 69 руб в GoDaddy для солидности.

Продакшн

В продакшне я использую Docker и Docker Compose. Они позволяют быстро поднимать и обновлять бота на разных хостингах (если это будет необходимо).

Docker логгер

Разрабатывая на Node.js я писал ошибки и отладочные сообщения в консоль и когда развернул в docker их смотреть стало не удобно.
На помощь может прийти сервис Papertrail, он позволяет сохранять любые сообщения откуда угодно в том числе в формате syslog.
Чтобы передать все сообщения из всех контейнеров можно использовать образ gliderlabs/logspout. Настраивается он очень просто. Вот вырезка из docker-compose.yml

  logger:     image: gliderlabs/logspout:latest     volumes:       - /var/run/docker.sock:/var/run/docker.sock     command: 'syslog+tls://logsX.papertrailapp.com:PORT'     restart: always

Как запустить Yarn в Docker-контейнере

... RUN curl -o- -L https://yarnpkg.com/install.sh | bash RUN $HOME/.yarn/bin/yarn install ...

Результат

Бот запущен в конце декабря 2016 года и доступен по имени @youtube_subs_watcher_bot
Исходники на GitHub
ссылка на оригинал статьи https://habrahabr.ru/post/319016/

Классы типов в Scala (с небольшим обзором библиотеки cats)

При слове "полиморфизм" сразу вспоминается объектно-ориентированное программирование, в котором полиморфизм является одним из столпов (Полиморфизм для начинающих). (Причём, по-видимому, более важным, чем другие столпы.) Оказывается, что можно достичь сходного эффекта и другим путём, который в ряде случаев оказывается более предпочтительным. Например, с помощью классов типов можно приписать новые возможности уже существующим типам, у которых нельзя изменить предка, или, используя тип данных с несовместимыми классами, "решить" проблему множественного наследования.

На Хабре уже есть несколько публикаций, дающих представление о классах типов:

  1. @IvanGolovach Разработка → "FP на Scala: Что такое функтор?" — 2015.
    Здесь затрагиваются классы типов при рассмотрении функторов. В ходе рассмотрения приводятся несколько примеров классов типов.
  2. Михаил Потанин @potan Разработка → "Классы типов на C++" — 2013.
    В этой публикации реализуют классы типов в C++. По-видимому, предполагается, что читатель в какой-то степени уже знаком с type class’ами, поэтому собственно про классы типов сказано не очень много.
  3. @VoidEx Разработка → "Классы типов, монады" — 2009.
    Описаны классы типов в Haskell (с примерами реализации на С++).
  4. @IvanP Разработка → "Полиморфия и классы в Haskell" — 2013.
    Описан параметрический и ad-hoc полиморфизм, классы типов и несколько стандартных классов. Всё описание сделано для языка Haskell.

В этом посте хотелось бы отразить взгляд на классы типов как на один из повседневных инструментов программистов. Причём, если в Haskell’е без них вообще не обойтись, то в Sсala можно прекрасно сносно жить, не зная об их существовании. Однако, при ближайшем рассмотрении оказывается, что такой инструмент весьма полезен при написании повторно используемых программ. Кроме того, есть целый ряд универсальных библиотек, которые основаны на этом инструменте, и, чтобы ими пользоваться, также надо понимать классы типов.

Неизменяемые типы данных

В функциональном программировании широкую популярность приобрели неизменяемые типы данных. Так как данные не могут произвольно меняться, то нет причины их скрывать, и вместо сокрытия данных теперь используются открытые типы, где данные — публичны. (Тем самым, среди трёх столпов ООП — полиморфизма, наследования и инкапсуляции, — один оказывается несколько задвинут в сторону.)
Свободно доступные данные позволяют использовать внешние алгоритмы их обработки. Нет необходимости привязывать алгоритмы к самому объекту и преодолевать искусственные барьеры между объектами, если алгоритм обработки использует несколько разнотипных объектов.
Оказывается, что значительная часть множества структур данных может быть смоделирована с использованием всего двух механизмов (Алгебраические типы данных). Во-первых, создание записей или кортежей ("тип-произведение"). Во-вторых, создание альтернативных реализаций одного родительского типа — enum, наследование интерфейсов, sealed trait — "тип-сумма".

Пример:

// тип-сумма следующих альтернативных реализаций: sealed trait Form object Form {   //  тип-произведение String X String   case class Form1(number: String, title: String) extends Form    // тип-произведение UUID X String X Int   case class Form2(id: UUID, documentation: String, count: Int) extends Form   // == Unit (тривиальное произведение нулевого количества типов)   case object BadForm extends Form }

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

  • сформировать HTML-представление для пользователя
  • выполнить валидацию по бизнес-правилам
  • сериализовать/десериализовать
  • рассчитать некоторые метрики

Все эти виды обработки реализуются обособленно и не обязаны ничего знать друг о друге. В таких алгоритмах основным способом обработки различных подтипов с доступам к реальным данным является pattern-matching. С помощью pattern-matching’а мы одновременно проверяем какого конкретного подтипа объект, и извлекаем значения интересующих нас полей.
Размещение алгоритмов вне конкретных типов обладает следующими преимуществами:

  1. Логика алгоритма не размазывается по каждому подтипу, а локализована в отдельном модуле.
  2. Логика одного способа обработки не перемешана с другими способами обработки внутри каждого класса данных. Упрощается поддержка разных алгоритмов разными разработчиками.
  3. Нет необходимости добавлять зависимость от библиотеки СУБД к модулю, в котором объявляется модель данных.
  4. Легко добавить новые методы обработки к существующим типам данных. Они добавляются "ортогонально" в независимых модулях.

Классы типов

Предположим, мы реализовали некоторый алгоритм вне наших типов данных. Если в этом алгоритме прямо используются наши типы, то мы не сможем его использовать повторно для других похожих данных. Это, с одной стороны, неплохо, так как такой алгоритм проще написать, но, с другой стороны, его общность ограничена. Это значит, в общем случае, что алгоритм будет использоваться реже, и, по-видимому, будет хуже протестирован (при тех же суммарных экономических затратах), либо затраты на поддержку будут выше.
Поэтому хотелось бы иметь механизмы, позволяющие обобщить наш алгоритм на другие типы данных (существующие и перспективные). Это позволит использовать тот же алгоритм во многих случаях и окупит затраты на его разработку и тестирование.
В ООП предлагается выделить общий интерфейс для "похожих" данных и реализовывать алгоритм в терминах этого общего интерфейса. В конкретных классах, наследующих этот интерфейс, нам достаточно реализовать эти общие методы. Тем самым мы получаем до некоторой степени полиморфный алгоритм. Однако эти операции, входящие в интерфейс "похожих" данных, нам необходимо реализовать в самих данных.
Классы типов представляют собой следующий шаг в обособлении кода, играющего разные роли в программе. Операции, которые мы хотим выполнять над данными, перемещаются в отдельный класс, не являющийся предком данных. В алгоритм вместе с данными передаётся экземпляр этого класса для этого типа данных.

Пример:
Пусть в интересующем нас алгоритме используется сравнение данных по порядку. Такое сравнение может быть представлено функцией

def compare[T](a: T, b: T): Int

Поместим эту функцию в класс типа Ordering:

trait Ordering[T] {   def compare(a: T, b: T): Int }

Теперь пусть наш универсальный алгоритм занимается сортировкой. Он должен принимать тип данных, с которыми он работает:

def sort[T](list: List[T]): List[T]

так как внутри алгоритма производится сравнение элементов, то этому алгоритму необходимо передать экземпляр класса Ordering для нашего типа T:

def sort[T: Ordering](list: List[T]): List[T]

либо, что тоже самое:

def sort[T](list: List[T])(implicit o: Ordering[T]): List[T]

Алгоритм при необходимости вызова операции compare должен получить экземпляр класса типа с помощью implicitly[Ordering[T]].compare(a,b).

Нам остаётся только предоставить экземпляр класса типа для нашего типа данных:

implicit object FormOrdering extends Ordering[Form] {    def compare(a: Form, b: Form): Int = (a,b) match {     case (Form1(numberA, titleA), Form1(numberB, titleB)) => numberA - numberB     case (BadForm, BadForm) => 0    ...     case _ => 0   } }

Таким образом, мы достигаем общности алгоритма без загромождения наших данных кусками кода, относящимися к специфическому алгоритму.

Дополнительное удобство

Как сделать методы доступными напрямую у самого типа? Например, мы бы хотели внутри алгоритма сравнивать объекты с помощью метода a compare b, без явного вызова метода класса типа.
Для этого используется обычный в Scala механизм pimp-my-library:

implicit class OrderingOps[T:Ordering](a:T){   def compare(b:T): Int = implicitly[Ordering[T]].compare(a,b) }

Тем самым, у всех типов, для которых имеется экземпляр Ordering, появится новый "метод" compare.
Если такое желание возникает всякий раз, то можно использовать библиотеку simulacrum, которая создаёт такой вспомогательный метод со всей необходимой обвязкой с помощью макросов:

import simulacrum.typeclass @typeclass trait Ordering[T]{    def compare(a: T, b: T): Int }

Пример: Класс типа для переписывания деревьев (символьное решение уравнений, оптимизация программ)

Рассмотрим пример класса типа для пользовательской структуры данных.
Одним из механизмов, используемых для оптимизации программ, является переписывание AST с сохранением семантики. При этом производится обход всех узлов дерева (в глубину или в ширину), и для каждого узла выполняется поиск соответствующего шаблона (pattern matching) и в случае сопоставления шаблона узел переписывается по соответствующему правилу.
Для разных задач (уравнения, программы) типы, составляющие дерево AST, отличаются, шаблоны сопоставления/оптимизации — тоже отличаются. Однако алгоритм обхода — одинаковый.
Этот алгоритм — претендент на абстрагирование с использованием классов типов. К произвольному типу дерева мы должны добавить какие-то операции, используемые в алгоритме обхода дерева.

import simulacrum.typeclass  @typeclass trait RewritableTree[T] {   def children(node: T): List[T]   def replaceChildren(node: T, newChildren: List[T]): T }

собственно алгоритм переписывания

object RewritableTree {   def rewrite[T: RewritableTree](f: PartialFunction[T, T]): T => T = t => {     rewrite0(f)(t).getOrElse(t)   }    private def rewrite0[T: RewritableTree](f: PartialFunction[T, T])(t: T): Option[T] = {     import RewritableTree.ops._  // импортируем "методы", сгенерированные simulacrum'ом       val rt = implicitly[RewritableTree[T]]   - мы могли бы обойтись и без этого "метода"     val children = t.children // rt.children(t)      var changed = false // кроме собственно переписывания, нам надо знать, произошло ли изменение, чтобы не переписывать всё дерево без надобности     val updatedChildren = children.map{child =>       val res = rewrite0(f)(child)       changed = changed || res.isDefined       res.getOrElse(child)     }     // альтернативная реализация без локальной изменяемой переменной     //def rewriteList(lst: List[T], result: mutable.ListBuffer[T], changed: Boolean): (List[T], Boolean) = lst match {     //  case Nil => (result.toList, changed)     //  case head :: tail =>     //    val res = rewrite0(f)(head)     //    rewriteList(tail, result.append(res.getOrElse(head)), changed || res.isDefined)     //}     //val (updatedChildren, changed) = rewriteList(t.children, mutable.ListBuffer(), false)     val updatedTree =       if(changed)         t.replaceChildren(updatedChildren)       else         t     var changed2 = true     val updatedTree2 = f.applyOrElse(t1, (_:T) =>{changed2 = false; updatedTree})     if(changed || changed2)       Some(updatedTree2)     else       None   } }

С использованием этого же класса типа можно реализовать метод collect, собирающий какие-либо значения по мере обхода дерева.

Индуктивное определение классов типов для производных типов

Предположим, у нас уже реализован класс типа Ordering[T] для нашего типа T. А мы бы хотели отсортировать список Option[T]. Нельзя ли нам воспользоваться уже реализованным классом типа и просто дополнить недостающую функциональность?
Это можно сделать, если мы будем предоставлять реализацию класса типа налету, конструируя реализацию из имеющихся классов типа.

implicit def optionOrdering[T:Ordering]: Ordering[Option[T]] = new Ordering[Option[T]] {   def compare(a: Option[T], b: Option[T]): Int = (a, b) match {     case (Some(c), Some(d)) => implicitly[Ordering[T]].compare(c,d)     case (None, None) => 0     case (_, None) => 1     case (None, _) => -1   } }

Такая реализация автоматически подставляется в алгоритм сортировки для любых типов, для которых есть экземпляр класса типа Ordering[T].
Аналогичным образом можно конструировать классы типов для любых generic-типов, таких как, List[T], Tuple2[A,B], …

Стандартные классы типов (cats)

Операции, которые объявляются внутри классов типов, могут быть любыми. Для нашего алгоритма мы можем провести границу абстракции достаточно произвольно, например, весь алгоритм поместить в класс типа, либо наоборот, в класс типа поместить непосредственно методы доступа к данным. Оба этих граничных варианта не являются оптимальными с точки зрения повторного использования. Поэтому в классы типов стоит помещать минимальное количество операций, которые несложно реализовать для других типов, и при этом наш алгоритм может быть выражен через эти операции.
Как только мы начинаем с этой точки зрения смотреть на алгоритмы и доступ к данным, мы с большой вероятностью можем прийти к каким-либо часто используемым классам типов.

В стандартной библиотеке Scala имеется несколько классов типов: Ordering[T], Equiv[T], Numeric[T], Integral[T], …

В библиотеке typelevel/cats (как и в библиотеке scalaz) объявлено несколько дополнительных классов для простых типов с часто используемыми операциями (http://typelevel.org/cats/typeclasses.html):

  1. Полугруппа (Semigroup) — одна операция combine.
  2. Моноид — полугруппа с пустым ("нулевым") элементом — empty.

Например, для чисел можно определить операцию combine как сумму чисел, в этом случае нулевым элементом будет обычный ноль. Мы получим аддитивный моноид. Также можно использовать мультипликативный моноид, если в качестве операции combine взять умножение, а empty — единицу. Список чисел также можно рассматривать как моноид. В роли операции combine будет выступать склейка списков, а нулевым элементом — пустой список.

Пример:
Можно использовать моноид для реализации накопления. Создаём состояние с начальным значением, равным empty из моноида. Далее при поступлении данных на вход мы можем их объединить combine с теми, что уже находятся в состоянии. Например, можем взять тип Int с операцией "сумма". В этом случае в одном значении будет накапливаться сумма поступающих данных. Или взять моноид для List[T]. В этом случае все данные будут доступны в этом списке (на входе должны быть списки, либо каждое число надо будет обернуть в список). Алгоритм накопления в обоих случая является идентичным — вызвать метод combine для существующих данных и вновь поступивших. И алгоритм не зависит от конкретного типа, с которым он работает.
Также, если про какой-то тип нам известно, что он моноид (т.е. имеется экземпляр класса моноид для этого типа), то мы можем использовать foldLeft — свёртку для коллекции этих элементов (нам не надо её заново реализовывать).

Типы высших порядков

Кроме простых базовых типов классы типов могут использоваться для работы с типами, которые сами по себе имеют параметры. (Тем самым, класс типа требует поддержки типов высших порядков в языке.) Типы высших порядков характеризуются kind’ом — "типом типа":

  • простой тип имеет kind * (например, Int, String);
  • тип, принимающий один аргумент — * -> * (например, List[T], Option[T], Future[T]);
  • тип, принимающий два аргумента — * -> * -> * (например, функция Function1[A,B]). (Обратите внимание, что сама функция на уровне значений содержит одну стрелку A => B, а на уровне типов — A => B => (A=>B) — две стрелки (третья стрелочка — уже внутри самого типа).)

В библиотеке cats кроме классов типов, работающих с базовыми типами, есть классы типов, используемые при работе с конструкторами типов. В частности, для типов * -> *:

  1. Функтор — класс типа, содержащий одну операцию — map. Операция принимает объект, например, типа List[Int]и применяет указанную функцию к каждому элементу. Для List‘а и для Option, эта операция, вообще говоря, уже реализована в самом типе данных, и можно было бы не создавать класс типа. Однако, если мы хотим реализовывать универсальные алгоритмы с использованием операции map, то такой класс типа необходим.
  2. Монада — функтор, содержащий операцию flatMap, или bind, или >>= (а также flatten, map, pure). Этот класс типа, по-видимому, самый знаменитый. Его польза обусловлена тем фактом, что flatMap (bind) является достаточно универсальным способом склейки последовательных вычислений. На операции flatMap даже основан "сахар" в Scala — for-comprehension.

Пример:

  • Обработка списков. Собрать всех детей для коллекции объектов — val allChildren = objects.flatMap(_.children)
  • Обработка отсутствующих значений: val street = personOpt.flatMap(_.addressOpt).flatMap(_.streetOpt)
  • Отложенное исполнение запросов. Пусть результат некоторого запроса из БД может быть представлен типом DataTable[T]. С помощью flatMap мы можем определить подзапрос, извлекающий данные из результатов этого запроса. Такой запрос можно приклеить к исходному запросу, не выполняя первого запроса и не работая с коллекцией результатов. Склеенный запрос мы можем скомпилировать в SQL и отправить в БД для исполнения на стороне СУБД. Такой подход реализуется, например, в библиотеке Slick.

Для типов * -> * -> * в библиотеке cats тоже есть класс типа:

  1. Категория — операция compose + "нулевой" элемент — identity. Тип, для которого определён класс типа "категория", называется "стрелкой" (Arrow). Стрелки похожи на функции. В частности, для обычных функций операция compose соответствует методу andThen, а операция identity — функции identity.

Примеры категорий:

  1. Обычные функции.
  2. Модельные функции (в модельном языке).
  3. Линзы (свойства объектов, отделённые от классов) (см. библиотеку monocle).
  4. Направленный граф в Functional Reactive Programming (например, SynapseGrid).

Пример:
Для категорий ключевой возможностью является compose. Т.е. если наш алгоритм удаётся выразить в терминах compose, то мы можем этот алгоритм применить для любых категорий.
Пусть мы моделируем цепочку преобразований данных с помощью собственного DSL. Допустим, что каждое преобразование может быть представлено некоторым типом Transform[A,B].

фантомные типы

A и B — не обязательно типы из нашей модели данных. Это могут быть так называемые фантомные типы. Использование фантомных типов позволяет определить свои правила для разрешённых комбинаций преобразований, которые будут проверяться компилятором. Т.е. мы не сможем использовать метод compose для несовместимых преобразований.

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

Законы для классов типов

Классы типов, реализованные в библиотеке cats, моделируют понятия теории категорий. Поэтому методы этих классов типов должны удовлетворять определённым свойствам (которые описаны в теории). Например, для моноида:

  1. a combine empty = a = empty combine a — определение пустого элемента
  2. (a combine b) combine c = a combine (b combine c) — ассоциативность операции combine

Все свойства, которые требуются теорией категорий для класса типа, реализованы в виде "законов" — наборов "свойств" библиотеки ScalaCheck. И можно для любого экземпляра класса типа выполнить проверку, удовлетворяет ли этот экземпляр требованиям, предъявляемым к этому классу типа. Многие алгоритмы опираются на эти свойства, поэтому при реализации классов типов для своих данных, следует эти законы проверять в unit test’ах.
После того, как мы убедились, что наши реализации классов типов удовлетворяют имеющимся законам, мы можем быть в значительной степени уверены в корректности нашей программы, использующей алгоритмы из библиотек, опирающиеся на эти свойства классов типов.

Преимущества классов типов

По сравнению с обычными интерфейсами, которые необходимо реализовывать в потомках, классы типов обладают такими преимуществами:

  • можно реализовать класс для недоступного типа;
  • можно объявить операции, работающие с нулевым количеством экземпляров данного типа. В частности, метод empty: T, или метод parse: String => T;
  • можно индуктивным образом определить экземпляр составного типа, если есть экземпляры для базовых типов. Например, для Option[T] или для A \/ B.

Этими преимуществами можно воспользоваться самостоятельно в любой программе. Достаточно по-другому взглянуть на структуру своего кода.

В библиотеке cats (а также в библиотеке scalaz) имеется хорошо организованный и протестированный набор классов типов (из теории категорий), которые применяются во многих алгоритмах и библиотеках. При использовании этой библиотеки можно получить весьма универсальный программый код, который, к тому же будет хорошо протестирован уже написанными тестами и в некоторой степени валидирован с помощью проработанной математической теории.

ссылка на оригинал статьи https://habrahabr.ru/post/318960/

О сравнении объектов по значению — 5: Structure Equality Problematic

В предыдущей публикации мы вывели наиболее полный и корректный способ реализации сравнения по значению объектов — экземпляров классов (являющихся ссылочными типами — Reference Types) для платформы .NET.

Каким образом нужно модифицировать предложенный способ для корректной реализации сравнения по значению объектов — экземпляров структур (являющихся "типами по значению" — Value Types)?

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

Для предопределенных типов, таких как Boolean или Int32, под сравнением по значению понимается сравнение непосредственно значений экземпляров структур.

Если структура определена разработчиком — пользователем платформы, то сравнение по умолчанию автоматически реализуется как сравнение значений полей экземпляров структур.
(Подробности см. в описании метода ValueType.Equals(Object) и операторов == и !=)
Также при этом автоматически определенным образом реализуется метод ValueType.GetHashCode(), перекрывающий метод Object.GetHashCode().

И в этом случае есть несколько существенных подводных камней:

  1. При сравнении значений полей используется рефлексия, что влияет на производительность.

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

  3. Может оказаться, что с предметной точки зрения не все поля должны участвовать в сравнении (хотя, опять же, для структур в общем случае это можно считать неверным решением).

  4. И наконец, дефолтная реализация метода ValueType.GetHashCode() не соответствует общим требованиям к реализации метода GetHashCode() (о которых мы говорили в первой публикации):
    • значение хеш-кода, полученное с помощью ValueType.GetHashCode(), может оказаться непригодным для использования в качестве ключа в хеш-таблице;
    • если значение одного или нескольких полей объекта изменилось, то значение, полученное с помощью ValueType.GetHashCode(), также может оказаться непригодным для использования ключа в хеш-таблице;
    • в документации рекомендуется создавать собственную реализацию метода GetHashCode(), наиболее точно отражающую концепцию хеш-кода для данного типа.

Таким образом, с одной стороны, есть несколько причин общего характера, подталкивающих к реализации у структур собственного механизма сравнения объектов по значению (производительность, соответствие доменной модели).

С другой стороны, необходимость корректной реализации метода GetHashCode() автоматически приводит к необходимости реализации сравнения по значению, т.к. метод GetHashCode() в силу природы хеш-кода (см. первую публикацию) должен "знать", какие данные (поля) и как участвуют в сравнении по значению.

С третьей стороны, возможен и особый случай, когда есть "простая" структура, состоящая, например, только из полей-структур, для которых побайтовое сравнение с помощью рефлексии заведомо дает сементически верный результат (например, Int32).
В этом случае возможно реализовать GetHashCode() корректным образом (чтобы для равных объектов хеш-код всегда был один и тот же), не создавая при этом собственную реализацию сравнения по значению.

Например:

Simple Point Structure

    public struct Point     {         private int x;          private int y;          public int X {             get { return x; }             set { x = value; }         }          public int Y         {             get { return y; }             set { y = value; }         }          public override int GetHashCode() => x.GetHashCode() ^ y.GetHashCode();     }

Однако, в случае переписывания этого простого примера с использованием "автосвойств" картина выглядит уже менее ясной:

Simple Point Structure with Auto-Implemented Properties

    public struct Point     {         public int X { get; set; }          public int Y { get; set; }          public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode();     }

В документации к автосвойствам говорится об автоматическом создании anonymous backing field, соответствующим публичным свойствам.
Строго говоря, из описания неясно, будут ли равными с точки зрения дефолтной реализации сравнения по значению два объекта Point с попарно одинаковыми значениями X и Y:

  • Если дефолтная реализация сравнивает с помощью рефлексии значения полей, то как для разных объектов происходит сопоставление анонимных полей — что эти поля соответствуют друг друга, т.к. каждое соответствует свойству X, а эти соответствуют друг другу, т.к. каждое соответствует Y?
    Что если в двух разных объектах создаются backing-поля с разными именами вида (x1, y1) и (x2, y2)?
    Будет ли учитываться при сравнении, что x1 соответствует x2, а y1 соответствует y2?
  • Создаются ли при этом еще какие-то вспомогательные поля, которые могут иметь разные значения для одинаковых с точки зрения интерфейса (X, Y) объектов? Если да, то будут ли учитываться эти поля при сравнении?
  • Или, возможно, в случае структуры с автосвойствами, будет использоваться побайтовое сравнение всего содержимого структуры, без сравнения отдельных полей? Если да, то backing-поля для каждого объекта будут создаваться в памяти всегда в одном и том же порядке и с одинаковыми смещениями?

Скорее всего, где-то в недрах документации или книгах авторов, имеющих отношение к разработке платформы, можно найти положительные ответы на эти вопросы — в том смысле, что поведение для структур с явно объявленными полями и структур с автосвойствами будет давать одинаковый результат при дефолтном сравнении по значению.
Либо, если какая-то часть окажется недокументированной, то, скорее всего, поведение компилятора и исполняющей среды окажется ожидаемым.

Тем не менее, представляется, что в общем случае для структур предпочтительнее всегда реализовывать собственное сравнение по значению.
Развернутый пример с подробными комментариями, на основе знакомой по предыдущим публикациям сущности Person, рассмотрим в следующей публикации.

ссылка на оригинал статьи https://habrahabr.ru/post/315622/

Настройка SPICE-консоли виртуальных машин в OpenStack

Эта статья будет интересна администраторам облачной платформы OpenStack. Речь пойдет об отображении консоли виртуальных машин в дашборде. Дело в том, что по умолчанию в OpenStack используется noVNC консоль, которая с приемлемой скоростью работает в рамках локальной сети, но плохо подходит для работы с виртуалками, запущенными в удаленном датацентре. В этом случае отзывчивость консоли, мягко говоря, удручает.
В данном статье речь пойдет о том, как настроить в своей инсталляции Опенстека гораздо более быструю SPICE-консоль.

В Опенстеке есть два протокола графической консоли виртуалок — VNC и SPICE. Из коробки идет веб-реализация VNC клиента — noVNC.
Про SPICE знают гораздо меньше людей. Вообще, SPICE — это протокол удаленного доступа, который поддерживает массу полезных вещей, таких как передача потокового видео, передача аудио, копипаст, проброс USB и многое другое. Однако, в дашборде Опенстека используется SPICE-HTML5 веб-клиент, который все эти функции не поддерживает, но зато очень эффективно и быстро отображает консоль виртуалок, то бишь делает как раз то, что нужно.

В официальной документации (ссылка1, ссылка2) Опенстека довольно мало информации по настройке SPICE-консоли. К тому же говорится, что «VNC must be explicitly disabled to get access to the SPICE console». Это не совсем правда, скорее речь идет о том, что при включенной VNC-консоли вы не сможете из дашборда воспользоваться SPICE-консолью (но по прежнему сможете используя API, то бишь «nova get-spice-console», используя python-novaclient). К тому же SPICE-консоль будет доступна только для новых виртуалок, старые до хард-ребута, ресайза или миграции по-прежнему будут использовать только VNC.

Итак, в данной статье я использовал сразу две версии Опенстека от Mirantis: Kilo (Mirantis OpenStack 7.0) и Mitaka (Mirantis OpenStack 9.0). Как и во всех enterprise-дистрибутивах используется конфигурация с тремя контроллер-нодами и HTTPS на фронтенде. Гипервизор qemu-kvm, операционка везде Ubuntu 14.04, деплой облака осуществлялся через Fuel.

Конфигурация застрагивает и контроллер-ноды и компут. На контроллер-нодах делаем следующее.
Ставим сам пакет spice-html5:

apt-get install spice-html5

В конфиг Nova вносим следующие значения:
/etc/nova/nova.conf

[DEFAULT] ssl_only = True cert = '/path/to/SSL/cert' key = '/path/to/SSL/key' web=/usr/share/spice-html5  [spice] spicehtml5proxy_host = :: html5proxy_base_url = https://<FRONTEND_FQDN>:6082/spice_auto.html enabled = True keymap = en-us

где <FRONTEND_FQDN> — это FQDN вашего Horizon-дашборда. Очевидным образом, сертификат и ключ выше должны соответствовать FRONTEND_FQDN, иначе современный браузер не даст работать SPICE-виджету. Если у вас Horizon не юзает HTTPS, то настройки SSL можно не делать.

Для одновременной работы noVNC и SPICE нужно проделать такой финт:

cp -r /usr/share/novnc/* /usr/share/spice-html5/

Для работы через HTTPS нужно использовать Secure Websockets, для этого придется поправить файл /usr/share/spice-html5/spice_auto.html. В данном участке кода нужно исправить «ws://» на «wss://»

/usr/share/spice-html5/spice_auto.html

            function connect()             {                 var host, port, password, scheme = "wss://", uri;

Опять же для одновременной работы noVNC и SPICE необходимо поправить upstart-скрипты /etc/init/nova-novncproxy.conf и /etc/init/nova-spicehtml5proxy.conf. В обоих скриптах нужно закомментить одну строчку:

/etc/init/nova-spicehtml5proxy.conf

script     [ -r /etc/default/nova-consoleproxy ] && . /etc/default/nova-consoleproxy || exit 0     #[ "${NOVA_CONSOLE_PROXY_TYPE}" = "spicehtml5" ] || exit 0

/etc/init/nova-novncproxy.conf

script     [ -r /etc/default/nova-consoleproxy ] && . /etc/default/nova-consoleproxy || exit 0     #[ "${NOVA_CONSOLE_PROXY_TYPE}" = "novnc" ] || exit 0

Собственно, это позволяет убрать проверку типа консоли из файла /etc/default/nova-consoleproxy.

Теперь нужно поправить конфиги Haproxy:

/etc/haproxy/conf.d/170-nova-novncproxy.cfg

listen nova-novncproxy   bind <PUBLIC_VIP>:6080 ssl crt /var/lib/astute/haproxy/public_haproxy.pem no-sslv3 no-tls-tickets ciphers AES128+EECDH:AES128+EDH:AES256+EECDH:AES256+EDH   balance  source   option  httplog   option  http-buffer-request   timeout  http-request 10s   server controller1 192.168.57.6:6080 ssl verify none check   server controller2 192.168.57.3:6080 ssl verify none check   server controller3 192.168.57.7:6080 ssl verify none check

/etc/haproxy/conf.d/171-nova-spiceproxy.cfg

listen nova-novncproxy   bind <PUBLIC_VIP>:6082 ssl crt /var/lib/astute/haproxy/public_haproxy.pem no-sslv3 no-tls-tickets ciphers AES128+EECDH:AES128+EDH:AES256+EECDH:AES256+EDH   balance  source   option  httplog   timeout tunnel 3600s   server controller1 192.168.57.6:6082 ssl verify none check   server controller2 192.168.57.3:6082 ssl verify none check   server controller3 192.168.57.7:6082 ssl verify none check

где PUBLIC_VIP — это IP-адрес, на котором висит FRONTEND_FQDN.

Наконец, рестартуем сервисы на контроллер нодах:

service nova-spicehtml5proxy restart  service apache2 restart crm resource restart p_haproxy

здесь p_haproxy — это Pacemaker-ресурс для Haproxy, через который работают многочисленные сервисы Опенстека.

На каждой compute-ноде нужно внести изменения в конфиг Новы:
/etc/nova/nova.conf

[spice] spicehtml5proxy_host = :: html5proxy_base_url = https://<FRONTEND_FQDN>:6082/spice_auto.html enabled = True agent_enabled = True server_listen = :: server_proxyclient_address = COMPUTE_MGMT_IP keymap = en-us

здесь COMPUTE_MGMT_IP — адрес management-интерфейса данной compute-ноды (в Mirantis OpenStack есть разделение на public и management сети).

После этого нужно рестартовать сервис nova-compute:

service nova-compute restart

Теперь один важный момент. Я уже писал выше, что мы не выключаем VNC, т.к. в этом случае уже существующие виртуалки потеряют консоль в Дашборде. Однако, если мы деплоим облако с нуля, то имеет смысл вовсе выключить VNC. Для этого нужно в конфиге Новы на всех нодах выставить:

[DEFAULT] vnc_enabled = False novnc_enabled = False

Но так или иначе, если мы активируем VNC и SPICE вместе в облаке, в котором уже крутятся виртуалки, то внешне ничего не изменится ни для уже запущенных виртуалок, ни для новых — все равно будет открываться noVNC консоль. Если посмотреть в настройки Horizon, то тип используемой консоли управляется следующей настройкой:

/etc/openstack-dashboard/local_settings.py

# Set Console type: # valid options would be "AUTO", "VNC" or "SPICE" # CONSOLE_TYPE = "AUTO"

По умолчанию, значение AUTO, то бишь типа консоли выбирается автоматически. Но что это означает? Дело в одном файле, где выставляется приоритет консолей:

/usr/share/openstack-dashboard/openstack_dashboard/dashboards/project/instances/console.py

... CONSOLES = SortedDict([('VNC', api.nova.server_vnc_console),                        ('SPICE', api.nova.server_spice_console),                        ('RDP', api.nova.server_rdp_console),                        ('SERIAL', api.nova.server_serial_console)]) ...

Как видите, приоритет имеет VNC консоль, если она есть. Если ее нет, то будет искаться SPICE консоль. Имеет смысл поменять первые два пункта местами, тогда существующие виртуалки будут по-прежнему работать с медленной VNC, а новые — с новой быстрой SPICE. Как раз то, что нужно!

Субъективно, можно сказать, что SPICE-консоль очень быстрая. В режиме без графики тормозов нет вообще, в графическом режиме все работает тоже быстро, а по сравнению с VNC-протоколом — просто небо и земля! Так что всем рекомендую!

На этом настройку можно считать законченной, однако в конце покажу, как, собственно говоря, выглядят в XML-конфиге libvirt обе этих консоли:

    <graphics type='vnc' port='5900' autoport='yes' listen='0.0.0.0' keymap='en-us'>       <listen type='address' address='0.0.0.0'/>     </graphics>     <graphics type='spice' port='5901' autoport='yes' listen='::' keymap='en-us'>       <listen type='address' address='::'/>     </graphics>

Очевидно, что если у вас есть сетевой доступ к compute-ноде виртуалки, то вы можете использовать вместо веб-интерфейса любой другой клиент VNC/SPICE, просто подключаясь к вышеуказанному в конфигурации TCP порту (в данном случае 5900 для VNC и 5901 для SPICE).
ссылка на оригинал статьи https://habrahabr.ru/post/319072/

В Elite: Dangerous появились корабли неизвестной расы


Излучение генерируют корабли «чужих». Что это за излучение — пока неясно

В июле этого года геймеры Elite: Dangerous стали массово сообщать о странных зондах и сигналах, которые появляются в определенных зонах. Собственно, все бы ничего, ведь пока что игроки освоили не такую уж и большую часть игрового пространства (по словам разработчиков, здесь около 400 000 000 000 звезд). Собственно, те же разработчики и раньше сообщали о том, что в ходе игры новоявленным звездоплавателям будут открываться необычные вещи и встречаться странные объекты. Но с середины прошлого года странного и необычного стало еще больше, чем раньше.

Неидентифицированные объекты («unknown artifacts») встречаются игрокам на протяжении многих месяцев. Появились в игре и неизвестные зонды «unknown probes»). При попытке изучения такого зонда корабль геймера начинает светиться, после чего выключаются все системы, до момента, пока зонд не удалится на определенное расстояние. Плюс ко всему, эти системы еще и отправляют некое сообщение, из которого можно выделить схематичное изображение чего-то, что напоминает планету.

Обо всем этом сообщалось ранее. Некоторые геймеры воспринимают случившееся как прямое указание на скорое вторжение таргоидов (Thargoids). Эти существа выше человека, язык их слабо поддается расшифровке, сами они — полулегенда.

Сейчас начали появляться первые свидетельства того, что вторжение действительно может произойти. В игре появились необычные корабли, которые принадлежат неизвестной расе (таргоидам или нет — пока неясно). Но эти корабли отличаются от всего, виденного игроками ранее. Корабли довольное большие, симметричные, оснащены неизвестными источниками энергии и рядом непонятных инструментов.

Первым геймером, который столкнулся с этими объектами, стал владелец Xbox One DP Sayre. Видео эти вряд ли являются фейками, поскольку размещение роликов на Xbox DVR более сложная задача, чем простая публикация чего-либо на YouTube. Кстати, на этом сервисе тоже появилось несколько роликов, где показаны неизвестные корабли.

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

Встреча принесла много неожиданного. После того, как игрок вышел из гиперпространства (вернее, похоже на то, что его оттуда вывели), чужой корабль подошел почти вплотную и выполнил какие-то действия. Что он сделал — непонятно. Но в это время корабль геймера оставался недвижим и заработал только после того, как «чужие» удалились восвояси.

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

К сожалению, пока что о том, что происходит, можно лишь строить догадки. Если это действительно таргоиды, то геймерам можно готовиться к войне — ведь встреча с этими существами не сулит ничего хорошего. Суть и линию поведения таргоидов разработчики меняют от игры к игре, но одно остается постоянным. Они всегда таинственные, всегда обладают мощью и всегда опасны.

Что это может означать для игры? Будет ли осуществлено масштабное вторжение из соседней (или не соседней) галактики? Что делать игрокам? Ничего не ясно. А разработчики, как всегда, хранят секреты и не выдают никаких подробностей. После того, как начали массово появляться слухи о том, что в Elite: Dangerous есть «чужие», разработчики заявили буквально следующее: «Мы сейчас изучаем информацию о необычных встречах в среде Elite: Dangerous, но мы не можем комментировать галактические слухи и спекуляции». Что же, ничего не поделаешь, придется ждать.

Игра до сих пор находится в состоянии разработки, хотя и в этом статусе смогла стать очень популярной. Сюжетной линии, той, к которой привыкли геймеры других игр, здесь нет. Точнее, есть определенный сюжет, но он далеко не линейный, зато динамичный. Плюс всего этого в том, что разработчикам удается подкидывать все новые и новые сюрпризы, которые так нравятся геймерам.
ссылка на оригинал статьи https://geektimes.ru/post/284368/